diff --git a/.github/workflows/ci-build-upload-binaries.yml b/.github/workflows/ci-build-upload-binaries.yml index 00c28191bd..2a0f46f75e 100644 --- a/.github/workflows/ci-build-upload-binaries.yml +++ b/.github/workflows/ci-build-upload-binaries.yml @@ -62,10 +62,6 @@ jobs: echo 'RUSTFLAGS="--cfg tokio_unstable"' >> $GITHUB_ENV if: github.event_name == 'workflow_dispatch' && inputs.add_tokio_unstable == true - - name: Set CARGO_FEATURES - run: | - echo 'CARGO_FEATURES=--features wireguard' >> $GITHUB_ENV - - name: Install Rust stable uses: actions-rs/toolchain@v1 with: @@ -91,7 +87,7 @@ jobs: - name: Upload Artifact if: github.event_name == 'workflow_dispatch' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: nym-binaries-artifacts path: | diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 19d5fc327e..268de52c55 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -73,29 +73,27 @@ jobs: uses: actions-rs/cargo@v1 with: command: build - # Enable wireguard by default on linux only - args: --workspace --features wireguard - name: Build all examples if: matrix.os == 'custom-linux' uses: actions-rs/cargo@v1 with: command: build - args: --workspace --examples --features wireguard + args: --workspace --examples - name: Run all tests if: matrix.os == 'custom-linux' uses: actions-rs/cargo@v1 with: command: test - args: --workspace --features wireguard + args: --workspace - name: Run expensive tests if: (github.ref == 'refs/heads/develop' || github.event.pull_request.base.ref == 'develop' || github.event.pull_request.base.ref == 'master') && matrix.os == 'custom-linux' uses: actions-rs/cargo@v1 with: command: test - args: --workspace --features wireguard -- --ignored + args: --workspace -- --ignored - name: Annotate with clippy checks if: matrix.os == 'custom-linux' @@ -103,10 +101,10 @@ jobs: continue-on-error: true with: token: ${{ secrets.GITHUB_TOKEN }} - args: --workspace --features wireguard + args: --workspace - name: Clippy uses: actions-rs/cargo@v1 with: command: clippy - args: --workspace --all-targets --features wireguard -- -D warnings + args: --workspace --all-targets -- -D warnings diff --git a/.github/workflows/ci-contracts-upload-binaries.yml b/.github/workflows/ci-contracts-upload-binaries.yml index 4347e5f333..14d0b63eae 100644 --- a/.github/workflows/ci-contracts-upload-binaries.yml +++ b/.github/workflows/ci-contracts-upload-binaries.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - platform: [ubuntu-20.04] + platform: [ ubuntu-20.04 ] runs-on: ${{ matrix.platform }} env: @@ -58,6 +58,7 @@ jobs: cp contracts/target/wasm32-unknown-unknown/release/nym_coconut_dkg.wasm $OUTPUT_DIR cp contracts/target/wasm32-unknown-unknown/release/cw3_flex_multisig.wasm $OUTPUT_DIR cp contracts/target/wasm32-unknown-unknown/release/cw4_group.wasm $OUTPUT_DIR + cp contracts/target/wasm32-unknown-unknown/release/nym_ecash.wasm $OUTPUT_DIR - name: Deploy branch to CI www continue-on-error: true diff --git a/.github/workflows/nightly-security-audit.yml b/.github/workflows/nightly-security-audit.yml index 808cc20144..0addd5a6ce 100644 --- a/.github/workflows/nightly-security-audit.yml +++ b/.github/workflows/nightly-security-audit.yml @@ -20,7 +20,7 @@ jobs: find . -name Cargo.toml -exec cargo deny --manifest-path {} check \ advisories -A advisory-not-detected --hide-inclusion-graph \; &> \ >(uniq &> .github/workflows/support-files/notifications/deny.message ) - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: report path: .github/workflows/support-files/notifications/deny.message @@ -31,7 +31,7 @@ jobs: - name: Check out repository code uses: actions/checkout@v2 - name: Download report from previous job - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: report path: .github/workflows/support-files/notifications diff --git a/.github/workflows/publish-nym-binaries.yml b/.github/workflows/publish-nym-binaries.yml index be70c514ba..039598439a 100644 --- a/.github/workflows/publish-nym-binaries.yml +++ b/.github/workflows/publish-nym-binaries.yml @@ -51,10 +51,6 @@ jobs: echo 'RUSTFLAGS="--cfg tokio_unstable"' >> $GITHUB_ENV if: github.event_name == 'workflow_dispatch' && inputs.add_tokio_unstable == true - - name: Set CARGO_FEATURES - run: | - echo 'CARGO_FEATURES=--features wireguard' >> $GITHUB_ENV - - name: Install Rust stable uses: actions-rs/toolchain@v1 with: @@ -67,7 +63,7 @@ jobs: args: --workspace --release ${{ env.CARGO_FEATURES }} - name: Upload Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: my-artifact path: | diff --git a/.github/workflows/publish-nym-contracts.yml b/.github/workflows/publish-nym-contracts.yml index 493f99e8ac..189c906811 100644 --- a/.github/workflows/publish-nym-contracts.yml +++ b/.github/workflows/publish-nym-contracts.yml @@ -26,14 +26,14 @@ jobs: run: make contracts - name: Upload Mixnet Contract Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: mixnet_contract.wasm path: contracts/target/wasm32-unknown-unknown/release/mixnet_contract.wasm retention-days: 5 - name: Upload Vesting Contract Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: vesting_contract.wasm path: contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm diff --git a/.github/workflows/publish-nym-wallet-macos.yml b/.github/workflows/publish-nym-wallet-macos.yml index 4ecc48d276..9357555d3b 100644 --- a/.github/workflows/publish-nym-wallet-macos.yml +++ b/.github/workflows/publish-nym-wallet-macos.yml @@ -83,7 +83,7 @@ jobs: run: yarn && yarn build - name: Upload Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: nym-wallet.app.tar.gz path: nym-wallet/target/release/bundle/macos/nym-wallet.app.tar.gz diff --git a/.github/workflows/publish-nym-wallet-ubuntu.yml b/.github/workflows/publish-nym-wallet-ubuntu.yml index 1eaad9bef0..833bfaa745 100644 --- a/.github/workflows/publish-nym-wallet-ubuntu.yml +++ b/.github/workflows/publish-nym-wallet-ubuntu.yml @@ -62,7 +62,7 @@ jobs: TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} - name: Upload Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: nym-wallet_1.0.0_amd64.AppImage.tar.gz path: nym-wallet/target/release/bundle/appimage/nym-wallet*.AppImage.tar.gz diff --git a/.github/workflows/publish-nym-wallet-win10.yml b/.github/workflows/publish-nym-wallet-win11.yml similarity index 96% rename from .github/workflows/publish-nym-wallet-win10.yml rename to .github/workflows/publish-nym-wallet-win11.yml index 5bba585f18..312bcac6cf 100644 --- a/.github/workflows/publish-nym-wallet-win10.yml +++ b/.github/workflows/publish-nym-wallet-win11.yml @@ -1,4 +1,4 @@ -name: publish-nym-wallet-win10 +name: publish-nym-wallet-win11 on: workflow_dispatch: release: @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - platform: [windows10] + platform: [custom-windows-11] runs-on: ${{ matrix.platform }} outputs: @@ -62,6 +62,9 @@ jobs: fileName: '.env' encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }} + - name: Install Yarn + run: npm install -g yarn + - name: Install project dependencies shell: bash run: cd .. && yarn --network-timeout 100000 @@ -82,7 +85,7 @@ jobs: run: yarn build - name: Upload Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: nym-wallet_1.0.0_x64_en-US.msi path: nym-wallet/target/release/bundle/msi/nym-wallet_1.*.msi diff --git a/.github/workflows/publish-nyms5-android-apk.yml b/.github/workflows/publish-nyms5-android-apk.yml index 06f4c6c126..5df103cc0b 100644 --- a/.github/workflows/publish-nyms5-android-apk.yml +++ b/.github/workflows/publish-nyms5-android-apk.yml @@ -84,7 +84,7 @@ jobs: apk/nyms5-arch64-release.apk - name: Upload APKs - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: nyms5-apk-arch64 path: | @@ -99,7 +99,7 @@ jobs: - name: Checkout uses: actions/checkout@v3 - name: Download binary artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: nyms5-apk-arch64 path: apk diff --git a/.github/workflows/release-calculate-hash.yml b/.github/workflows/release-calculate-hash.yml index fb493b8541..94c764e6e0 100644 --- a/.github/workflows/release-calculate-hash.yml +++ b/.github/workflows/release-calculate-hash.yml @@ -30,7 +30,7 @@ jobs: with: release-tag-or-name-or-id: ${{ inputs.release_tag }} - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: Asset Hashes path: hashes.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a39429e55..fbed61bd54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,60 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ## [Unreleased] +## [2024.10-caramello] (2024-09-10) + +- Backport 4844 and 4845 ([#4857]) +- Bugfix/client registration vol2 ([#4856]) +- Remove wireguard feature flag and pass runtime enabled flag ([#4839]) +- Eliminate cancel unsafe sig awaiting ([#4834]) +- added explicit updateable admin to the mixnet contract ([#4822]) +- using legacy signing payload in CLI and verifying both variants in contract ([#4821]) +- adding ecash contract address ([#4819]) +- Check profit margin of node before defaulting to hardcoded value ([#4802]) +- Sync last_seen_bandwidth immediately ([#4774]) +- Feature/additional ecash nym cli utils ([#4773]) +- Better storage error logging ([#4772]) +- bugfix: make sure DKG parses data out of events if logs are empty ([#4764]) +- Fix clippy on rustc beta toolchain ([#4746]) +- Fix clippy for beta toolchain ([#4742]) +- Disable testnet-manager on non-unix ([#4741]) +- Don't set NYM_VPN_API to default ([#4740]) +- Update publish-nym-binaries.yml ([#4739]) +- Update ci-build-upload-binaries.yml ([#4738]) +- Add NYM_VPN_API to network config ([#4736]) +- Re-export RecipientFormattingError in nym sdk ([#4735]) +- Persist wireguard peers ([#4732]) +- Fix tokio error in 1.39 ([#4730]) +- Feature/vesting purge plus ranged cost params ([#4716]) +- Fix (some) feature unification build failures ([#4681]) +- Feature Compact Ecash : The One PR ([#4623]) + +[#4857]: https://github.com/nymtech/nym/pull/4857 +[#4856]: https://github.com/nymtech/nym/pull/4856 +[#4839]: https://github.com/nymtech/nym/pull/4839 +[#4834]: https://github.com/nymtech/nym/pull/4834 +[#4822]: https://github.com/nymtech/nym/pull/4822 +[#4821]: https://github.com/nymtech/nym/pull/4821 +[#4819]: https://github.com/nymtech/nym/pull/4819 +[#4802]: https://github.com/nymtech/nym/pull/4802 +[#4774]: https://github.com/nymtech/nym/pull/4774 +[#4773]: https://github.com/nymtech/nym/pull/4773 +[#4772]: https://github.com/nymtech/nym/pull/4772 +[#4764]: https://github.com/nymtech/nym/pull/4764 +[#4746]: https://github.com/nymtech/nym/pull/4746 +[#4742]: https://github.com/nymtech/nym/pull/4742 +[#4741]: https://github.com/nymtech/nym/pull/4741 +[#4740]: https://github.com/nymtech/nym/pull/4740 +[#4739]: https://github.com/nymtech/nym/pull/4739 +[#4738]: https://github.com/nymtech/nym/pull/4738 +[#4736]: https://github.com/nymtech/nym/pull/4736 +[#4735]: https://github.com/nymtech/nym/pull/4735 +[#4732]: https://github.com/nymtech/nym/pull/4732 +[#4730]: https://github.com/nymtech/nym/pull/4730 +[#4716]: https://github.com/nymtech/nym/pull/4716 +[#4681]: https://github.com/nymtech/nym/pull/4681 +[#4623]: https://github.com/nymtech/nym/pull/4623 + ## [2024.9-topdeck] (2024-07-26) - chore: fix 1.80 lint issues ([#4731]) diff --git a/Cargo.lock b/Cargo.lock index 80c9660bcc..1795ec333f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,18 @@ version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +[[package]] +name = "accessory" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87537f9ae7cfa78d5b8ebd1a1db25959f5e737126be4d8eb44a5452fc4b63cde" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "addr" version = "0.15.6" @@ -20,9 +32,9 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -74,7 +86,7 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.15", + "getrandom", "once_cell", "version_check", ] @@ -177,9 +189,9 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ "windows-sys 0.52.0", ] @@ -196,9 +208,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.83" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "argon2" @@ -254,7 +266,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -265,23 +277,24 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] name = "async-tungstenite" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e9efbe14612da0a19fb983059a0b621e9cf6225d7018ecab4f9988215540dc" +checksum = "3609af4bbf701ddaf1f6bb4e6257dff4ff8932327d0e685d3f653724c258b1ac" dependencies = [ "futures-io", "futures-util", "log", "pin-project-lite", - "rustls-native-certs", + "rustls-native-certs 0.7.0", + "rustls-pki-types", "tokio", - "tokio-rustls 0.24.1", - "tungstenite", + "tokio-rustls 0.25.0", + "tungstenite 0.21.0", ] [[package]] @@ -319,15 +332,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "autocfg" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" -dependencies = [ - "autocfg 1.3.0", -] - [[package]] name = "autocfg" version = "1.3.0" @@ -347,7 +351,7 @@ dependencies = [ "futures-util", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.28", + "hyper 0.14.29", "itoa", "matchit", "memchr", @@ -459,9 +463,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -540,13 +544,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93f2635620bf0b9d4576eb7bb9a38a55df78bd1205d26fa994b25911a69f212f" dependencies = [ "bitcoin_hashes", - "rand 0.8.5", + "rand", "rand_core 0.6.4", "serde", "unicode-normalization", "zeroize", ] +[[package]] +name = "bit-vec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22" + [[package]] name = "bitcoin_hashes" version = "0.11.0" @@ -639,16 +649,29 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "bloomfilter" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0bdbcf2078e0ba8a74e1fe0cf36f54054a04485759b61dfd60b174658e9607" +dependencies = [ + "bit-vec", + "getrandom", + "siphasher", +] + [[package]] name = "bls12_381" version = "0.8.0" -source = "git+https://github.com/jstuczyn/bls12_381?branch=feature/gt-serialization-0.8.0#c4543fde7d02efea6ecfcf22e14476ddb516b483" +source = "git+https://github.com/jstuczyn/bls12_381?branch=temp/experimental-serdect#22cd0a16b674af1629110a2dc8b6cf6c73ea4cd9" dependencies = [ "digest 0.9.0", "ff", "group", "pairing", "rand_core 0.6.4", + "serde", + "serdect 0.3.0-pre.0", "subtle 2.5.0", "zeroize", ] @@ -720,9 +743,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" dependencies = [ "serde", ] @@ -764,9 +787,9 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" [[package]] name = "cc" -version = "1.0.97" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" [[package]] name = "celes" @@ -791,9 +814,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha" @@ -896,9 +919,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.4" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" dependencies = [ "clap_builder", "clap_derive", @@ -906,45 +929,45 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" dependencies = [ "anstream", "anstyle", - "clap_lex 0.7.0", + "clap_lex 0.7.1", "strsim 0.11.1", ] [[package]] name = "clap_complete" -version = "4.5.2" +version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd79504325bf38b10165b02e89b4347300f855f273c4cb30c4a3209e6583275e" +checksum = "d2020fa13af48afc65a9a87335bda648309ab3d154cd03c7ff95b378c7ed39c4" dependencies = [ - "clap 4.5.4", + "clap 4.5.7", ] [[package]] name = "clap_complete_fig" -version = "4.5.0" +version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b3e65f91fabdd23cac3d57d39d5d938b4daabd070c335c006dccb866a61110" +checksum = "fb4bc503cddc1cd320736fb555d6598309ad07c2ddeaa23891a10ffb759ee612" dependencies = [ - "clap 4.5.4", + "clap 4.5.7", "clap_complete", ] [[package]] name = "clap_derive" -version = "4.5.4" +version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -958,18 +981,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" - -[[package]] -name = "cloudabi" -version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" -dependencies = [ - "bitflags 1.3.2", -] +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" [[package]] name = "colorchoice" @@ -1009,6 +1023,18 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "comfy-table" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b34115915337defe99b2aff5c2ce6771e5fbc4079f4b506301f5cf394c8452f7" +dependencies = [ + "crossterm 0.27.0", + "strum 0.26.3", + "strum_macros 0.26.4", + "unicode-width", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1018,6 +1044,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + [[package]] name = "console-api" version = "0.5.0" @@ -1135,19 +1174,19 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32560304ab4c365791fd307282f76637213d8083c1a98490c35159cd67852237" dependencies = [ - "prost 0.12.4", - "prost-types 0.12.4", - "tendermint-proto", + "prost 0.12.6", + "prost-types 0.12.6", + "tendermint-proto 0.34.1", ] [[package]] name = "cosmos-sdk-proto" -version = "0.20.0" -source = "git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features#41ed4631e146268b0300033c8bbb993d79a49f58" +version = "0.22.0-pre" +source = "git+https://github.com/cosmos/cosmos-rust?rev=4b1332e6d8258ac845cef71589c8d362a669675a#4b1332e6d8258ac845cef71589c8d362a669675a" dependencies = [ - "prost 0.12.4", - "prost-types 0.12.4", - "tendermint-proto", + "prost 0.12.6", + "prost-types 0.12.6", + "tendermint-proto 0.37.0", ] [[package]] @@ -1157,7 +1196,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47126f5364df9387b9d8559dcef62e99010e1d4098f39eb3f7ee4b5c254e40ea" dependencies = [ "bip32", - "cosmos-sdk-proto 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cosmos-sdk-proto 0.20.0", "ecdsa", "eyre", "k256", @@ -1166,17 +1205,17 @@ dependencies = [ "serde_json", "signature", "subtle-encoding", - "tendermint", + "tendermint 0.34.1", "thiserror", ] [[package]] name = "cosmrs" -version = "0.15.0" -source = "git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features#41ed4631e146268b0300033c8bbb993d79a49f58" +version = "0.17.0-pre" +source = "git+https://github.com/cosmos/cosmos-rust?rev=4b1332e6d8258ac845cef71589c8d362a669675a#4b1332e6d8258ac845cef71589c8d362a669675a" dependencies = [ "bip32", - "cosmos-sdk-proto 0.20.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmos-sdk-proto 0.22.0-pre", "ecdsa", "eyre", "k256", @@ -1185,7 +1224,7 @@ dependencies = [ "serde_json", "signature", "subtle-encoding", - "tendermint", + "tendermint 0.37.0", "tendermint-rpc", "thiserror", ] @@ -1257,6 +1296,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cosmwasm-storage" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8935079712688b8d8d4c036378b38b49d13692621c6fcba96700fadfd5126a18" +dependencies = [ + "cosmwasm-std", + "serde", +] + [[package]] name = "cpufeatures" version = "0.2.12" @@ -1283,9 +1332,9 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -1316,6 +1365,32 @@ dependencies = [ "walkdir", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap 4.5.7", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + [[package]] name = "criterion-plot" version = "0.5.0" @@ -1328,9 +1403,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] @@ -1365,9 +1440,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" @@ -1378,8 +1453,8 @@ dependencies = [ "bitflags 1.3.2", "crossterm_winapi", "libc", - "mio", - "parking_lot 0.12.2", + "mio 0.8.11", + "parking_lot 0.12.3", "signal-hook", "signal-hook-mio", "winapi", @@ -1394,13 +1469,26 @@ dependencies = [ "bitflags 1.3.2", "crossterm_winapi", "libc", - "mio", - "parking_lot 0.12.2", + "mio 0.8.11", + "parking_lot 0.12.3", "signal-hook", "signal-hook-mio", "winapi", ] +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.5.0", + "crossterm_winapi", + "libc", + "parking_lot 0.12.3", + "winapi", +] + [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -1558,7 +1646,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1683,12 +1771,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" dependencies = [ - "darling_core 0.20.8", - "darling_macro 0.20.8", + "darling_core 0.20.9", + "darling_macro 0.20.9", ] [[package]] @@ -1707,16 +1795,16 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim 0.10.0", - "syn 2.0.63", + "strsim 0.11.1", + "syn 2.0.66", ] [[package]] @@ -1732,13 +1820,13 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ - "darling_core 0.20.8", + "darling_core 0.20.9", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1780,6 +1868,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "delegate-display" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98a85201f233142ac819bbf6226e36d0b5e129a47bd325084674261c82d4cd66" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "der" version = "0.7.9" @@ -1841,7 +1941,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1915,6 +2015,29 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "dkg-bypass-contract" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-storage-plus", + "nym-coconut-dkg-common", + "nym-contracts-common", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -1943,7 +2066,7 @@ dependencies = [ "digest 0.10.7", "elliptic-curve", "rfc6979", - "serdect", + "serdect 0.2.0", "signature", "spki", ] @@ -2004,9 +2127,9 @@ dependencies = [ [[package]] name = "either" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" [[package]] name = "elliptic-curve" @@ -2023,11 +2146,17 @@ dependencies = [ "pkcs8", "rand_core 0.6.4", "sec1", - "serdect", + "serdect 0.2.0", "subtle 2.5.0", "zeroize", ] +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.34" @@ -2093,14 +2222,14 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "explorer-api" -version = "1.1.38" +version = "1.1.39" dependencies = [ "chrono", - "clap 4.5.4", + "clap 4.5.7", "dotenvy", "humantime-serde", "isocountry", - "itertools 0.12.1", + "itertools 0.13.0", "log", "maxminddb", "nym-bin-common", @@ -2112,8 +2241,8 @@ dependencies = [ "nym-validator-client", "okapi", "pretty_env_logger", - "rand 0.8.5", - "rand_pcg 0.3.1", + "rand", + "rand_pcg", "rand_seeder", "reqwest 0.12.4", "rocket", @@ -2181,6 +2310,18 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fancy_constructor" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f71f317e4af73b2f8f608fac190c52eac4b1879d2145df1db2fe48881ca69435" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -2214,9 +2355,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "figment" -version = "0.10.18" +version = "0.10.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d032832d74006f99547004d49410a4b4218e4c33382d56ca3ff89df74f86b953" +checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" dependencies = [ "atomic 0.6.0", "pear", @@ -2238,12 +2379,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "finl_unicode" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" - [[package]] name = "flate2" version = "1.0.30" @@ -2306,12 +2441,6 @@ dependencies = [ "libc", ] -[[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" - [[package]] name = "funty" version = "2.0.0" @@ -2400,7 +2529,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -2473,19 +2602,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", - "wasm-bindgen", -] - [[package]] name = "getrandom" version = "0.2.15" @@ -2495,7 +2611,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] @@ -2523,9 +2639,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "glob" @@ -2837,12 +2953,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", - "futures-core", + "futures-util", "http 1.1.0", "http-body 1.0.0", "pin-project-lite", @@ -2856,9 +2972,9 @@ checksum = "08a397c49fec283e3d6211adbe480be95aae5f304cfb923e9970e08956d5168a" [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "d0e7a4dd27b9476dc40cb050d3632d3bba3a70ddbff012285f7f8559a1e7e545" [[package]] name = "httpcodec" @@ -2903,9 +3019,9 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.28" +version = "0.14.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" dependencies = [ "bytes", "futures-channel", @@ -2953,7 +3069,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http 0.2.12", - "hyper 0.14.28", + "hyper 0.14.29", "rustls 0.21.12", "tokio", "tokio-rustls 0.24.1", @@ -2982,7 +3098,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper 0.14.28", + "hyper 0.14.29", "pin-project-lite", "tokio", "tokio-io-timeout", @@ -2990,9 +3106,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" dependencies = [ "bytes", "futures-channel", @@ -3031,6 +3147,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -3049,12 +3283,14 @@ dependencies = [ [[package]] name = "idna" -version = "0.5.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "icu_normalizer", + "icu_properties", + "smallvec", + "utf8_iter", ] [[package]] @@ -3065,13 +3301,15 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexed_db_futures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfbcff6ae46750b15cc594bfd277b188cbddcfdc1817848f97f03f26f8625b9e" +version = "0.4.2" +source = "git+https://github.com/TiemenSch/rust-indexed-db?branch=update-uuid#9745d015707008b0c410115d787014a6d1af2efb" dependencies = [ + "accessory", "cfg-if", + "delegate-display", + "fancy_constructor", "js-sys", - "uuid 1.6.1", + "uuid", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -3083,7 +3321,7 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ - "autocfg 1.3.0", + "autocfg", "hashbrown 0.12.3", "serde", ] @@ -3099,6 +3337,19 @@ dependencies = [ "serde", ] +[[package]] +name = "indicatif" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +dependencies = [ + "console", + "instant", + "number_prefix", + "portable-atomic", + "unicode-width", +] + [[package]] name = "inlinable_string" version = "0.1.15" @@ -3153,9 +3404,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", ] @@ -3281,6 +3532,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -3403,9 +3663,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.154" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libm" @@ -3436,9 +3696,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.16" +version = "1.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" +checksum = "c15da26e5af7e25c90b37a2d75cdbf940cf4a55316de9d84c679c9b8bfabf82e" dependencies = [ "cc", "libc", @@ -3448,9 +3708,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lioness" @@ -3464,13 +3724,19 @@ dependencies = [ "keystream", ] +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + [[package]] name = "lock_api" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ - "autocfg 1.3.0", + "autocfg", "scopeguard", ] @@ -3511,6 +3777,53 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58093314a45e00c77d5c508f76e77c3396afbbc0d01506e7fae47b018bac2b1d" +[[package]] +name = "macroific" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05c00ac596022625d01047c421a0d97d7f09a18e429187b341c201cb631b9dd" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "macroific_macro", +] + +[[package]] +name = "macroific_attr_parse" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd94d5da95b30ae6e10621ad02340909346ad91661f3f8c0f2b62345e46a2f67" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "macroific_core" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13198c120864097a565ccb3ff947672d969932b7975ebd4085732c9f09435e55" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "macroific_macro" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c9853143cbed7f1e41dc39fee95f9b361bec65c8dc2a01bf609be01b61f5ae" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "matchers" version = "0.1.0" @@ -3550,7 +3863,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ - "autocfg 1.3.0", + "autocfg", ] [[package]] @@ -3577,9 +3890,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ "adler", ] @@ -3592,10 +3905,22 @@ checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "wasi", + "windows-sys 0.52.0", +] + [[package]] name = "mix-fetch-wasm" version = "1.3.0-rc.0" @@ -3608,7 +3933,7 @@ dependencies = [ "nym-ordered-buffer", "nym-service-providers-common", "nym-socks5-requests", - "rand 0.8.5", + "rand", "serde", "serde-wasm-bindgen 0.6.5", "thiserror", @@ -3738,9 +4063,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.5.0", "cfg-if", @@ -3777,7 +4102,7 @@ dependencies = [ "inotify", "kqueue", "libc", - "mio", + "mio 0.8.11", "walkdir", "windows-sys 0.45.0", ] @@ -3824,7 +4149,7 @@ version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ - "autocfg 1.3.0", + "autocfg", "libm", ] @@ -3847,16 +4172,24 @@ dependencies = [ "libc", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "nym-api" -version = "1.1.42" +version = "1.1.43" dependencies = [ "anyhow", "async-trait", + "bincode", "bip39", + "bloomfilter", "bs58 0.5.1", "cfg-if", - "clap 4.5.4", + "clap 4.5.7", "console-subscriber", "cosmwasm-std", "cw-utils", @@ -3867,21 +4200,25 @@ dependencies = [ "futures", "getset", "humantime-serde", - "itertools 0.12.1", + "itertools 0.13.0", "k256", "log", "nym-api-requests", "nym-bandwidth-controller", "nym-bin-common", "nym-coconut", - "nym-coconut-bandwidth-contract-common", "nym-coconut-dkg-common", + "nym-compact-ecash", "nym-config", "nym-contracts-common", "nym-credential-storage", "nym-credentials", + "nym-credentials-interface", "nym-crypto", "nym-dkg", + "nym-ecash-contract-common", + "nym-ecash-double-spending", + "nym-ecash-time", "nym-gateway-client", "nym-inclusion-probability", "nym-mixnet-contract-common", @@ -3896,8 +4233,8 @@ dependencies = [ "nym-vesting-contract-common", "okapi", "pin-project", - "rand 0.8.5", - "rand_chacha 0.3.1", + "rand", + "rand_chacha", "reqwest 0.12.4", "rocket", "rocket_cors", @@ -3923,19 +4260,24 @@ name = "nym-api-requests" version = "0.1.0" dependencies = [ "bs58 0.5.1", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "cosmwasm-std", "ecdsa", "getset", + "nym-compact-ecash", "nym-credentials-interface", "nym-crypto", + "nym-ecash-time", "nym-mixnet-contract-common", "nym-node-requests", + "nym-serde-helpers", "rocket", "schemars", "serde", "serde_json", - "tendermint", + "sha2 0.10.8", + "tendermint 0.37.0", + "thiserror", "time", "ts-rs", ] @@ -3958,7 +4300,8 @@ dependencies = [ "bincode", "bs58 0.5.1", "bytes", - "clap 4.5.4", + "clap 4.5.7", + "defguard_wireguard_rs", "fastrand 2.1.0", "futures", "ipnetwork 0.16.0", @@ -3977,7 +4320,7 @@ dependencies = [ "nym-types", "nym-wireguard", "nym-wireguard-types", - "rand 0.8.5", + "rand", "serde", "serde_json", "thiserror", @@ -3994,7 +4337,7 @@ dependencies = [ "bincode", "nym-sphinx", "nym-wireguard-types", - "rand 0.8.5", + "rand", "serde", ] @@ -4004,14 +4347,15 @@ version = "0.1.0" dependencies = [ "bip39", "log", - "nym-coconut", "nym-credential-storage", "nym-credentials", "nym-credentials-interface", "nym-crypto", + "nym-ecash-contract-common", + "nym-ecash-time", "nym-network-defaults", "nym-validator-client", - "rand 0.8.5", + "rand", "thiserror", "url", "zeroize", @@ -4021,7 +4365,7 @@ dependencies = [ name = "nym-bin-common" version = "0.6.0" dependencies = [ - "clap 4.5.4", + "clap 4.5.7", "clap_complete", "clap_complete_fig", "const-str", @@ -4045,7 +4389,7 @@ name = "nym-bity-integration" version = "0.1.0" dependencies = [ "anyhow", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "eyre", "k256", "nym-cli-commands", @@ -4057,13 +4401,13 @@ dependencies = [ [[package]] name = "nym-cli" -version = "1.1.40" +version = "1.1.41" dependencies = [ "anyhow", "base64 0.13.1", "bip39", "bs58 0.5.1", - "clap 4.5.4", + "clap 4.5.7", "clap_complete", "clap_complete_fig", "dotenvy", @@ -4085,13 +4429,14 @@ name = "nym-cli-commands" version = "1.0.0" dependencies = [ "anyhow", - "base64 0.13.1", + "base64 0.21.7", "bip39", "bs58 0.5.1", "cfg-if", - "clap 4.5.4", - "comfy-table", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "clap 4.5.7", + "colored", + "comfy-table 6.2.0", + "cosmrs 0.17.0-pre", "cosmwasm-std", "csv", "cw-utils", @@ -4104,7 +4449,6 @@ dependencies = [ "nym-bandwidth-controller", "nym-bin-common", "nym-client-core", - "nym-coconut-bandwidth-contract-common", "nym-coconut-dkg-common", "nym-config", "nym-contracts-common", @@ -4113,6 +4457,7 @@ dependencies = [ "nym-credentials", "nym-credentials-interface", "nym-crypto", + "nym-ecash-contract-common", "nym-id", "nym-mixnet-contract-common", "nym-multisig-contract-common", @@ -4122,24 +4467,24 @@ dependencies = [ "nym-types", "nym-validator-client", "nym-vesting-contract-common", - "rand 0.6.5", + "rand", "serde", "serde_json", "tap", "thiserror", "time", "tokio", - "toml 0.5.11", + "toml 0.8.14", "url", "zeroize", ] [[package]] name = "nym-client" -version = "1.1.39" +version = "1.1.40" dependencies = [ "bs58 0.5.1", - "clap 4.5.4", + "clap 4.5.7", "dirs 4.0.0", "futures", "log", @@ -4159,7 +4504,7 @@ dependencies = [ "nym-task", "nym-topology", "nym-validator-client", - "rand 0.8.5", + "rand", "serde", "serde_json", "tap", @@ -4179,7 +4524,8 @@ dependencies = [ "base64 0.21.7", "bs58 0.5.1", "cfg-if", - "clap 4.5.4", + "clap 4.5.7", + "comfy-table 7.1.1", "futures", "gloo-timers", "http-body-util", @@ -4194,7 +4540,9 @@ dependencies = [ "nym-config", "nym-country-group", "nym-credential-storage", + "nym-credentials-interface", "nym-crypto", + "nym-ecash-time", "nym-explorer-client", "nym-gateway-client", "nym-gateway-requests", @@ -4207,7 +4555,7 @@ dependencies = [ "nym-task", "nym-topology", "nym-validator-client", - "rand 0.8.5", + "rand", "serde", "serde_json", "sha2 0.10.8", @@ -4219,7 +4567,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-tungstenite", - "tungstenite", + "tungstenite 0.20.1", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -4248,7 +4596,7 @@ name = "nym-client-core-gateways-storage" version = "0.1.0" dependencies = [ "async-trait", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "log", "nym-crypto", "nym-gateway-requests", @@ -4287,7 +4635,7 @@ dependencies = [ "nym-bin-common", "nym-node-tester-utils", "nym-node-tester-wasm", - "rand 0.8.5", + "rand", "serde", "serde-wasm-bindgen 0.6.5", "serde_json", @@ -4315,17 +4663,17 @@ version = "0.5.0" dependencies = [ "bls12_381", "bs58 0.5.1", - "criterion", + "criterion 0.4.0", "digest 0.9.0", "doc-comment", "ff", - "getrandom 0.2.15", + "getrandom", "group", - "itertools 0.12.1", + "itertools 0.13.0", "nym-dkg", "nym-pemstore", - "rand 0.8.5", - "rand_chacha 0.3.1", + "rand", + "rand_chacha", "serde", "serde_derive", "sha2 0.9.9", @@ -4356,6 +4704,29 @@ dependencies = [ "nym-multisig-contract-common", ] +[[package]] +name = "nym-compact-ecash" +version = "0.1.0" +dependencies = [ + "bincode", + "bls12_381", + "bs58 0.5.1", + "cfg-if", + "criterion 0.5.1", + "digest 0.9.0", + "ff", + "group", + "itertools 0.12.1", + "nym-network-defaults", + "nym-pemstore", + "rand", + "rayon", + "serde", + "sha2 0.9.9", + "thiserror", + "zeroize", +] + [[package]] name = "nym-config" version = "0.1.0" @@ -4397,7 +4768,12 @@ name = "nym-credential-storage" version = "0.1.0" dependencies = [ "async-trait", + "bincode", "log", + "nym-compact-ecash", + "nym-credentials", + "nym-ecash-time", + "serde", "sqlx", "thiserror", "tokio", @@ -4411,12 +4787,14 @@ dependencies = [ "log", "nym-bandwidth-controller", "nym-client-core", - "nym-coconut", "nym-config", "nym-credential-storage", "nym-credentials", + "nym-credentials-interface", + "nym-ecash-time", "nym-validator-client", "thiserror", + "time", "tokio", ] @@ -4426,13 +4804,16 @@ version = "0.1.0" dependencies = [ "bincode", "bls12_381", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "log", "nym-api-requests", "nym-credentials-interface", "nym-crypto", + "nym-ecash-contract-common", + "nym-ecash-time", + "nym-network-defaults", "nym-validator-client", - "rand 0.8.5", + "rand", "serde", "thiserror", "time", @@ -4444,9 +4825,14 @@ name = "nym-credentials-interface" version = "0.1.0" dependencies = [ "bls12_381", - "nym-coconut", + "nym-compact-ecash", + "nym-ecash-time", + "nym-network-defaults", + "rand", "serde", + "strum 0.25.0", "thiserror", + "time", ] [[package]] @@ -4465,8 +4851,8 @@ dependencies = [ "hmac", "nym-pemstore", "nym-sphinx-types", - "rand 0.8.5", - "rand_chacha 0.3.1", + "rand", + "rand_chacha", "serde", "serde_bytes", "subtle-encoding", @@ -4482,14 +4868,14 @@ dependencies = [ "bitvec", "bls12_381", "bs58 0.5.1", - "criterion", + "criterion 0.4.0", "ff", "group", "lazy_static", "nym-contracts-common", "nym-pemstore", - "rand 0.8.5", - "rand_chacha 0.3.1", + "rand", + "rand_chacha", "rand_core 0.6.4", "serde", "serde_derive", @@ -4498,6 +4884,37 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-ecash-contract-common" +version = "0.1.0" +dependencies = [ + "bs58 0.5.1", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-utils", + "cw2", + "nym-multisig-contract-common", + "thiserror", +] + +[[package]] +name = "nym-ecash-double-spending" +version = "0.1.0" +dependencies = [ + "bit-vec", + "bloomfilter", + "nym-network-defaults", +] + +[[package]] +name = "nym-ecash-time" +version = "0.1.0" +dependencies = [ + "nym-compact-ecash", + "time", +] + [[package]] name = "nym-execute" version = "0.1.0" @@ -4550,9 +4967,12 @@ dependencies = [ "anyhow", "async-trait", "bip39", + "bloomfilter", "bs58 0.5.1", - "clap 4.5.4", + "clap 4.5.7", "colored", + "cosmwasm-std", + "cw-utils", "dashmap", "defguard_wireguard_rs", "dirs 4.0.0", @@ -4560,7 +4980,6 @@ dependencies = [ "futures", "humantime-serde", "ipnetwork 0.16.0", - "log", "nym-api-requests", "nym-authenticator", "nym-bin-common", @@ -4568,7 +4987,10 @@ dependencies = [ "nym-credentials", "nym-credentials-interface", "nym-crypto", + "nym-ecash-contract-common", + "nym-ecash-double-spending", "nym-gateway-requests", + "nym-gateway-storage", "nym-ip-packet-router", "nym-mixnet-client", "nym-mixnode-common", @@ -4583,9 +5005,10 @@ dependencies = [ "nym-wireguard", "nym-wireguard-types", "once_cell", - "rand 0.8.5", + "rand", "serde", "serde_json", + "si-scale", "sqlx", "subtle-encoding", "thiserror", @@ -4594,6 +5017,7 @@ dependencies = [ "tokio-stream", "tokio-tungstenite", "tokio-util", + "tracing", "url", "zeroize", ] @@ -4603,7 +5027,7 @@ name = "nym-gateway-client" version = "0.1.0" dependencies = [ "futures", - "getrandom 0.2.15", + "getrandom", "gloo-utils 0.2.0", "log", "nym-bandwidth-controller", @@ -4616,7 +5040,7 @@ dependencies = [ "nym-sphinx", "nym-task", "nym-validator-client", - "rand 0.8.5", + "rand", "serde", "si-scale", "thiserror", @@ -4624,7 +5048,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-tungstenite", - "tungstenite", + "tungstenite 0.20.1", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -4639,22 +5063,42 @@ dependencies = [ "bs58 0.5.1", "futures", "generic-array 0.14.7", - "log", + "nym-compact-ecash", "nym-credentials", "nym-credentials-interface", "nym-crypto", "nym-pemstore", "nym-sphinx", - "rand 0.8.5", + "nym-task", + "rand", "serde", "serde_json", "thiserror", "tokio", - "tungstenite", + "tracing", + "tungstenite 0.20.1", "wasmtimer", "zeroize", ] +[[package]] +name = "nym-gateway-storage" +version = "0.1.0" +dependencies = [ + "async-trait", + "bincode", + "defguard_wireguard_rs", + "log", + "nym-credentials-interface", + "nym-gateway-requests", + "nym-sphinx", + "sqlx", + "thiserror", + "time", + "tokio", + "tracing", +] + [[package]] name = "nym-group-contract-common" version = "0.1.0" @@ -4713,7 +5157,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bs58 0.5.1", - "clap 4.5.4", + "clap 4.5.7", "nym-bin-common", "nym-credential-storage", "nym-id", @@ -4726,7 +5170,7 @@ name = "nym-inclusion-probability" version = "0.1.0" dependencies = [ "log", - "rand 0.8.5", + "rand", "thiserror", ] @@ -4739,7 +5183,7 @@ dependencies = [ "nym-bin-common", "nym-crypto", "nym-sphinx", - "rand 0.8.5", + "rand", "serde", "thiserror", "time", @@ -4755,7 +5199,7 @@ dependencies = [ "bincode", "bs58 0.5.1", "bytes", - "clap 4.5.4", + "clap 4.5.7", "etherparse", "futures", "log", @@ -4776,7 +5220,7 @@ dependencies = [ "nym-types", "nym-wireguard", "nym-wireguard-types", - "rand 0.8.5", + "rand", "reqwest 0.12.4", "serde", "serde_json", @@ -4829,11 +5273,12 @@ dependencies = [ "bs58 0.4.0", "cosmwasm-schema", "cosmwasm-std", + "cw-controllers", "cw2", "humantime-serde", "log", "nym-contracts-common", - "rand_chacha 0.3.1", + "rand_chacha", "schemars", "serde", "serde-json-wasm", @@ -4850,7 +5295,7 @@ dependencies = [ "anyhow", "axum 0.7.5", "bs58 0.5.1", - "clap 4.5.4", + "clap 4.5.7", "colored", "cupid", "dirs 4.0.0", @@ -4876,7 +5321,7 @@ dependencies = [ "nym-topology", "nym-types", "nym-validator-client", - "rand 0.8.5", + "rand", "serde", "serde_json", "sysinfo 0.27.8", @@ -4909,7 +5354,7 @@ dependencies = [ "nym-sphinx-types", "nym-task", "nym-validator-client", - "rand 0.8.5", + "rand", "serde", "thiserror", "time", @@ -4937,26 +5382,22 @@ dependencies = [ name = "nym-network-defaults" version = "0.1.0" dependencies = [ - "cfg-if", "dotenvy", - "hex-literal", "log", - "once_cell", "schemars", "serde", - "thiserror", "url", ] [[package]] name = "nym-network-requester" -version = "1.1.40" +version = "1.1.41" dependencies = [ "addr", "anyhow", "async-trait", "bs58 0.5.1", - "clap 4.5.4", + "clap 4.5.7", "dirs 4.0.0", "futures", "humantime-serde", @@ -4983,7 +5424,7 @@ dependencies = [ "nym-types", "pretty_env_logger", "publicsuffix", - "rand 0.8.5", + "rand", "regex", "reqwest 0.12.4", "serde", @@ -5001,14 +5442,14 @@ dependencies = [ [[package]] name = "nym-node" -version = "1.1.6" +version = "1.1.7" dependencies = [ "anyhow", "bip39", "bs58 0.5.1", "cargo_metadata", "celes", - "clap 4.5.4", + "clap 4.5.7", "colored", "cupid", "humantime-serde", @@ -5030,7 +5471,7 @@ dependencies = [ "nym-types", "nym-wireguard", "nym-wireguard-types", - "rand 0.8.5", + "rand", "semver 1.0.23", "serde", "serde_json", @@ -5064,7 +5505,7 @@ dependencies = [ "nym-task", "nym-wireguard", "nym-wireguard-types", - "rand 0.8.5", + "rand", "serde_json", "thiserror", "time", @@ -5091,7 +5532,7 @@ dependencies = [ "nym-exit-policy", "nym-http-api-client", "nym-wireguard-types", - "rand_chacha 0.3.1", + "rand_chacha", "schemars", "serde", "serde_json", @@ -5112,7 +5553,7 @@ dependencies = [ "nym-sphinx-params", "nym-task", "nym-topology", - "rand 0.8.5", + "rand", "serde", "serde_json", "thiserror", @@ -5127,7 +5568,7 @@ dependencies = [ "futures", "js-sys", "nym-node-tester-utils", - "rand 0.8.5", + "rand", "serde", "serde-wasm-bindgen 0.6.5", "thiserror", @@ -5155,7 +5596,7 @@ name = "nym-nr-query" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.5.4", + "clap 4.5.7", "log", "nym-bin-common", "nym-network-defaults", @@ -5181,12 +5622,12 @@ dependencies = [ "blake3", "chacha20", "chacha20poly1305", - "criterion", + "criterion 0.4.0", "curve25519-dalek 4.1.2", "fastrand 2.1.0", - "getrandom 0.2.15", + "getrandom", "log", - "rand 0.8.5", + "rand", "rayon", "sphinx-packet", "thiserror", @@ -5221,6 +5662,7 @@ dependencies = [ "nym-credential-storage", "nym-credential-utils", "nym-credentials", + "nym-credentials-interface", "nym-crypto", "nym-gateway-requests", "nym-network-defaults", @@ -5232,9 +5674,9 @@ dependencies = [ "nym-task", "nym-topology", "nym-validator-client", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "pretty_env_logger", - "rand 0.8.5", + "rand", "reqwest 0.12.4", "tap", "thiserror", @@ -5243,6 +5685,16 @@ dependencies = [ "tokio-util", "toml 0.5.11", "url", + "zeroize", +] + +[[package]] +name = "nym-serde-helpers" +version = "0.1.0" +dependencies = [ + "base64 0.21.7", + "bs58 0.5.1", + "serde", ] [[package]] @@ -5264,10 +5716,10 @@ dependencies = [ [[package]] name = "nym-socks5-client" -version = "1.1.39" +version = "1.1.40" dependencies = [ "bs58 0.5.1", - "clap 4.5.4", + "clap 4.5.7", "log", "nym-bin-common", "nym-client-core", @@ -5284,7 +5736,7 @@ dependencies = [ "nym-sphinx", "nym-topology", "nym-validator-client", - "rand 0.8.5", + "rand", "serde", "serde_json", "tap", @@ -5317,7 +5769,7 @@ dependencies = [ "nym-task", "nym-validator-client", "pin-project", - "rand 0.8.5", + "rand", "reqwest 0.12.4", "schemars", "serde", @@ -5343,7 +5795,7 @@ dependencies = [ "nym-credential-storage", "nym-crypto", "nym-socks5-client-core", - "rand 0.8.5", + "rand", "safer-ffi", "serde", "tokio", @@ -5397,7 +5849,7 @@ dependencies = [ "nym-sphinx-routing", "nym-sphinx-types", "nym-topology", - "rand 0.8.5", + "rand", "rand_distr", "thiserror", "tokio", @@ -5415,7 +5867,7 @@ dependencies = [ "nym-sphinx-routing", "nym-sphinx-types", "nym-topology", - "rand 0.8.5", + "rand", "serde", "thiserror", "zeroize", @@ -5427,7 +5879,7 @@ version = "0.1.0" dependencies = [ "nym-crypto", "nym-sphinx-types", - "rand 0.8.5", + "rand", "serde", "thiserror", ] @@ -5443,8 +5895,8 @@ dependencies = [ "nym-sphinx-routing", "nym-sphinx-types", "nym-topology", - "rand 0.8.5", - "rand_chacha 0.3.1", + "rand", + "rand_chacha", "serde", "thiserror", "wasm-bindgen", @@ -5458,7 +5910,7 @@ dependencies = [ "nym-sphinx-addressing", "nym-sphinx-params", "nym-sphinx-types", - "rand 0.8.5", + "rand", "thiserror", ] @@ -5475,7 +5927,7 @@ dependencies = [ "nym-sphinx-routing", "nym-sphinx-types", "nym-topology", - "rand 0.8.5", + "rand", "thiserror", ] @@ -5536,8 +5988,8 @@ dependencies = [ "aes-gcm", "argon2", "generic-array 0.14.7", - "getrandom 0.2.15", - "rand 0.8.5", + "getrandom", + "rand", "serde", "serde_json", "thiserror", @@ -5572,7 +6024,7 @@ dependencies = [ "nym-sphinx-addressing", "nym-sphinx-routing", "nym-sphinx-types", - "rand 0.8.5", + "rand", "semver 0.11.0", "serde", "serde_json", @@ -5599,11 +6051,11 @@ name = "nym-types" version = "1.0.0" dependencies = [ "base64 0.21.7", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "cosmwasm-std", "eyre", "hmac", - "itertools 0.12.1", + "itertools 0.13.0", "log", "nym-config", "nym-crypto", @@ -5632,7 +6084,7 @@ dependencies = [ "bip32", "bip39", "colored", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "cosmwasm-std", "cw-controllers", "cw-utils", @@ -5642,27 +6094,29 @@ dependencies = [ "eyre", "flate2", "futures", - "itertools 0.12.1", + "itertools 0.13.0", "log", "nym-api-requests", - "nym-coconut", "nym-coconut-bandwidth-contract-common", "nym-coconut-dkg-common", + "nym-compact-ecash", "nym-config", "nym-contracts-common", + "nym-ecash-contract-common", "nym-group-contract-common", "nym-http-api-client", "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-network-defaults", "nym-vesting-contract-common", - "prost 0.12.4", + "prost 0.12.6", "reqwest 0.12.4", "serde", "serde_json", "sha2 0.9.9", "tendermint-rpc", "thiserror", + "time", "tokio", "ts-rs", "url", @@ -5676,22 +6130,25 @@ version = "0.1.0" dependencies = [ "anyhow", "bip39", - "clap 4.5.4", + "clap 4.5.7", "cosmwasm-std", "futures", "humantime 2.1.0", "humantime-serde", "nym-bin-common", - "nym-coconut", "nym-coconut-bandwidth-contract-common", "nym-coconut-dkg-common", + "nym-compact-ecash", "nym-config", "nym-credentials", + "nym-credentials-interface", "nym-crypto", + "nym-ecash-time", "nym-network-defaults", "nym-task", "nym-validator-client", "nyxd-scraper", + "rand_chacha", "serde", "serde_with", "sha2 0.10.8", @@ -5722,7 +6179,7 @@ dependencies = [ name = "nym-wallet-types" version = "1.0.0" dependencies = [ - "cosmrs 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cosmrs 0.15.0", "cosmwasm-std", "hex-literal", "nym-config", @@ -5742,12 +6199,14 @@ name = "nym-wireguard" version = "0.1.0" dependencies = [ "base64 0.21.7", + "bincode", "chrono", "dashmap", "defguard_wireguard_rs", "ip_network", "log", "nym-crypto", + "nym-gateway-storage", "nym-network-defaults", "nym-task", "nym-wireguard-types", @@ -5768,7 +6227,7 @@ dependencies = [ "nym-config", "nym-crypto", "nym-network-defaults", - "rand 0.8.5", + "rand", "serde", "serde_json", "sha2 0.10.8", @@ -5779,11 +6238,11 @@ dependencies = [ [[package]] name = "nymvisor" -version = "0.1.5" +version = "0.1.6" dependencies = [ "anyhow", "bytes", - "clap 4.5.4", + "clap 4.5.7", "dotenvy", "flate2", "futures", @@ -5813,14 +6272,14 @@ version = "0.1.0" dependencies = [ "async-trait", "const_format", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "eyre", "futures", "humantime 2.1.0", "serde", "sha2 0.10.8", "sqlx", - "tendermint", + "tendermint 0.37.0", "tendermint-rpc", "thiserror", "time", @@ -5833,9 +6292,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" dependencies = [ "memchr", ] @@ -5978,7 +6437,7 @@ dependencies = [ "once_cell", "opentelemetry_api", "percent-encoding", - "rand 0.8.5", + "rand", "thiserror", "tokio", "tokio-stream", @@ -6039,9 +6498,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core 0.9.10", @@ -6111,14 +6570,14 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] name = "peg" -version = "0.7.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c0b841ea54f523f7aa556956fbd293bcbe06f2e67d2eb732b7278aaf1d166a" +checksum = "8a625d12ad770914cbf7eff6f9314c3ef803bfe364a1b20bc36ddf56673e71e5" dependencies = [ "peg-macros", "peg-runtime", @@ -6126,9 +6585,9 @@ dependencies = [ [[package]] name = "peg-macros" -version = "0.7.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aa52829b8decbef693af90202711348ab001456803ba2a98eb4ec8fb70844c" +checksum = "f241d42067ed3ab6a4fece1db720838e1418f36d868585a27931f95d6bc03582" dependencies = [ "peg-runtime", "proc-macro2", @@ -6137,9 +6596,9 @@ dependencies = [ [[package]] name = "peg-runtime" -version = "0.7.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c719dcf55f09a3a7e764c6649ab594c18a177e3599c467983cdf644bfc0a4088" +checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a" [[package]] name = "pem" @@ -6189,7 +6648,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -6220,7 +6679,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -6259,9 +6718,9 @@ checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7" [[package]] name = "plotters" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" dependencies = [ "num-traits", "plotters-backend", @@ -6272,15 +6731,15 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" +checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" [[package]] name = "plotters-svg" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" dependencies = [ "plotters-backend", ] @@ -6291,7 +6750,7 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ - "autocfg 1.3.0", + "autocfg", "bitflags 1.3.2", "cfg-if", "concurrent-queue", @@ -6324,6 +6783,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + [[package]] name = "powerfmt" version = "0.2.0" @@ -6382,9 +6847,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.82" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] @@ -6397,7 +6862,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", "version_check", "yansi", ] @@ -6412,7 +6877,7 @@ dependencies = [ "fnv", "lazy_static", "memchr", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "protobuf", "thiserror", ] @@ -6429,12 +6894,12 @@ dependencies = [ [[package]] name = "prost" -version = "0.12.4" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f5d036824e4761737860779c906171497f6d55681139d8312388f8fe398922" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ "bytes", - "prost-derive 0.12.5", + "prost-derive 0.12.6", ] [[package]] @@ -6452,15 +6917,15 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9554e3ab233f0a932403704f1a1d08c30d5ccd931adfdfa1e8b5a19b52c1d55a" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -6474,11 +6939,11 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.12.4" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3235c33eb02c1f1e212abdbe34c78b264b038fb58ca612664343271e36e55ffe" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" dependencies = [ - "prost 0.12.4", + "prost 0.12.6", ] [[package]] @@ -6489,9 +6954,9 @@ checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" [[package]] name = "psl" -version = "2.1.39" +version = "2.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b320cda4ad7e8f4269fa415754418f83b38c666a5e2e99ea48825b274a373f3" +checksum = "ecec637c2e9d0c8c4bf78df069a53a103ebe3dbd0dc7eff1d60c1006a1c97254" dependencies = [ "psl-types", ] @@ -6539,38 +7004,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" -[[package]] -name = "rand" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" -dependencies = [ - "autocfg 0.1.8", - "libc", - "rand_chacha 0.1.1", - "rand_core 0.4.2", - "rand_hc 0.1.0", - "rand_isaac", - "rand_jitter", - "rand_os", - "rand_pcg 0.1.2", - "rand_xorshift", - "winapi", -] - -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc 0.2.0", -] - [[package]] name = "rand" version = "0.8.5" @@ -6578,30 +7011,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha 0.3.1", + "rand_chacha", "rand_core 0.6.4", ] -[[package]] -name = "rand_chacha" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" -dependencies = [ - "autocfg 0.1.8", - "rand_core 0.3.1", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - [[package]] name = "rand_chacha" version = "0.3.1" @@ -6612,29 +7025,11 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "rand_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -dependencies = [ - "rand_core 0.4.2", -] - -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - [[package]] name = "rand_core" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] [[package]] name = "rand_core" @@ -6642,7 +7037,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom", ] [[package]] @@ -6652,69 +7047,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" dependencies = [ "num-traits", - "rand 0.8.5", -] - -[[package]] -name = "rand_hc" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" -dependencies = [ - "rand_core 0.3.1", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rand_isaac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" -dependencies = [ - "rand_core 0.3.1", -] - -[[package]] -name = "rand_jitter" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" -dependencies = [ - "libc", - "rand_core 0.4.2", - "winapi", -] - -[[package]] -name = "rand_os" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" -dependencies = [ - "cloudabi", - "fuchsia-cprng", - "libc", - "rand_core 0.4.2", - "rdrand", - "winapi", -] - -[[package]] -name = "rand_pcg" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" -dependencies = [ - "autocfg 0.1.8", - "rand_core 0.4.2", + "rand", ] [[package]] @@ -6735,15 +7068,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "rand_xorshift" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" -dependencies = [ - "rand_core 0.3.1", -] - [[package]] name = "rayon" version = "1.10.0" @@ -6764,15 +7088,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", -] - [[package]] name = "redox_syscall" version = "0.2.16" @@ -6806,7 +7121,7 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ - "getrandom 0.2.15", + "getrandom", "libredox", "thiserror", ] @@ -6828,19 +7143,19 @@ checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] name = "regex" -version = "1.10.4" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.6", - "regex-syntax 0.8.3", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", ] [[package]] @@ -6854,13 +7169,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.3", + "regex-syntax 0.8.4", ] [[package]] @@ -6871,9 +7186,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "reqwest" @@ -6889,7 +7204,7 @@ dependencies = [ "h2", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.28", + "hyper 0.14.29", "hyper-rustls 0.24.2", "ipnet", "js-sys", @@ -6899,7 +7214,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls 0.21.12", - "rustls-native-certs", + "rustls-native-certs 0.6.3", "rustls-pemfile 1.0.4", "serde", "serde_json", @@ -6956,7 +7271,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 0.26.1", + "webpki-roots 0.26.2", "winreg 0.52.0", ] @@ -6993,7 +7308,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom", "libc", "spin 0.9.8", "untrusted 0.9.0", @@ -7028,9 +7343,9 @@ dependencies = [ "memchr", "multer", "num_cpus", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "pin-project-lite", - "rand 0.8.5", + "rand", "ref-cast", "rocket_codegen", "rocket_http", @@ -7059,7 +7374,7 @@ dependencies = [ "proc-macro2", "quote", "rocket_http", - "syn 2.0.63", + "syn 2.0.66", "unicode-xid", "version_check", ] @@ -7091,7 +7406,7 @@ dependencies = [ "either", "futures", "http 0.2.12", - "hyper 0.14.28", + "hyper 0.14.29", "indexmap 2.2.6", "log", "memchr", @@ -7156,7 +7471,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.63", + "syn 2.0.66", "walkdir", ] @@ -7240,7 +7555,7 @@ dependencies = [ "log", "ring 0.17.8", "rustls-pki-types", - "rustls-webpki 0.102.3", + "rustls-webpki 0.102.4", "subtle 2.5.0", "zeroize", ] @@ -7257,6 +7572,19 @@ dependencies = [ "security-framework", ] +[[package]] +name = "rustls-native-certs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.1.2", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -7294,9 +7622,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.3" +version = "0.102.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" +checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" dependencies = [ "ring 0.17.8", "rustls-pki-types", @@ -7305,9 +7633,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092474d1a01ea8278f69e6a358998405fae5b8b963ddaeb2b0b04a128bf1dfb0" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" @@ -7317,9 +7645,9 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "safer-ffi" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01e0f9180bdf2f29dd5c731789ba0e68f65233c822ece645d288ec7d112e6333" +checksum = "44abae8773dc41fb96af52696b834b1f4c806006b456b22ee3602f7b061e3ad0" dependencies = [ "inventory", "libc", @@ -7334,9 +7662,9 @@ dependencies = [ [[package]] name = "safer_ffi-proc_macros" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "209b37c0f75bc1a994ff8c8290b77005ea09951b59f6855f8c42667a57ed3285" +checksum = "6c9d4117a8a72f9b615169d4d720d79e74931f74003c73cc2f3927c700156ddf" dependencies = [ "macro_rules_attribute", "prettyplease", @@ -7365,9 +7693,9 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.19" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6e7ed6919cb46507fb01ff1654309219f62b4d603822501b0b80d42f6f21ef" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" dependencies = [ "dyn-clone", "indexmap 1.9.3", @@ -7378,14 +7706,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.19" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185f2b7aa7e02d418e453790dde16890256bbd2bcd04b7dc5348811052b53f49" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" dependencies = [ "proc-macro2", "quote", - "serde_derive_internals 0.29.0", - "syn 2.0.63", + "serde_derive_internals 0.29.1", + "syn 2.0.66", ] [[package]] @@ -7420,7 +7748,7 @@ dependencies = [ "der", "generic-array 0.14.7", "pkcs8", - "serdect", + "serdect 0.2.0", "subtle 2.5.0", "zeroize", ] @@ -7492,9 +7820,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.201" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] @@ -7541,13 +7869,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.201" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -7558,18 +7886,18 @@ checksum = "e578a843d40b4189a4d66bba51d7684f57da5bd7c304c64e14bd63efbef49509" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] name = "serde_derive_internals" -version = "0.29.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -7601,7 +7929,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -7649,10 +7977,10 @@ version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" dependencies = [ - "darling 0.20.8", + "darling 0.20.9", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -7678,6 +8006,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serdect" +version = "0.3.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791ef964bfaba6be28a5c3f0c56836e17cb711ac009ca1074b9c735a3ebf240a" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -7745,7 +8083,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" dependencies = [ "libc", - "mio", + "mio 0.8.11", "signal-hook", ] @@ -7768,13 +8106,19 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ - "autocfg 1.3.0", + "autocfg", ] [[package]] @@ -7845,7 +8189,7 @@ dependencies = [ "hmac", "lioness", "log", - "rand 0.8.5", + "rand", "rand_distr", "sha2 0.10.8", "subtle 2.5.0", @@ -7878,11 +8222,10 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" +checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" dependencies = [ - "itertools 0.12.1", "nom", "unicode_categories", ] @@ -7980,7 +8323,7 @@ name = "ssl-inject" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.5.4", + "clap 4.5.7", "hex", "tokio", ] @@ -7994,6 +8337,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "state" version = "0.6.0" @@ -8005,13 +8354,13 @@ dependencies = [ [[package]] name = "stringprep" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" dependencies = [ - "finl_unicode", "unicode-bidi", "unicode-normalization", + "unicode-properties", ] [[package]] @@ -8050,6 +8399,12 @@ dependencies = [ "strum_macros 0.25.3", ] +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + [[package]] name = "strum_macros" version = "0.23.1" @@ -8086,7 +8441,20 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.63", + "syn 2.0.66", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.66", ] [[package]] @@ -8129,9 +8497,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.63" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", @@ -8150,6 +8518,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "sysinfo" version = "0.27.8" @@ -8209,9 +8588,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909" dependencies = [ "filetime", "libc", @@ -8245,8 +8624,8 @@ dependencies = [ "k256", "num-traits", "once_cell", - "prost 0.12.4", - "prost-types 0.12.4", + "prost 0.12.6", + "prost-types 0.12.6", "ripemd", "serde", "serde_bytes", @@ -8256,22 +8635,53 @@ dependencies = [ "signature", "subtle 2.5.0", "subtle-encoding", - "tendermint-proto", + "tendermint-proto 0.34.1", + "time", + "zeroize", +] + +[[package]] +name = "tendermint" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "954496fbc9716eb4446cdd6d00c071a3e2f22578d62aa03b40c7e5b4fda3ed42" +dependencies = [ + "bytes", + "digest 0.10.7", + "ed25519", + "ed25519-consensus", + "flex-error", + "futures", + "k256", + "num-traits", + "once_cell", + "prost 0.12.6", + "prost-types 0.12.6", + "ripemd", + "serde", + "serde_bytes", + "serde_json", + "serde_repr", + "sha2 0.10.8", + "signature", + "subtle 2.5.0", + "subtle-encoding", + "tendermint-proto 0.37.0", "time", "zeroize", ] [[package]] name = "tendermint-config" -version = "0.34.1" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1a02da769166e2052cd537b1a97c78017632c2d9e19266367b27e73910434fc" +checksum = "f84b11b57d20ee4492a1452faff85f5c520adc36ca9fe5e701066935255bb89f" dependencies = [ "flex-error", "serde", "serde_json", - "tendermint", - "toml 0.5.11", + "tendermint 0.37.0", + "toml 0.8.14", "url", ] @@ -8285,8 +8695,24 @@ dependencies = [ "flex-error", "num-derive", "num-traits", - "prost 0.12.4", - "prost-types 0.12.4", + "prost 0.12.6", + "prost-types 0.12.6", + "serde", + "serde_bytes", + "subtle-encoding", + "time", +] + +[[package]] +name = "tendermint-proto" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc87024548c7f3da479885201e3da20ef29e85a3b13d04606b380ac4c7120d87" +dependencies = [ + "bytes", + "flex-error", + "prost 0.12.6", + "prost-types 0.12.6", "serde", "serde_bytes", "subtle-encoding", @@ -8295,18 +8721,19 @@ dependencies = [ [[package]] name = "tendermint-rpc" -version = "0.34.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbf0a4753b46a190f367337e0163d0b552a2674a6bac54e74f9f2cdcde2969b" +checksum = "dfdc2281e271277fda184d96d874a6fe59f569b130b634289257baacfc95aa85" dependencies = [ "async-trait", "async-tungstenite", "bytes", "flex-error", "futures", - "getrandom 0.2.15", + "getrandom", "peg", "pin-project", + "rand", "reqwest 0.11.27", "semver 1.0.23", "serde", @@ -8314,15 +8741,15 @@ dependencies = [ "serde_json", "subtle 2.5.0", "subtle-encoding", - "tendermint", + "tendermint 0.37.0", "tendermint-config", - "tendermint-proto", + "tendermint-proto 0.37.0", "thiserror", "time", "tokio", "tracing", "url", - "uuid 0.8.2", + "uuid", "walkdir", ] @@ -8335,6 +8762,45 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "testnet-manager" +version = "0.1.0" +dependencies = [ + "anyhow", + "bip39", + "bs58 0.5.1", + "clap 4.5.7", + "console", + "cw-utils", + "dkg-bypass-contract", + "indicatif", + "nym-bin-common", + "nym-coconut-dkg-common", + "nym-compact-ecash", + "nym-config", + "nym-contracts-common", + "nym-crypto", + "nym-ecash-contract-common", + "nym-group-contract-common", + "nym-mixnet-contract-common", + "nym-multisig-contract-common", + "nym-pemstore", + "nym-validator-client", + "nym-vesting-contract-common", + "rand", + "serde", + "serde_json", + "sqlx", + "tempfile", + "thiserror", + "time", + "tokio", + "toml 0.8.14", + "tracing", + "url", + "zeroize", +] + [[package]] name = "textwrap" version = "0.16.1" @@ -8343,22 +8809,22 @@ checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" [[package]] name = "thiserror" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -8427,6 +8893,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -8454,22 +8930,21 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.37.0" +version = "1.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" dependencies = [ "backtrace", "bytes", "libc", - "mio", - "num_cpus", - "parking_lot 0.12.2", + "mio 1.0.1", + "parking_lot 0.12.3", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "tracing", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -8484,13 +8959,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -8564,12 +9039,12 @@ dependencies = [ [[package]] name = "tokio-tun" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65d79912ba514490b1f5a574b585e19082bd2a6b238970c87c57a66bd77206b5" +checksum = "68f5381752d5832fc811f89d54fc334951aa435022f494190ba7151661f206df" dependencies = [ "libc", - "nix 0.28.0", + "nix 0.29.0", "thiserror", "tokio", ] @@ -8585,7 +9060,7 @@ dependencies = [ "rustls 0.21.12", "tokio", "tokio-rustls 0.24.1", - "tungstenite", + "tungstenite 0.20.1", "webpki-roots 0.25.4", ] @@ -8670,7 +9145,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.8", + "winnow 0.6.13", ] [[package]] @@ -8688,7 +9163,7 @@ dependencies = [ "h2", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.28", + "hyper 0.14.29", "hyper-timeout", "percent-encoding", "pin-project", @@ -8712,7 +9187,7 @@ dependencies = [ "indexmap 1.9.3", "pin-project", "pin-project-lite", - "rand 0.8.5", + "rand", "slab", "tokio", "tokio-util", @@ -8778,7 +9253,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -8936,7 +9411,7 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", "termcolor", ] @@ -8963,7 +9438,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals 0.28.0", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -8978,7 +9453,7 @@ dependencies = [ "http 0.2.12", "httparse", "log", - "rand 0.8.5", + "rand", "rustls 0.21.12", "sha1", "thiserror", @@ -8987,6 +9462,27 @@ dependencies = [ "webpki-roots 0.24.0", ] +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand", + "rustls 0.22.4", + "rustls-pki-types", + "sha1", + "thiserror", + "url", + "utf-8", +] + [[package]] name = "typenum" version = "1.17.0" @@ -9058,6 +9554,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" + [[package]] name = "unicode-segmentation" version = "1.11.0" @@ -9066,9 +9568,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "unicode-xid" @@ -9127,12 +9629,12 @@ checksum = "0976c77def3f1f75c4ef892a292c31c0bbe9e3d0702c63044d7c76db298171a3" [[package]] name = "url" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" dependencies = [ "form_urlencoded", - "idna 0.5.0", + "idna 1.0.0", "percent-encoding", "serde", ] @@ -9150,10 +9652,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] -name = "utf8parse" -version = "0.2.1" +name = "utf16_iter" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utoipa" @@ -9177,7 +9691,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -9198,17 +9712,11 @@ dependencies = [ [[package]] name = "uuid" -version = "0.8.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" - -[[package]] -name = "uuid" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ - "getrandom 0.2.15", + "getrandom", "serde", "wasm-bindgen", ] @@ -9271,12 +9779,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -9304,7 +9806,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", "wasm-bindgen-shared", ] @@ -9338,7 +9840,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -9371,7 +9873,7 @@ checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -9392,7 +9894,7 @@ dependencies = [ "nym-task", "nym-topology", "nym-validator-client", - "rand 0.8.5", + "rand", "serde", "serde-wasm-bindgen 0.6.5", "thiserror", @@ -9441,11 +9943,11 @@ version = "0.1.0" dependencies = [ "console_error_panic_hook", "futures", - "getrandom 0.2.15", + "getrandom", "gloo-net", "gloo-utils 0.2.0", "js-sys", - "tungstenite", + "tungstenite 0.20.1", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -9459,7 +9961,7 @@ checksum = "5f656cd8858a5164932d8a90f936700860976ec21eb00e0fe2aa8cab13f6b4cf" dependencies = [ "futures", "js-sys", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "pin-utils", "slab", "wasm-bindgen", @@ -9511,9 +10013,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" +checksum = "3c452ad30530b54a4d8e71952716a212b08efd0f3562baa66c29a618b07da7c3" dependencies = [ "rustls-pki-types", ] @@ -9793,9 +10295,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.8" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" dependencies = [ "memchr", ] @@ -9840,6 +10342,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "wyz" version = "0.5.1" @@ -9881,6 +10395,30 @@ dependencies = [ "is-terminal", ] +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.34" @@ -9898,7 +10436,28 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", + "synstructure", ] [[package]] @@ -9918,7 +10477,29 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", +] + +[[package]] +name = "zerovec" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", ] [[package]] @@ -9940,20 +10521,21 @@ dependencies = [ "anyhow", "async-trait", "bs58 0.5.1", - "getrandom 0.2.15", + "getrandom", "js-sys", "nym-bin-common", "nym-coconut", + "nym-compact-ecash", "nym-credentials", "nym-crypto", "nym-http-api-client", - "rand 0.7.3", + "rand", "reqwest 0.12.4", "serde", "thiserror", "tokio", "tsify", - "uuid 1.6.1", + "uuid", "wasm-bindgen", "wasm-utils", "wasmtimer", diff --git a/Cargo.toml b/Cargo.toml index 18608553af..237f005d32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ members = [ "common/commands", "common/config", "common/cosmwasm-smart-contracts/coconut-bandwidth-contract", + "common/cosmwasm-smart-contracts/ecash-contract", "common/cosmwasm-smart-contracts/coconut-dkg", "common/cosmwasm-smart-contracts/contracts-common", "common/cosmwasm-smart-contracts/group-contract", @@ -47,8 +48,12 @@ members = [ "common/credentials-interface", "common/crypto", "common/dkg", + "common/ecash-double-spending", + "common/ecash-time", "common/execute", "common/exit-policy", + "common/gateway-requests", + "common/gateway-storage", "common/http-api-client", "common/http-api-common", "common/inclusion-probability", @@ -59,6 +64,7 @@ members = [ "common/node-tester-utils", "common/nonexhaustive-delayqueue", "common/nymcoconut", + "common/nym_offline_compact_ecash", "common/nym-id", "common/nym-metrics", "common/nymsphinx", @@ -74,6 +80,7 @@ members = [ "common/nymsphinx/types", "common/nyxd-scraper", "common/pemstore", + "common/serde-helpers", "common/socks5-client-core", "common/socks5/proxy-helpers", "common/socks5/requests", @@ -91,7 +98,6 @@ members = [ "explorer-api/explorer-api-requests", "explorer-api/explorer-client", "gateway", - "gateway/gateway-requests", "integrations/bity", "mixnode", "sdk/lib/socks5-listener", @@ -120,6 +126,8 @@ members = [ "wasm/mix-fetch", "wasm/node-tester", "wasm/zknym-lib", + "tools/internal/testnet-manager", + "tools/internal/testnet-manager/dkg-bypass-contract", ] default-members = [ @@ -132,13 +140,14 @@ default-members = [ "tools/nymvisor", "explorer-api", "nym-validator-rewarder", - "nym-node" + "nym-node", ] exclude = [ "explorer", "contracts", "nym-wallet", + "nym-vpn/ui/src-tauri", "cpu-cycles", "sdk/ffi/cpp", ] @@ -163,8 +172,13 @@ axum-extra = "0.9.3" base64 = "0.21.4" bincode = "1.3.3" bip39 = { version = "2.0.0", features = ["zeroize"] } + +# can we unify those? +bit-vec = "0.7.0" bitvec = "1.0.0" + blake3 = "1.3.1" +bloomfilter = "1.0.14" bs58 = "0.5.1" bytecodec = "0.4.15" bytes = "1.5.0" @@ -216,11 +230,11 @@ httpcodec = "0.2.3" humantime = "2.1.0" humantime-serde = "1.1.1" hyper = "1.3.1" -indexed_db_futures = "0.3.0" inquire = "0.6.2" ip_network = "0.4.1" ipnetwork = "0.16" isocountry = "0.3.2" +itertools = "0.13.0" k256 = "0.13" lazy_static = "1.4.0" ledger-transport = "0.10.0" @@ -277,11 +291,11 @@ tar = "0.4.40" tempfile = "3.5.0" thiserror = "1.0.48" time = "0.3.30" -tokio = "1.33.0" -tokio-stream = "0.1.14" -tokio-test = "0.4.2" +tokio = "1.39" +tokio-stream = "0.1.15" +tokio-test = "0.4.4" tokio-tungstenite = { version = "0.20.1" } -tokio-util = "0.7.10" +tokio-util = "0.7.11" toml = "0.8.14" tower = "0.4.13" tower-http = "0.5.2" @@ -305,7 +319,8 @@ prometheus = { version = "0.13.0" } # coconut/DKG related # unfortunately until https://github.com/zkcrypto/bls12_381/issues/10 is resolved, we have to rely on the fork # as we need to be able to serialize Gt so that we could create the lookup table for baby-step-giant-step algorithm -bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", default-features = false, branch = "feature/gt-serialization-0.8.0" } +# plus to make our live easier we need serde support from https://github.com/zkcrypto/bls12_381/pull/125 +bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", default-features = false, branch = "temp/experimental-serdect" } group = { version = "0.13.0", default-features = false } ff = { version = "0.13.0", default-features = false } @@ -328,16 +343,22 @@ cw-controllers = { version = "=1.1.0" } # cosmrs-related bip32 = { version = "0.5.1", default-features = false } -# temporarily using a fork again (yay.) because we need staking and slashing support -cosmrs = { git = "https://github.com/jstuczyn/cosmos-rust", branch = "nym-temp/all-validator-features" } -#cosmrs = { git = "https://github.com/jstuczyn/cosmos-rust", branch = "nym-temp/all-validator-features" } # unfortuntely we need a fork by yours truly to get the staking support -tendermint = "0.34" # same version as used by cosmrs -tendermint-rpc = "0.34" # same version as used by cosmrs +# temporarily using a fork again (yay.) because we need staking and slashing support (which are already on main but not released) +# plus response message parsing (which is, as of the time of writing this message, waiting to get merged) +#cosmrs = { path = "../cosmos-rust-fork/cosmos-rust/cosmrs" } +cosmrs = { git = "https://github.com/cosmos/cosmos-rust", rev = "4b1332e6d8258ac845cef71589c8d362a669675a" } # unfortuntely we need a fork by yours truly to get the staking support +tendermint = "0.37.0" # same version as used by cosmrs +tendermint-rpc = "0.37.0" # same version as used by cosmrs prost = { version = "0.12", default-features = false } # wasm-related dependencies gloo-utils = "0.2.0" gloo-net = "0.5.0" + +# use a separate branch due to feature unification failures +# this is blocked until the upstream removes outdates `wasm_bindgen` feature usage +# indexed_db_futures = "0.4.1" +indexed_db_futures = { git = "https://github.com/TiemenSch/rust-indexed-db", branch = "update-uuid" } js-sys = "0.3.69" serde-wasm-bindgen = "0.6.5" tsify = "0.4.5" @@ -345,7 +366,6 @@ wasm-bindgen = "0.2.92" wasm-bindgen-futures = "0.4.39" wasmtimer = "0.2.0" web-sys = "0.3.69" -itertools = "0.12.0" # Profile settings for individual crates diff --git a/Makefile b/Makefile index ca56edb30b..f5c323db7f 100644 --- a/Makefile +++ b/Makefile @@ -133,7 +133,7 @@ clippy: sdk-wasm-lint # Build contracts ready for deploy # ----------------------------------------------------------------------------- -CONTRACTS=vesting_contract mixnet_contract +CONTRACTS=vesting_contract mixnet_contract nym_ecash CONTRACTS_WASM=$(addsuffix .wasm, $(CONTRACTS)) CONTRACTS_OUT_DIR=contracts/target/wasm32-unknown-unknown/release diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index a951162886..7e253719c2 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-client" -version = "1.1.39" +version = "1.1.40" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] description = "Implementation of the Nym Client" edition = "2021" @@ -26,30 +26,46 @@ clap = { workspace = true, features = ["cargo", "derive"] } dirs = { workspace = true } log = { workspace = true } # self explanatory rand = { workspace = true } -serde = { workspace = true, features = ["derive"] } # for config serialization/deserialization +serde = { workspace = true, features = [ + "derive", +] } # for config serialization/deserialization serde_json = { workspace = true } thiserror = { workspace = true } tap = { workspace = true } time = { workspace = true } -tokio = { workspace = true, features = ["rt-multi-thread", "net", "signal"] } # async runtime +tokio = { workspace = true, features = [ + "rt-multi-thread", + "net", + "signal", +] } # async runtime tokio-tungstenite = { workspace = true } zeroize = { workspace = true } ## internal nym-bandwidth-controller = { path = "../../common/bandwidth-controller" } -nym-bin-common = { path = "../../common/bin-common", features = ["output_format", "clap"] } -nym-client-core = { path = "../../common/client-core", features = ["fs-surb-storage", "fs-gateways-storage", "cli"] } +nym-bin-common = { path = "../../common/bin-common", features = [ + "output_format", + "clap", +] } +nym-client-core = { path = "../../common/client-core", features = [ + "fs-credentials-storage", + "fs-surb-storage", + "fs-gateways-storage", + "cli", +] } nym-config = { path = "../../common/config" } nym-credential-storage = { path = "../../common/credential-storage" } nym-credentials = { path = "../../common/credentials" } nym-crypto = { path = "../../common/crypto" } -nym-gateway-requests = { path = "../../gateway/gateway-requests" } +nym-gateway-requests = { path = "../../common/gateway-requests" } nym-network-defaults = { path = "../../common/network-defaults" } nym-sphinx = { path = "../../common/nymsphinx" } nym-pemstore = { path = "../../common/pemstore" } nym-task = { path = "../../common/task" } nym-topology = { path = "../../common/topology" } -nym-validator-client = { path = "../../common/client-libs/validator-client", features = ["http-client"] } +nym-validator-client = { path = "../../common/client-libs/validator-client", features = [ + "http-client", +] } nym-client-websocket-requests = { path = "websocket-requests" } nym-id = { path = "../../common/nym-id" } diff --git a/clients/native/src/commands/mod.rs b/clients/native/src/commands/mod.rs index c8f3bb0c0d..31aa4ae8ec 100644 --- a/clients/native/src/commands/mod.rs +++ b/clients/native/src/commands/mod.rs @@ -26,6 +26,7 @@ pub(crate) mod import_credential; pub(crate) mod init; mod list_gateways; pub(crate) mod run; +mod show_ticketbooks; mod switch_gateway; pub(crate) struct CliNativeClient; @@ -84,6 +85,9 @@ pub(crate) enum Commands { /// Change the currently active gateway. Note that you must have already registered with the new gateway! SwitchGateway(switch_gateway::Args), + /// Display information associated with the imported ticketbooks, + ShowTicketbooks(show_ticketbooks::Args), + /// Show build information of this binary BuildInfo(build_info::BuildInfo), @@ -116,6 +120,7 @@ pub(crate) async fn execute(args: Cli) -> Result<(), Box list_gateways::execute(args).await?, Commands::AddGateway(args) => add_gateway::execute(args).await?, Commands::SwitchGateway(args) => switch_gateway::execute(args).await?, + Commands::ShowTicketbooks(args) => show_ticketbooks::execute(args).await?, Commands::BuildInfo(m) => build_info::execute(m), Commands::Completions(s) => s.generate(&mut Cli::command(), bin_name), Commands::GenerateFigSpec => fig_generate(&mut Cli::command(), bin_name), diff --git a/clients/native/src/commands/show_ticketbooks.rs b/clients/native/src/commands/show_ticketbooks.rs new file mode 100644 index 0000000000..2518a50ff2 --- /dev/null +++ b/clients/native/src/commands/show_ticketbooks.rs @@ -0,0 +1,32 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::commands::CliNativeClient; +use crate::error::ClientError; +use nym_bin_common::output_format::OutputFormat; +use nym_client_core::cli_helpers::client_show_ticketbooks::{ + show_ticketbooks, CommonShowTicketbooksArgs, +}; + +#[derive(clap::Args)] +pub(crate) struct Args { + #[command(flatten)] + common_args: CommonShowTicketbooksArgs, + + #[arg(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +impl AsRef for Args { + fn as_ref(&self) -> &CommonShowTicketbooksArgs { + &self.common_args + } +} + +pub(crate) async fn execute(args: Args) -> Result<(), ClientError> { + let output = args.output; + let res = show_ticketbooks::(args).await?; + + println!("{}", output.format(&res)); + Ok(()) +} diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index b7641e6496..ee15f6747c 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-socks5-client" -version = "1.1.39" +version = "1.1.40" authors = ["Dave Hrycyszyn "] description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address" edition = "2021" @@ -11,7 +11,9 @@ license.workspace = true bs58 = { workspace = true } clap = { workspace = true, features = ["cargo", "derive"] } log = { workspace = true } -serde = { workspace = true, features = ["derive"] } # for config serialization/deserialization +serde = { workspace = true, features = [ + "derive", +] } # for config serialization/deserialization serde_json = { workspace = true } tap = { workspace = true } thiserror = { workspace = true } @@ -22,13 +24,21 @@ url = { workspace = true } zeroize = { workspace = true } # internal -nym-bin-common = { path = "../../common/bin-common", features = ["output_format"] } -nym-client-core = { path = "../../common/client-core", features = ["fs-surb-storage", "fs-gateways-storage", "cli"] } +nym-bin-common = { path = "../../common/bin-common", features = [ + "output_format", + "clap", +] } +nym-client-core = { path = "../../common/client-core", features = [ + "fs-credentials-storage", + "fs-surb-storage", + "fs-gateways-storage", + "cli", +] } nym-config = { path = "../../common/config" } nym-credential-storage = { path = "../../common/credential-storage" } nym-credentials = { path = "../../common/credentials" } nym-crypto = { path = "../../common/crypto" } -nym-gateway-requests = { path = "../../gateway/gateway-requests" } +nym-gateway-requests = { path = "../../common/gateway-requests" } nym-id = { path = "../../common/nym-id" } nym-network-defaults = { path = "../../common/network-defaults" } nym-ordered-buffer = { path = "../../common/socks5/ordered-buffer" } @@ -36,7 +46,9 @@ nym-pemstore = { path = "../../common/pemstore" } nym-socks5-client-core = { path = "../../common/socks5-client-core" } nym-sphinx = { path = "../../common/nymsphinx" } nym-topology = { path = "../../common/topology" } -nym-validator-client = { path = "../../common/client-libs/validator-client", features = ["http-client"] } +nym-validator-client = { path = "../../common/client-libs/validator-client", features = [ + "http-client", +] } [features] default = [] diff --git a/clients/socks5/src/commands/mod.rs b/clients/socks5/src/commands/mod.rs index 59ff639e1c..34f0738a12 100644 --- a/clients/socks5/src/commands/mod.rs +++ b/clients/socks5/src/commands/mod.rs @@ -30,6 +30,7 @@ mod import_credential; pub mod init; mod list_gateways; pub(crate) mod run; +mod show_ticketbooks; mod switch_gateway; pub(crate) struct CliSocks5Client; @@ -88,6 +89,9 @@ pub(crate) enum Commands { /// Change the currently active gateway. Note that you must have already registered with the new gateway! SwitchGateway(switch_gateway::Args), + /// Display information associated with the imported ticketbooks, + ShowTicketbooks(show_ticketbooks::Args), + /// Show build information of this binary BuildInfo(build_info::BuildInfo), @@ -123,6 +127,7 @@ pub(crate) async fn execute(args: Cli) -> Result<(), Box list_gateways::execute(args).await?, Commands::AddGateway(args) => add_gateway::execute(args).await?, Commands::SwitchGateway(args) => switch_gateway::execute(args).await?, + Commands::ShowTicketbooks(args) => show_ticketbooks::execute(args).await?, Commands::BuildInfo(m) => build_info::execute(m), Commands::Completions(s) => s.generate(&mut Cli::command(), bin_name), Commands::GenerateFigSpec => fig_generate(&mut Cli::command(), bin_name), diff --git a/clients/socks5/src/commands/show_ticketbooks.rs b/clients/socks5/src/commands/show_ticketbooks.rs new file mode 100644 index 0000000000..41a37ef6eb --- /dev/null +++ b/clients/socks5/src/commands/show_ticketbooks.rs @@ -0,0 +1,32 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::commands::CliSocks5Client; +use crate::error::Socks5ClientError; +use nym_bin_common::output_format::OutputFormat; +use nym_client_core::cli_helpers::client_show_ticketbooks::{ + show_ticketbooks, CommonShowTicketbooksArgs, +}; + +#[derive(clap::Args)] +pub(crate) struct Args { + #[command(flatten)] + common_args: CommonShowTicketbooksArgs, + + #[arg(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +impl AsRef for Args { + fn as_ref(&self) -> &CommonShowTicketbooksArgs { + &self.common_args + } +} + +pub(crate) async fn execute(args: Args) -> Result<(), Socks5ClientError> { + let output = args.output; + let res = show_ticketbooks::(args).await?; + + println!("{}", output.format(&res)); + Ok(()) +} diff --git a/common/bandwidth-controller/Cargo.toml b/common/bandwidth-controller/Cargo.toml index b122849639..e56476afa6 100644 --- a/common/bandwidth-controller/Cargo.toml +++ b/common/bandwidth-controller/Cargo.toml @@ -14,13 +14,14 @@ thiserror = { workspace = true } url = { workspace = true } zeroize = { workspace = true } -nym-coconut = { path = "../nymcoconut" } +nym-ecash-time = { path = "../ecash-time" } nym-credential-storage = { path = "../credential-storage" } nym-credentials = { path = "../credentials" } nym-credentials-interface = { path = "../credentials-interface" } nym-crypto = { path = "../crypto", features = ["rand", "asymmetric", "symmetric", "aes", "hashing"] } nym-network-defaults = { path = "../network-defaults" } nym-validator-client = { path = "../client-libs/validator-client", default-features = false } +nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" } [target."cfg(not(target_arch = \"wasm32\"))".dependencies.nym-validator-client] path = "../client-libs/validator-client" diff --git a/common/bandwidth-controller/src/acquire/mod.rs b/common/bandwidth-controller/src/acquire/mod.rs index 4f2431b45b..e2d18d5545 100644 --- a/common/bandwidth-controller/src/acquire/mod.rs +++ b/common/bandwidth-controller/src/acquire/mod.rs @@ -1,87 +1,132 @@ -// Copyright 2023 - Nym Technologies SA +// Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::error::BandwidthControllerError; -use nym_credential_storage::models::StorableIssuedCredential; +use crate::utils::{ + get_aggregate_verification_key, get_coin_index_signatures, get_expiration_date_signatures, +}; +use log::info; use nym_credential_storage::storage::Storage; -use nym_credentials::coconut::bandwidth::{CredentialType, IssuanceBandwidthCredential}; -use nym_credentials::coconut::utils::obtain_aggregate_signature; -use nym_crypto::asymmetric::{encryption, identity}; -use nym_validator_client::coconut::all_coconut_api_clients; -use nym_validator_client::nyxd::contract_traits::CoconutBandwidthSigningClient; -use nym_validator_client::nyxd::contract_traits::DkgQueryClient; -use nym_validator_client::nyxd::Coin; +use nym_credentials::ecash::bandwidth::IssuanceTicketBook; +use nym_credentials::ecash::utils::obtain_aggregate_wallet; +use nym_credentials::IssuedTicketBook; +use nym_credentials_interface::TicketType; +use nym_crypto::asymmetric::identity; +use nym_ecash_time::{ecash_default_expiration_date, Date}; +use nym_validator_client::coconut::all_ecash_api_clients; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nyxd::contract_traits::EcashSigningClient; +use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, EcashQueryClient}; +use nym_validator_client::nyxd::cosmwasm_client::ToSingletonContractData; +use nym_validator_client::EcashApiClient; use rand::rngs::OsRng; -use state::State; -use zeroize::Zeroizing; -pub mod state; - -pub async fn deposit(client: &C, amount: Coin) -> Result +pub async fn make_deposit( + client: &C, + client_id: &[u8], + expiration: Option, + ticketbook_type: TicketType, +) -> Result where - C: CoconutBandwidthSigningClient + Sync, + C: EcashSigningClient + EcashQueryClient + Sync, { let mut rng = OsRng; let signing_key = identity::PrivateKey::new(&mut rng); - let encryption_key = encryption::PrivateKey::new(&mut rng); + let expiration = expiration.unwrap_or_else(ecash_default_expiration_date); - let tx_hash = client - .deposit( - amount.clone(), - CredentialType::Voucher.to_string(), + let deposit_amount = client.get_required_deposit_amount().await?; + info!("we'll need to deposit {deposit_amount} to obtain the ticketbook"); + let result = client + .make_ticketbook_deposit( signing_key.public_key().to_base58_string(), - encryption_key.public_key().to_base58_string(), + deposit_amount.into(), None, ) - .await? - .transaction_hash; + .await?; - let voucher = - IssuanceBandwidthCredential::new_voucher(amount, tx_hash, signing_key, encryption_key); + let deposit_id = result.parse_singleton_u32_contract_data()?; - let state = State { voucher }; + info!("our ticketbook deposit has been stored under id {deposit_id}"); - Ok(state) + Ok(IssuanceTicketBook::new_with_expiration( + deposit_id, + client_id, + signing_key, + ticketbook_type, + expiration, + )) } -pub async fn get_bandwidth_voucher( - state: &State, +pub async fn query_and_persist_required_global_data( + storage: &S, + epoch_id: EpochId, + expiration_date: Date, + apis: Vec, +) -> Result<(), BandwidthControllerError> +where + S: Storage, + ::StorageError: Send + Sync + 'static, +{ + log::info!("Getting master verification key"); + // this will also persist the key in the storage if was not there already + get_aggregate_verification_key(storage, epoch_id, apis.clone()).await?; + + log::info!("Getting expiration date signatures"); + // this will also persist the signatures in the storage if they were not there already + get_expiration_date_signatures(storage, epoch_id, expiration_date, apis.clone()).await?; + + log::info!("Getting coin indices signatures"); + // this will also persist the signatures in the storage if they were not there already + get_coin_index_signatures(storage, epoch_id, apis).await?; + Ok(()) +} + +pub async fn get_ticket_book( + issuance_data: &IssuanceTicketBook, client: &C, storage: &St, -) -> Result<(), BandwidthControllerError> + apis: Option>, +) -> Result where C: DkgQueryClient + Send + Sync, St: Storage, ::StorageError: Send + Sync + 'static, { - // temporary - assert!(state.voucher.typ().is_voucher()); - let epoch_id = client.get_current_epoch().await?.epoch_id; let threshold = client .get_current_epoch_threshold() .await? .ok_or(BandwidthControllerError::NoThreshold)?; - let coconut_api_clients = all_coconut_api_clients(client, epoch_id).await?; - - let signature = - obtain_aggregate_signature(&state.voucher, &coconut_api_clients, threshold).await?; - let issued = state.voucher.to_issued_credential(signature, epoch_id); - - // make sure the data gets zeroized after persisting it - let credential_data = Zeroizing::new(issued.pack_v1()); - let storable = StorableIssuedCredential { - serialization_revision: issued.current_serialization_revision(), - credential_data: credential_data.as_ref(), - credential_type: issued.typ().to_string(), - epoch_id: epoch_id - .try_into() - .expect("our epoch is has run over u32::MAX!"), + let apis = match apis { + Some(apis) => apis, + None => all_ecash_api_clients(client, epoch_id).await?, }; + log::info!("Querying wallet signatures"); + let wallet = obtain_aggregate_wallet(issuance_data, &apis, threshold).await?; + info!("managed to obtain sufficient number of partial signatures!"); + + log::info!("Getting expiration date signatures"); + // this will also persist the signatures in the storage if they were not there already + get_expiration_date_signatures( + storage, + epoch_id, + issuance_data.expiration_date(), + apis.clone(), + ) + .await?; + + log::info!("Getting coin indices signatures"); + // this will also persist the signatures in the storage if they were not there already + get_coin_index_signatures(storage, epoch_id, apis).await?; + + let issued = issuance_data.to_issued_ticketbook(wallet, epoch_id); + + info!("persisting the ticketbook into the storage..."); storage - .insert_issued_credential(storable) + .insert_issued_ticketbook(&issued) .await - .map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err))) + .map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err)))?; + Ok(issued) } diff --git a/common/bandwidth-controller/src/acquire/state.rs b/common/bandwidth-controller/src/acquire/state.rs deleted file mode 100644 index 68945289b7..0000000000 --- a/common/bandwidth-controller/src/acquire/state.rs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2022-2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use nym_credentials::coconut::bandwidth::IssuanceBandwidthCredential; - -pub struct State { - pub voucher: IssuanceBandwidthCredential, -} - -impl State { - pub fn new(voucher: IssuanceBandwidthCredential) -> Self { - State { voucher } - } -} diff --git a/common/bandwidth-controller/src/error.rs b/common/bandwidth-controller/src/error.rs index 44832aaab4..01619b53ed 100644 --- a/common/bandwidth-controller/src/error.rs +++ b/common/bandwidth-controller/src/error.rs @@ -1,12 +1,12 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_coconut::CoconutError; use nym_credential_storage::error::StorageError; use nym_credentials::error::Error as CredentialsError; +use nym_credentials_interface::CompactEcashError; use nym_crypto::asymmetric::encryption::KeyRecoveryError; use nym_crypto::asymmetric::identity::Ed25519RecoveryError; -use nym_validator_client::coconut::CoconutApiError; +use nym_validator_client::coconut::EcashApiError; use nym_validator_client::error::ValidatorClientError; use thiserror::Error; @@ -16,7 +16,7 @@ pub enum BandwidthControllerError { Nyxd(#[from] nym_validator_client::nyxd::error::NyxdError), #[error("coconut api query failure: {0}")] - CoconutApiError(#[from] CoconutApiError), + CoconutApiError(#[from] EcashApiError), #[error("There was a credential storage error - {0}")] CredentialStorageError(Box), @@ -28,8 +28,8 @@ pub enum BandwidthControllerError { #[error(transparent)] StorageError(#[from] StorageError), - #[error("Coconut error - {0}")] - CoconutError(#[from] CoconutError), + #[error("Ecash error - {0}")] + EcashError(#[from] CompactEcashError), #[error("Validator client error - {0}")] ValidatorError(#[from] ValidatorClientError), @@ -51,4 +51,15 @@ pub enum BandwidthControllerError { #[error("can't handle recovering storage with revision {stored}. {expected} was expected")] UnsupportedCredentialStorageRevision { stored: u8, expected: u8 }, + + #[error("did not receive a valid response for aggregated data ({typ}) from ANY nym-api")] + ExhaustedApiQueries { typ: String }, +} + +impl BandwidthControllerError { + pub fn credential_storage_error( + source: impl std::error::Error + Send + Sync + 'static, + ) -> Self { + BandwidthControllerError::CredentialStorageError(Box::new(source)) + } } diff --git a/common/bandwidth-controller/src/lib.rs b/common/bandwidth-controller/src/lib.rs index 91201b9a92..a164d3df60 100644 --- a/common/bandwidth-controller/src/lib.rs +++ b/common/bandwidth-controller/src/lib.rs @@ -1,16 +1,24 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] +#![warn(clippy::todo)] +#![warn(clippy::dbg_macro)] + use crate::error::BandwidthControllerError; -use crate::utils::stored_credential_to_issued_bandwidth; -use log::{debug, error, warn}; +use crate::utils::{ + get_aggregate_verification_key, get_coin_index_signatures, get_expiration_date_signatures, + ApiClientsWrapper, +}; +use log::error; +use nym_credential_storage::models::RetrievedTicketbook; use nym_credential_storage::storage::Storage; -use nym_credentials::coconut::bandwidth::issued::BandwidthCredentialIssuedDataVariant; -use nym_credentials::coconut::bandwidth::CredentialSpendingData; -use nym_credentials::coconut::utils::obtain_aggregate_verification_key; -use nym_credentials::IssuedBandwidthCredential; -use nym_credentials_interface::VerificationKey; -use nym_validator_client::coconut::all_coconut_api_clients; +use nym_credentials::ecash::bandwidth::CredentialSpendingData; +use nym_credentials_interface::{ + AnnotatedCoinIndexSignature, AnnotatedExpirationDateSignature, VerificationKeyAuth, +}; +use nym_ecash_time::Date; use nym_validator_client::nym_api::EpochId; use nym_validator_client::nyxd::contract_traits::DkgQueryClient; @@ -35,13 +43,20 @@ pub struct PreparedCredential { /// could use correct verification key for validation. pub epoch_id: EpochId, - /// The database id of the stored credential. - pub credential_id: i64, + /// Auxiliary metadata associated with the withdrawn credential + pub metadata: PreparedCredentialMetadata, } -pub struct RetrievedCredential { - pub credential: IssuedBandwidthCredential, - pub credential_id: i64, +#[derive(Copy, Clone)] +pub struct PreparedCredentialMetadata { + /// The database id of the stored credential. + pub ticketbook_id: i64, + + /// The number of tickets withdrawn in this credential + pub tickets_withdrawn: u32, + + /// The amount of tickets used INCLUDING those tickets that JUST got withdrawn + pub used_tickets: u32, } impl BandwidthController { @@ -50,111 +65,157 @@ impl BandwidthController { } /// Tries to retrieve one of the stored, unused credentials that hasn't yet expired. - /// It marks any retrieved intermediate credentials as expired. - pub async fn get_next_usable_credential( + pub async fn get_next_usable_ticketbook( &self, - gateway_id: &str, - ) -> Result + tickets: u32, + ) -> Result where ::StorageError: Send + Sync + 'static, { - loop { - let Some(maybe_next) = self - .storage - .get_next_unspent_credential(gateway_id) - .await - .map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err)))? - else { - return Err(BandwidthControllerError::NoCredentialsAvailable); - }; - let id = maybe_next.id; + let Some(ticketbook) = self + .storage + .get_next_unspent_usable_ticketbook(tickets) + .await + .map_err(BandwidthControllerError::credential_storage_error)? + else { + return Err(BandwidthControllerError::NoCredentialsAvailable); + }; - // try to deserialize it - let valid_credential = match stored_credential_to_issued_bandwidth(maybe_next) { - // check if it has already expired - Ok(credential) => match credential.variant_data() { - BandwidthCredentialIssuedDataVariant::Voucher(_) => { - debug!("credential {id} is a bandwidth voucher"); - credential - } - BandwidthCredentialIssuedDataVariant::FreePass(freepass_info) => { - debug!("credential {id} is a free pass"); - if freepass_info.expired() { - warn!("the free pass (id: {id}) has already expired! The expiration was set to {}", freepass_info.expiry_date()); - self.storage.mark_expired(id).await.map_err(|err| { - BandwidthControllerError::CredentialStorageError(Box::new(err)) - })?; - continue; - } - credential - } - }, - Err(err) => { - error!("failed to deserialize credential with id {id}: {err}. it may need to be manually removed from the storage"); - return Err(err); - } - }; - return Ok(RetrievedCredential { - credential: valid_credential, - credential_id: id, - }); - } + Ok(ticketbook) } - pub fn storage(&self) -> &St { - &self.storage + pub async fn attempt_revert_ticket_usage( + &self, + info: PreparedCredentialMetadata, + ) -> Result + where + ::StorageError: Send + Sync + 'static, + { + self.storage + .attempt_revert_ticketbook_withdrawal( + info.ticketbook_id, + info.used_tickets, + info.tickets_withdrawn, + ) + .await + .map_err(BandwidthControllerError::credential_storage_error) } async fn get_aggregate_verification_key( &self, epoch_id: EpochId, - ) -> Result + apis: &mut ApiClientsWrapper, + ) -> Result where C: DkgQueryClient + Sync + Send, ::StorageError: Send + Sync + 'static, { - let coconut_api_clients = all_coconut_api_clients(&self.client, epoch_id).await?; - Ok(obtain_aggregate_verification_key(&coconut_api_clients)?) + let ecash_apis = apis.get_or_init(epoch_id, &self.client).await?; + get_aggregate_verification_key(&self.storage, epoch_id, ecash_apis).await } - pub async fn prepare_bandwidth_credential( + async fn get_coin_index_signatures( &self, - gateway_id: &str, + epoch_id: EpochId, + apis: &mut ApiClientsWrapper, + ) -> Result, BandwidthControllerError> + where + C: DkgQueryClient + Sync + Send, + ::StorageError: Send + Sync + 'static, + { + let ecash_apis = apis.get_or_init(epoch_id, &self.client).await?; + get_coin_index_signatures(&self.storage, epoch_id, ecash_apis).await + } + + async fn get_expiration_date_signatures( + &self, + epoch_id: EpochId, + expiration_date: Date, + apis: &mut ApiClientsWrapper, + ) -> Result, BandwidthControllerError> + where + C: DkgQueryClient + Sync + Send, + ::StorageError: Send + Sync + 'static, + { + let ecash_apis = apis.get_or_init(epoch_id, &self.client).await?; + get_expiration_date_signatures(&self.storage, epoch_id, expiration_date, ecash_apis).await + } + + async fn prepare_ecash_ticket_inner( + &self, + provider_pk: [u8; 32], + tickets_to_spend: u32, + mut retrieved_ticketbook: RetrievedTicketbook, + ) -> Result + where + C: DkgQueryClient + Sync + Send, + ::StorageError: Send + Sync + 'static, + { + let epoch_id = retrieved_ticketbook.ticketbook.epoch_id(); + let expiration_date = retrieved_ticketbook.ticketbook.expiration_date(); + let mut api_clients = Default::default(); + + let verification_key = self + .get_aggregate_verification_key(epoch_id, &mut api_clients) + .await?; + let expiration_signatures = self + .get_expiration_date_signatures(epoch_id, expiration_date, &mut api_clients) + .await?; + let coin_indices_signatures = self + .get_coin_index_signatures(epoch_id, &mut api_clients) + .await?; + + let pay_info = retrieved_ticketbook + .ticketbook + .generate_pay_info(provider_pk); + + let spend_request = retrieved_ticketbook.ticketbook.prepare_for_spending( + &verification_key, + pay_info.into(), + &coin_indices_signatures, + &expiration_signatures, + tickets_to_spend as u64, + )?; + Ok(spend_request) + } + + pub async fn prepare_ecash_ticket( + &self, + provider_pk: [u8; 32], + tickets_to_spend: u32, ) -> Result where C: DkgQueryClient + Sync + Send, ::StorageError: Send + Sync + 'static, { - let retrieved_credential = self.get_next_usable_credential(gateway_id).await?; + let retrieved_ticketbook = self.get_next_usable_ticketbook(tickets_to_spend).await?; - let epoch_id = retrieved_credential.credential.epoch_id(); - let credential_id = retrieved_credential.credential_id; + let ticketbook_id = retrieved_ticketbook.ticketbook_id; + let epoch_id = retrieved_ticketbook.ticketbook.epoch_id(); - let verification_key = self.get_aggregate_verification_key(epoch_id).await?; + let used_tickets = + retrieved_ticketbook.ticketbook.spent_tickets() as u32 + tickets_to_spend; + let metadata = PreparedCredentialMetadata { + ticketbook_id, + tickets_withdrawn: tickets_to_spend, + used_tickets, + }; - let spend_request = retrieved_credential - .credential - .prepare_for_spending(&verification_key)?; - - Ok(PreparedCredential { - data: spend_request, - epoch_id, - credential_id, - }) - } - - pub async fn consume_credential( - &self, - id: i64, - gateway_id: &str, - ) -> Result<(), BandwidthControllerError> - where - ::StorageError: Send + Sync + 'static, - { - self.storage - .consume_coconut_credential(id, gateway_id) + match self + .prepare_ecash_ticket_inner(provider_pk, tickets_to_spend, retrieved_ticketbook) .await - .map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err))) + { + Ok(data) => Ok(PreparedCredential { + data, + epoch_id, + metadata, + }), + Err(err) => { + error!("failed to prepare credential spending request. attempting to revert withdrawal..."); + self.attempt_revert_ticket_usage(metadata).await?; + Err(err) + } + } } } diff --git a/common/bandwidth-controller/src/utils.rs b/common/bandwidth-controller/src/utils.rs index 89891dba48..0d2427b2f8 100644 --- a/common/bandwidth-controller/src/utils.rs +++ b/common/bandwidth-controller/src/utils.rs @@ -2,21 +2,180 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::BandwidthControllerError; -use nym_credential_storage::models::StoredIssuedCredential; -use nym_credentials::coconut::bandwidth::issued::CURRENT_SERIALIZATION_REVISION; -use nym_credentials::coconut::bandwidth::IssuedBandwidthCredential; +use log::warn; +use nym_credential_storage::storage::Storage; +use nym_credentials_interface::{ + AnnotatedCoinIndexSignature, AnnotatedExpirationDateSignature, VerificationKeyAuth, +}; +use nym_ecash_time::Date; +use nym_validator_client::coconut::all_ecash_api_clients; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nyxd::contract_traits::DkgQueryClient; +use nym_validator_client::EcashApiClient; +use rand::prelude::SliceRandom; +use rand::thread_rng; +use std::fmt::Display; +use std::future::Future; -pub fn stored_credential_to_issued_bandwidth( - cred: StoredIssuedCredential, -) -> Result { - if cred.serialization_revision != CURRENT_SERIALIZATION_REVISION { - return Err( - BandwidthControllerError::UnsupportedCredentialStorageRevision { - stored: cred.serialization_revision, - expected: CURRENT_SERIALIZATION_REVISION, - }, - ); +// it really doesn't need the RwLock because it's never moved across tasks, +// but we need all the Send/Sync action +#[derive(Default)] +pub(crate) struct ApiClientsWrapper(Option>); + +impl ApiClientsWrapper { + pub(crate) async fn get_or_init( + &mut self, + epoch_id: EpochId, + dkg_client: &C, + ) -> Result, BandwidthControllerError> + where + C: DkgQueryClient + Sync + Send, + { + if let Some(cached) = &self.0 { + return Ok(cached.clone()); + } + + let clients = all_ecash_api_clients(dkg_client, epoch_id).await?; + + // technically we don't have to be cloning all the clients here, but it's way simpler than + // dealing with locking and whatnot given the performance penalty is negligible + self.0 = Some(clients.clone()); + Ok(clients) } - - Ok(IssuedBandwidthCredential::unpack_v1(&cred.credential_data)?) +} + +pub(crate) async fn query_random_apis_until_success( + mut apis: Vec, + f: F, + typ: impl Into, +) -> Result +where + F: Fn(EcashApiClient) -> U, + U: Future>, + E: Display, +{ + // try apis in pseudorandom way to remove any bias towards the first registered dealer + apis.shuffle(&mut thread_rng()); + + for api in apis { + let disp = api.to_string(); + match f(api).await { + Ok(res) => return Ok(res), + Err(err) => { + warn!("failed to obtain valid response from API {disp}: {err}") + } + } + } + Err(BandwidthControllerError::ExhaustedApiQueries { typ: typ.into() }) +} + +pub(crate) async fn get_aggregate_verification_key( + storage: &St, + epoch_id: EpochId, + ecash_apis: Vec, +) -> Result +where + St: Storage, + ::StorageError: Send + Sync + 'static, +{ + if let Some(stored) = storage + .get_master_verification_key(epoch_id) + .await + .map_err(BandwidthControllerError::credential_storage_error)? + { + return Ok(stored); + }; + + let master_vk = query_random_apis_until_success( + ecash_apis, + |api| async move { api.api_client.master_verification_key(Some(epoch_id)).await }, + format!("aggregated verification key for epoch {epoch_id}"), + ) + .await? + .key; + + // store the retrieved key + storage + .insert_master_verification_key(epoch_id, &master_vk) + .await + .map_err(BandwidthControllerError::credential_storage_error)?; + + Ok(master_vk) +} + +pub(crate) async fn get_coin_index_signatures( + storage: &St, + epoch_id: EpochId, + ecash_apis: Vec, +) -> Result, BandwidthControllerError> +where + St: Storage, + ::StorageError: Send + Sync + 'static, +{ + if let Some(stored) = storage + .get_coin_index_signatures(epoch_id) + .await + .map_err(BandwidthControllerError::credential_storage_error)? + { + return Ok(stored); + }; + + let index_sigs = query_random_apis_until_success( + ecash_apis, + |api| async move { + api.api_client + .global_coin_indices_signatures(Some(epoch_id)) + .await + }, + format!("aggregated coin index signatures for epoch {epoch_id}"), + ) + .await? + .signatures; + + // store the retrieved key + storage + .insert_coin_index_signatures(epoch_id, &index_sigs) + .await + .map_err(BandwidthControllerError::credential_storage_error)?; + + Ok(index_sigs) +} + +pub(crate) async fn get_expiration_date_signatures( + storage: &St, + epoch_id: EpochId, + expiration_date: Date, + ecash_apis: Vec, +) -> Result, BandwidthControllerError> +where + St: Storage, + ::StorageError: Send + Sync + 'static, +{ + if let Some(stored) = storage + .get_expiration_date_signatures(expiration_date) + .await + .map_err(BandwidthControllerError::credential_storage_error)? + { + return Ok(stored); + }; + + let expiration_sigs = query_random_apis_until_success( + ecash_apis, + |api| async move { + api.api_client + .global_expiration_date_signatures(Some(expiration_date)) + .await + }, + format!("aggregated coin index signatures for date {expiration_date}"), + ) + .await? + .signatures; + + // store the retrieved key + storage + .insert_expiration_date_signatures(epoch_id, expiration_date, &expiration_sigs) + .await + .map_err(BandwidthControllerError::credential_storage_error)?; + + Ok(expiration_sigs) } diff --git a/common/client-core/Cargo.toml b/common/client-core/Cargo.toml index 1d7c34d51c..763f0d8967 100644 --- a/common/client-core/Cargo.toml +++ b/common/client-core/Cargo.toml @@ -14,6 +14,7 @@ base64 = { workspace = true } bs58 = { workspace = true } cfg-if = { workspace = true } clap = { workspace = true, optional = true } +comfy-table = { version = "7.1.1", optional = true } futures = { workspace = true } humantime-serde = { workspace = true } log = { workspace = true } @@ -37,7 +38,7 @@ nym-country-group = { path = "../country-group" } nym-crypto = { path = "../crypto" } nym-explorer-client = { path = "../../explorer-api/explorer-client" } nym-gateway-client = { path = "../client-libs/gateway-client" } -nym-gateway-requests = { path = "../../gateway/gateway-requests" } +nym-gateway-requests = { path = "../gateway-requests" } nym-metrics = { path = "../nym-metrics" } nym-nonexhaustive-delayqueue = { path = "../nonexhaustive-delayqueue" } nym-sphinx = { path = "../nymsphinx" } @@ -45,11 +46,15 @@ nym-pemstore = { path = "../pemstore" } nym-topology = { path = "../topology", features = ["serializable"] } nym-validator-client = { path = "../client-libs/validator-client", default-features = false } nym-task = { path = "../task" } +nym-credentials-interface = { path = "../credentials-interface" } nym-credential-storage = { path = "../credential-storage" } nym-network-defaults = { path = "../network-defaults" } -nym-client-core-config-types = { path = "./config-types", features = ["disk-persistence"] } +nym-client-core-config-types = { path = "./config-types", features = [ + "disk-persistence", +] } nym-client-core-surb-storage = { path = "./surb-storage" } nym-client-core-gateways-storage = { path = "./gateways-storage" } +nym-ecash-time = { path = "../ecash-time" } ### For serving prometheus metrics [target."cfg(not(target_arch = \"wasm32\"))".dependencies.hyper] @@ -112,7 +117,8 @@ tempfile = { workspace = true } [features] default = [] -cli = ["clap"] +cli = ["clap", "comfy-table"] +fs-credentials-storage = ["nym-credential-storage/persistent-storage"] fs-surb-storage = ["nym-client-core-surb-storage/fs-surb-storage"] fs-gateways-storage = ["nym-client-core-gateways-storage/fs-gateways-storage"] wasm = ["nym-gateway-client/wasm"] diff --git a/common/client-core/gateways-storage/Cargo.toml b/common/client-core/gateways-storage/Cargo.toml index 4392798c0b..e91c889ed6 100644 --- a/common/client-core/gateways-storage/Cargo.toml +++ b/common/client-core/gateways-storage/Cargo.toml @@ -18,7 +18,7 @@ url.workspace = true zeroize = { workspace = true, features = ["zeroize_derive"] } nym-crypto = { path = "../../crypto", features = ["asymmetric"] } -nym-gateway-requests = { path = "../../../gateway/gateway-requests" } +nym-gateway-requests = { path = "../../gateway-requests" } [target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] workspace = true @@ -27,7 +27,12 @@ optional = true [build-dependencies] tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } -sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } +sqlx = { workspace = true, features = [ + "runtime-tokio-rustls", + "sqlite", + "macros", + "migrate", +] } [features] -fs-gateways-storage = ["sqlx"] \ No newline at end of file +fs-gateways-storage = ["sqlx"] diff --git a/common/client-core/src/cli_helpers/client_show_ticketbooks.rs b/common/client-core/src/cli_helpers/client_show_ticketbooks.rs new file mode 100644 index 0000000000..3a91a6665b --- /dev/null +++ b/common/client-core/src/cli_helpers/client_show_ticketbooks.rs @@ -0,0 +1,140 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli_helpers::{CliClient, CliClientConfig}; +use crate::error::ClientCoreError; +use nym_credential_storage::models::BasicTicketbookInformation; +use nym_credential_storage::storage::Storage; +use nym_credentials_interface::TicketType; +use nym_ecash_time::ecash_today; +use serde::{Deserialize, Serialize}; +use time::Date; + +#[derive(Serialize, Deserialize)] +pub struct AvailableTicketbook { + pub id: i64, + pub typ: TicketType, + pub expiration: Date, + pub issued_tickets: u32, + pub claimed_tickets: u32, + pub ticket_size: u64, +} + +impl AvailableTicketbook { + #[cfg(feature = "cli")] + fn table_row(&self) -> comfy_table::Row { + let ecash_today = ecash_today().date(); + + let issued = self.issued_tickets; + let si_issued = si_scale::helpers::bibytes2((issued as u64 * self.ticket_size) as f64); + + let claimed = self.claimed_tickets; + let si_claimed = si_scale::helpers::bibytes2((claimed as u64 * self.ticket_size) as f64); + + let remaining = issued - claimed; + let si_remaining = + si_scale::helpers::bibytes2((remaining as u64 * self.ticket_size) as f64); + let si_size = si_scale::helpers::bibytes2(self.ticket_size as f64); + + let expiration = if self.expiration <= ecash_today { + comfy_table::Cell::new(format!("EXPIRED ON {}", self.expiration)) + .fg(comfy_table::Color::Red) + .add_attribute(comfy_table::Attribute::Bold) + } else { + comfy_table::Cell::new(self.expiration.to_string()) + }; + + vec![ + comfy_table::Cell::new(self.id.to_string()), + comfy_table::Cell::new(self.typ), + expiration, + comfy_table::Cell::new(format!("{issued} ({si_issued})")), + comfy_table::Cell::new(format!("{claimed} ({si_claimed})")), + comfy_table::Cell::new(format!("{remaining} ({si_remaining})")), + comfy_table::Cell::new(si_size), + ] + .into() + } +} + +impl TryFrom for AvailableTicketbook { + type Error = ClientCoreError; + + fn try_from(value: BasicTicketbookInformation) -> Result { + let typ = value + .ticketbook_type + .parse() + .map_err(|_| ClientCoreError::UnknownTicketType)?; + Ok(AvailableTicketbook { + id: value.id, + typ, + expiration: value.expiration_date, + issued_tickets: value.total_tickets, + claimed_tickets: value.used_tickets, + ticket_size: typ.to_repr().bandwidth_value(), + }) + } +} + +#[derive(Serialize, Deserialize)] +#[serde(transparent)] +pub struct AvailableTicketbooks(Vec); + +#[cfg(feature = "cli")] +impl std::fmt::Display for AvailableTicketbooks { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut table = comfy_table::Table::new(); + table.set_header(vec![ + "id", + "type", + "expiration", + "issued tickets (bandwidth)", + "claimed tickets (bandwidth)", + "remaining tickets (bandwidth)", + "ticket size", + ]); + + for ticketbook in &self.0 { + table.add_row(ticketbook.table_row()); + } + + writeln!(f, "{table}")?; + Ok(()) + } +} + +#[cfg_attr(feature = "cli", derive(clap::Args))] +#[derive(Debug, Clone)] +pub struct CommonShowTicketbooksArgs { + /// Id of client that is going to display the ticketbook information + #[cfg_attr(feature = "cli", clap(long))] + pub id: String, +} + +pub async fn show_ticketbooks(args: A) -> Result +where + A: AsRef, + C: CliClient, +{ + let common_args = args.as_ref(); + let id = &common_args.id; + + let config = C::try_load_current_config(id).await?; + let paths = config.common_paths(); + + let credentials_store = + nym_credential_storage::initialise_persistent_storage(&paths.credentials_database).await; + let ticketbooks = credentials_store + .get_ticketbooks_info() + .await + .map_err(|err| ClientCoreError::CredentialStoreError { + source: Box::new(err), + })?; + + Ok(AvailableTicketbooks( + ticketbooks + .into_iter() + .map(TryInto::::try_into) + .collect::>()?, + )) +} diff --git a/common/client-core/src/cli_helpers/mod.rs b/common/client-core/src/cli_helpers/mod.rs index 5ab51b8b4c..a3660a60e6 100644 --- a/common/client-core/src/cli_helpers/mod.rs +++ b/common/client-core/src/cli_helpers/mod.rs @@ -6,6 +6,7 @@ pub mod client_import_credential; pub mod client_init; pub mod client_list_gateways; pub mod client_run; +pub mod client_show_ticketbooks; pub mod client_switch_gateway; pub mod traits; mod types; diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index 804f16869c..7d494bce35 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -40,6 +40,7 @@ use nym_bandwidth_controller::BandwidthController; use nym_client_core_gateways_storage::{GatewayDetails, GatewaysDetailsStore}; use nym_credential_storage::storage::Storage as CredentialStorage; use nym_crypto::asymmetric::{encryption, identity}; +use nym_gateway_client::client::config::GatewayClientConfig; use nym_gateway_client::{ AcknowledgementReceiver, GatewayClient, GatewayConfig, MixnetMessageReceiver, PacketRouter, }; @@ -403,6 +404,11 @@ where gateway_listener, ); GatewayClient::new( + GatewayClientConfig::new_default() + .with_disabled_credentials_mode(config.client.disabled_credentials_mode) + .with_response_timeout( + config.debug.gateway_connection.gateway_response_timeout, + ), cfg, managed_keys.identity_keypair(), Some(details.derived_aes128_ctr_blake3_hmac_keys), @@ -410,8 +416,6 @@ where bandwidth_controller, shutdown, ) - .with_disabled_credentials_mode(config.client.disabled_credentials_mode) - .with_response_timeout(config.debug.gateway_connection.gateway_response_timeout) }; gateway_client diff --git a/common/client-core/src/client/base_client/storage/mod.rs b/common/client-core/src/client/base_client/storage/mod.rs index e7c15315a6..8e325ddcd5 100644 --- a/common/client-core/src/client/base_client/storage/mod.rs +++ b/common/client-core/src/client/base_client/storage/mod.rs @@ -23,7 +23,7 @@ use crate::{ config::{self, disk_persistence::CommonClientPaths}, error::ClientCoreError, }; -#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "fs-credentials-storage"))] use nym_credential_storage::persistent_storage::PersistentStorage as PersistentCredentialStorage; pub use nym_client_core_gateways_storage as gateways_storage; diff --git a/common/client-core/src/client/mix_traffic/transceiver.rs b/common/client-core/src/client/mix_traffic/transceiver.rs index b6197fb490..0862911d93 100644 --- a/common/client-core/src/client/mix_traffic/transceiver.rs +++ b/common/client-core/src/client/mix_traffic/transceiver.rs @@ -3,10 +3,12 @@ use async_trait::async_trait; use log::{debug, error}; +use nym_credential_storage::storage::Storage as CredentialStorage; use nym_crypto::asymmetric::identity; use nym_gateway_client::GatewayClient; pub use nym_gateway_client::{GatewayPacketRouter, PacketRouter}; use nym_sphinx::forwarding::packet::MixPacket; +use nym_validator_client::nyxd::contract_traits::DkgQueryClient; use std::fmt::Debug; use std::os::raw::c_int as RawFd; use thiserror::Error; @@ -111,8 +113,9 @@ impl RemoteGateway { impl GatewayTransceiver for RemoteGateway where - C: Send, - St: Send, + C: DkgQueryClient + Send + Sync, + St: CredentialStorage, + ::StorageError: Send + Sync + 'static, { fn gateway_identity(&self) -> identity::PublicKey { self.gateway_client.gateway_identity() @@ -126,8 +129,9 @@ where #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl GatewaySender for RemoteGateway where - C: Send, - St: Send, + C: DkgQueryClient + Send + Sync, + St: CredentialStorage, + ::StorageError: Send + Sync + 'static, { async fn send_mix_packet(&mut self, packet: MixPacket) -> Result<(), ErasedGatewayError> { self.gateway_client diff --git a/common/client-core/src/error.rs b/common/client-core/src/error.rs index bdfecfda5e..cb9b9126a3 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -63,6 +63,14 @@ pub enum ClientCoreError { source: Box, }, + #[error("experienced a failure with our credentials storage: {source}")] + CredentialStoreError { + source: Box, + }, + + #[error("the provided ticket type is invalid")] + UnknownTicketType, + #[error("the gateway id is invalid - {0}")] UnableToCreatePublicKeyFromGatewayId(Ed25519RecoveryError), diff --git a/common/client-core/src/lib.rs b/common/client-core/src/lib.rs index 1eb90ddcf7..ffa1402859 100644 --- a/common/client-core/src/lib.rs +++ b/common/client-core/src/lib.rs @@ -2,7 +2,9 @@ use std::future::Future; #[cfg(all( not(target_arch = "wasm32"), + feature = "cli", feature = "fs-surb-storage", + feature = "fs-credentials-storage", feature = "fs-gateways-storage" ))] pub mod cli_helpers; diff --git a/common/client-libs/gateway-client/Cargo.toml b/common/client-libs/gateway-client/Cargo.toml index 9eb2e96402..640d9694df 100644 --- a/common/client-libs/gateway-client/Cargo.toml +++ b/common/client-libs/gateway-client/Cargo.toml @@ -24,7 +24,7 @@ nym-bandwidth-controller = { path = "../../bandwidth-controller" } nym-credentials = { path = "../../credentials" } nym-credential-storage = { path = "../../credential-storage" } nym-crypto = { path = "../../crypto" } -nym-gateway-requests = { path = "../../../gateway/gateway-requests" } +nym-gateway-requests = { path = "../../gateway-requests" } nym-network-defaults = { path = "../../network-defaults" } nym-sphinx = { path = "../../nymsphinx" } nym-pemstore = { path = "../../pemstore" } diff --git a/common/client-libs/gateway-client/src/bandwidth.rs b/common/client-libs/gateway-client/src/bandwidth.rs new file mode 100644 index 0000000000..f232c004ec --- /dev/null +++ b/common/client-libs/gateway-client/src/bandwidth.rs @@ -0,0 +1,89 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use si_scale::helpers::bibytes2; +use std::sync::atomic::{AtomicI64, Ordering}; +use std::sync::Arc; +use std::time::Duration; +use time::OffsetDateTime; + +#[derive(Clone, Default)] +pub struct ClientBandwidth { + inner: Arc, +} + +#[derive(Default)] +struct ClientBandwidthInner { + /// the actual bandwidth amount (in bytes) available + available: AtomicI64, + + /// defines the timestamp when the bandwidth information has been logged to the logs stream + last_logged_ts: AtomicI64, + + /// defines the timestamp when the bandwidth value was last updated + last_updated_ts: AtomicI64, +} + +impl ClientBandwidth { + pub(crate) fn new_empty() -> Self { + ClientBandwidth { + inner: Arc::new(ClientBandwidthInner { + available: AtomicI64::new(0), + last_logged_ts: AtomicI64::new(0), + last_updated_ts: AtomicI64::new(0), + }), + } + } + pub(crate) fn remaining(&self) -> i64 { + self.inner.available.load(Ordering::Acquire) + } + + pub(crate) fn maybe_log_bandwidth(&self, now: Option) { + let last = self.last_logged(); + let now = now.unwrap_or_else(OffsetDateTime::now_utc); + if last + Duration::from_secs(10) < now { + self.log_bandwidth(Some(now)) + } + } + + pub(crate) fn log_bandwidth(&self, now: Option) { + let now = now.unwrap_or_else(OffsetDateTime::now_utc); + + let remaining = self.remaining(); + let remaining_bi2 = bibytes2(remaining as f64); + + if remaining < 0 { + log::warn!("OUT OF BANDWIDTH. remaining: {remaining_bi2}"); + } else { + log::info!("remaining bandwidth: {remaining_bi2}"); + } + + self.inner + .last_logged_ts + .store(now.unix_timestamp(), Ordering::Relaxed) + } + + pub(crate) fn update_and_maybe_log(&self, remaining: i64) { + let now = OffsetDateTime::now_utc(); + self.inner.available.store(remaining, Ordering::Release); + self.inner + .last_updated_ts + .store(now.unix_timestamp(), Ordering::Relaxed); + self.maybe_log_bandwidth(Some(now)) + } + + pub(crate) fn update_and_log(&self, remaining: i64) { + let now = OffsetDateTime::now_utc(); + self.inner.available.store(remaining, Ordering::Release); + self.inner + .last_updated_ts + .store(now.unix_timestamp(), Ordering::Relaxed); + self.log_bandwidth(Some(now)) + } + + fn last_logged(&self) -> OffsetDateTime { + // SAFETY: this value is always populated with valid timestamps + OffsetDateTime::from_unix_timestamp(self.inner.last_logged_ts.load(Ordering::Relaxed)) + .unwrap() + } +} diff --git a/common/client-libs/gateway-client/src/client/config.rs b/common/client-libs/gateway-client/src/client/config.rs new file mode 100644 index 0000000000..fd7bfc142d --- /dev/null +++ b/common/client-libs/gateway-client/src/client/config.rs @@ -0,0 +1,135 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::GatewayClientError; +use nym_network_defaults::TicketTypeRepr::V1MixnetEntry; +use si_scale::helpers::bibytes2; +use std::time::Duration; + +#[derive(Debug, Default, Clone, Copy)] +pub struct GatewayClientConfig { + pub connection: Connection, + pub bandwidth: BandwidthTickets, +} + +impl GatewayClientConfig { + pub fn new_default() -> Self { + Default::default() + } + + #[must_use] + pub fn with_disabled_credentials_mode(mut self, disabled_credentials_mode: bool) -> Self { + self.bandwidth.require_tickets = !disabled_credentials_mode; + self + } + + #[must_use] + pub fn with_reconnection_on_failure(mut self, should_reconnect_on_failure: bool) -> Self { + self.connection.should_reconnect_on_failure = should_reconnect_on_failure; + self + } + + #[must_use] + pub fn with_response_timeout(mut self, response_timeout_duration: Duration) -> Self { + self.connection.response_timeout_duration = response_timeout_duration; + self + } + + #[must_use] + pub fn with_reconnection_attempts(mut self, reconnection_attempts: usize) -> Self { + self.connection.reconnection_attempts = reconnection_attempts; + self + } + + #[must_use] + pub fn with_reconnection_backoff(mut self, backoff: Duration) -> Self { + self.connection.reconnection_backoff = backoff; + self + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Connection { + /// Specifies the timeout for gateway responses + pub response_timeout_duration: Duration, + + /// Specifies whether client should try to reconnect to gateway on connection failure. + pub should_reconnect_on_failure: bool, + + /// Specifies maximum number of attempts client will try to reconnect to gateway on failure + /// before giving up. + pub reconnection_attempts: usize, + + /// Delay between each subsequent reconnection attempt. + pub reconnection_backoff: Duration, +} + +impl Connection { + // Set this to a high value for now, so that we don't risk sporadic timeouts that might cause + // bought bandwidth tokens to not have time to be spent; Once we remove the gateway from the + // bandwidth bridging protocol, we can come back to a smaller timeout value + pub const DEFAULT_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5 * 60); + pub const DEFAULT_RECONNECTION_ATTEMPTS: usize = 10; + pub const DEFAULT_RECONNECTION_BACKOFF: Duration = Duration::from_secs(5); +} + +impl Default for Connection { + fn default() -> Self { + Connection { + response_timeout_duration: Self::DEFAULT_RESPONSE_TIMEOUT, + should_reconnect_on_failure: true, + reconnection_attempts: Self::DEFAULT_RECONNECTION_ATTEMPTS, + reconnection_backoff: Self::DEFAULT_RECONNECTION_BACKOFF, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct BandwidthTickets { + /// specifies whether this client will be sending bandwidth tickets or will attempt to use 'free' testnet bandwidth instead + pub require_tickets: bool, + + /// specifies threshold (in bytes) under which the client should send another ticket to the gateway + pub remaining_bandwidth_threshold: i64, + + /// specifies threshold (in bytes) under which the client will NOT send any tickets because it got accused of double spending and got its bandwidth revoked + /// if not specified, the client will always send tickets + pub cutoff_remaining_bandwidth_threshold: Option, +} + +impl BandwidthTickets { + // TO BE CHANGED \/ + pub const DEFAULT_REQUIRES_TICKETS: bool = false; + + // 20% of entry ticket value + pub const DEFAULT_REMAINING_BANDWIDTH_THRESHOLD: i64 = + (V1MixnetEntry.bandwidth_value() / 5) as i64; + + pub const DEFAULT_CUTOFF_REMAINING_BANDWIDTH_THRESHOLD: Option = None; + + pub fn ensure_above_cutoff(&self, available: i64) -> Result<(), GatewayClientError> { + if let Some(cutoff) = self.cutoff_remaining_bandwidth_threshold { + if available < cutoff { + let available_bi2 = bibytes2(available as f64); + let cutoff_bi2 = bibytes2(cutoff as f64); + return Err(GatewayClientError::BandwidthBelowCutoffValue { + available_bi2, + cutoff_bi2, + }); + } + } + + Ok(()) + } +} + +impl Default for BandwidthTickets { + fn default() -> Self { + BandwidthTickets { + require_tickets: Self::DEFAULT_REQUIRES_TICKETS, + remaining_bandwidth_threshold: Self::DEFAULT_REMAINING_BANDWIDTH_THRESHOLD, + cutoff_remaining_bandwidth_threshold: + Self::DEFAULT_CUTOFF_REMAINING_BANDWIDTH_THRESHOLD, + } + } +} diff --git a/common/client-libs/gateway-client/src/client.rs b/common/client-libs/gateway-client/src/client/mod.rs similarity index 83% rename from common/client-libs/gateway-client/src/client.rs rename to common/client-libs/gateway-client/src/client/mod.rs index 91c320bb46..d899ec2928 100644 --- a/common/client-libs/gateway-client/src/client.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -1,12 +1,13 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 - +use crate::bandwidth::ClientBandwidth; +use crate::client::config::GatewayClientConfig; use crate::error::GatewayClientError; use crate::packet_router::PacketRouter; pub use crate::packet_router::{ AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender, }; -use crate::socket_state::{ws_fd, PartiallyDelegated, SocketState}; +use crate::socket_state::{ws_fd, PartiallyDelegatedHandle, SocketState}; use crate::traits::GatewayPacketRouter; use crate::{cleanup_socket_message, try_decrypt_binary_message}; use futures::{SinkExt, StreamExt}; @@ -23,14 +24,11 @@ use nym_gateway_requests::{ BinaryRequest, ClientControlRequest, ServerResponse, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION, }; -use nym_network_defaults::{REMAINING_BANDWIDTH_THRESHOLD, TOKENS_TO_BURN}; use nym_sphinx::forwarding::packet::MixPacket; use nym_task::TaskClient; use nym_validator_client::nyxd::contract_traits::DkgQueryClient; use rand::rngs::OsRng; - use std::sync::Arc; -use std::time::Duration; use tungstenite::protocol::Message; use url::Url; @@ -48,12 +46,7 @@ use wasm_utils::websocket::JSWebsocket; #[cfg(target_arch = "wasm32")] use wasmtimer::tokio::sleep; -// Set this to a high value for now, so that we don't risk sporadic timeouts that might cause -// bought bandwidth tokens to not have time to be spent; Once we remove the gateway from the -// bandwidth bridging protocol, we can come back to a smaller timeout value -const DEFAULT_GATEWAY_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5 * 60); -const DEFAULT_RECONNECTION_ATTEMPTS: usize = 10; -const DEFAULT_RECONNECTION_BACKOFF: Duration = Duration::from_secs(5); +pub mod config; pub struct GatewayConfig { pub gateway_identity: identity::PublicKey, @@ -79,29 +72,19 @@ impl GatewayConfig { } // TODO: this should be refactored into a state machine that keeps track of its authentication state -#[derive(Debug)] pub struct GatewayClient { + pub cfg: GatewayClientConfig, + authenticated: bool, - disabled_credentials_mode: bool, - bandwidth_remaining: i64, + bandwidth: ClientBandwidth, gateway_address: String, gateway_identity: identity::PublicKey, local_identity: Arc, shared_key: Option>, connection: SocketState, packet_router: PacketRouter, - response_timeout_duration: Duration, bandwidth_controller: Option>, - // reconnection related variables - /// Specifies whether client should try to reconnect to gateway on connection failure. - should_reconnect_on_failure: bool, - /// Specifies maximum number of attempts client will try to reconnect to gateway on failure - /// before giving up. - reconnection_attempts: usize, - /// Delay between each subsequent reconnection attempt. - reconnection_backoff: Duration, - // currently unused (but populated) negotiated_protocol: Option, @@ -111,7 +94,8 @@ pub struct GatewayClient { impl GatewayClient { pub fn new( - config: GatewayConfig, + cfg: GatewayClientConfig, + gateway_config: GatewayConfig, local_identity: Arc, // TODO: make it mandatory. if you don't want to pass it, use `new_init` shared_key: Option>, @@ -120,55 +104,21 @@ impl GatewayClient { task_client: TaskClient, ) -> Self { GatewayClient { + cfg, authenticated: false, - disabled_credentials_mode: true, - bandwidth_remaining: 0, - gateway_address: config.gateway_listener, - gateway_identity: config.gateway_identity, + bandwidth: ClientBandwidth::new_empty(), + gateway_address: gateway_config.gateway_listener, + gateway_identity: gateway_config.gateway_identity, local_identity, shared_key, connection: SocketState::NotConnected, packet_router, - response_timeout_duration: DEFAULT_GATEWAY_RESPONSE_TIMEOUT, bandwidth_controller, - should_reconnect_on_failure: true, - reconnection_attempts: DEFAULT_RECONNECTION_ATTEMPTS, - reconnection_backoff: DEFAULT_RECONNECTION_BACKOFF, negotiated_protocol: None, task_client, } } - #[must_use] - pub fn with_disabled_credentials_mode(mut self, disabled_credentials_mode: bool) -> Self { - self.disabled_credentials_mode = disabled_credentials_mode; - self - } - - #[must_use] - pub fn with_reconnection_on_failure(mut self, should_reconnect_on_failure: bool) -> Self { - self.should_reconnect_on_failure = should_reconnect_on_failure; - self - } - - #[must_use] - pub fn with_response_timeout(mut self, response_timeout_duration: Duration) -> Self { - self.response_timeout_duration = response_timeout_duration; - self - } - - #[must_use] - pub fn with_reconnection_attempts(mut self, reconnection_attempts: usize) -> Self { - self.reconnection_attempts = reconnection_attempts; - self - } - - #[must_use] - pub fn with_reconnection_backoff(mut self, backoff: Duration) -> Self { - self.reconnection_backoff = backoff; - self - } - pub fn gateway_identity(&self) -> identity::PublicKey { self.gateway_identity } @@ -182,7 +132,7 @@ impl GatewayClient { } pub fn remaining_bandwidth(&self) -> i64 { - self.bandwidth_remaining + self.bandwidth.remaining() } #[cfg(not(target_arch = "wasm32"))] @@ -258,18 +208,21 @@ impl GatewayClient { info!("Attempting gateway reconnection..."); self.authenticated = false; - for i in 1..self.reconnection_attempts { - info!("attempt {}...", i); + for i in 1..self.cfg.connection.reconnection_attempts { + info!("reconnection attempt {}...", i); if self.try_reconnect().await.is_ok() { info!("managed to reconnect!"); return Ok(()); } - sleep(self.reconnection_backoff).await; + sleep(self.cfg.connection.reconnection_backoff).await; } // final attempt (done separately to be able to return a proper error) - info!("attempt {}", self.reconnection_attempts); + info!( + "reconnection attempt {}", + self.cfg.connection.reconnection_attempts + ); match self.try_reconnect().await { Ok(_) => { info!("managed to reconnect!"); @@ -278,7 +231,7 @@ impl GatewayClient { Err(err) => { error!( "failed to reconnect after {} attempts", - self.reconnection_attempts + self.cfg.connection.reconnection_attempts ); Err(err) } @@ -294,7 +247,7 @@ impl GatewayClient { _ => return Err(GatewayClientError::ConnectionInInvalidState), }; - let timeout = sleep(self.response_timeout_duration); + let timeout = sleep(self.cfg.connection.response_timeout_duration); tokio::pin!(timeout); loop { @@ -463,7 +416,9 @@ impl GatewayClient { ws_stream, self.local_identity.as_ref(), self.gateway_identity, - !self.disabled_credentials_mode, + self.cfg.bandwidth.require_tickets, + #[cfg(not(target_arch = "wasm32"))] + self.task_client.clone(), ) .await .map_err(GatewayClientError::RegistrationFailure), @@ -525,7 +480,7 @@ impl GatewayClient { self_address, encrypted_address, iv, - !self.disabled_credentials_mode, + self.cfg.bandwidth.require_tickets, ) .into(); @@ -537,7 +492,8 @@ impl GatewayClient { } => { self.check_gateway_protocol(protocol_version)?; self.authenticated = status; - self.bandwidth_remaining = bandwidth_remaining; + self.bandwidth.update_and_maybe_log(bandwidth_remaining); + self.negotiated_protocol = protocol_version; log::debug!("authenticated: {status}, bandwidth remaining: {bandwidth_remaining}"); self.task_client.send_status_msg(Box::new( @@ -576,56 +532,74 @@ impl GatewayClient { } } - async fn claim_coconut_bandwidth( + async fn claim_ecash_bandwidth( &mut self, credential: CredentialSpendingData, ) -> Result<(), GatewayClientError> { let mut rng = OsRng; let iv = IV::new_random(&mut rng); - let msg = ClientControlRequest::new_enc_coconut_bandwidth_credential_v2( + let msg = ClientControlRequest::new_enc_ecash_credential( credential, self.shared_key.as_ref().unwrap(), iv, ) .into(); - self.bandwidth_remaining = match self.send_websocket_message(msg).await? { + let bandwidth_remaining = match self.send_websocket_message(msg).await? { ServerResponse::Bandwidth { available_total } => Ok(available_total), ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), + ServerResponse::TypedError { error } => { + Err(GatewayClientError::TypedGatewayError(error)) + } _ => Err(GatewayClientError::UnexpectedResponse), }?; + + // TODO: create tracing span + info!("managed to claim ecash bandwidth"); + self.bandwidth.update_and_log(bandwidth_remaining); + Ok(()) } async fn try_claim_testnet_bandwidth(&mut self) -> Result<(), GatewayClientError> { let msg = ClientControlRequest::ClaimFreeTestnetBandwidth.into(); - self.bandwidth_remaining = match self.send_websocket_message(msg).await? { + let bandwidth_remaining = match self.send_websocket_message(msg).await? { ServerResponse::Bandwidth { available_total } => Ok(available_total), ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), _ => Err(GatewayClientError::UnexpectedResponse), }?; + info!("managed to claim testnet bandwidth"); + self.bandwidth.update_and_log(bandwidth_remaining); + Ok(()) } + fn unchecked_bandwidth_controller(&self) -> &BandwidthController { + self.bandwidth_controller.as_ref().unwrap() + } + pub async fn claim_bandwidth(&mut self) -> Result<(), GatewayClientError> where C: DkgQueryClient + Send + Sync, St: CredentialStorage, ::StorageError: Send + Sync + 'static, { + // TODO: make it configurable + const TICKETS_TO_SPEND: u32 = 1; + if !self.authenticated { return Err(GatewayClientError::NotAuthenticated); } if self.shared_key.is_none() { return Err(GatewayClientError::NoSharedKeyAvailable); } - if self.bandwidth_controller.is_none() && !self.disabled_credentials_mode { + if self.bandwidth_controller.is_none() && self.cfg.bandwidth.require_tickets { return Err(GatewayClientError::NoBandwidthControllerAvailable); } warn!("Not enough bandwidth. Trying to get more bandwidth, this might take a while"); - if self.disabled_credentials_mode { + if !self.cfg.bandwidth.require_tickets { info!("The client is running in disabled credentials mode - attempting to claim bandwidth without a credential"); return self.try_claim_testnet_bandwidth().await; } @@ -641,49 +615,52 @@ impl GatewayClient { negotiated_protocol: Some(gateway_protocol), }); } - - let gateway_id = self.gateway_identity().to_base58_string(); - let prepared_credential = self - .bandwidth_controller - .as_ref() - .unwrap() - .prepare_bandwidth_credential(&gateway_id) + .unchecked_bandwidth_controller() + .prepare_ecash_ticket(self.gateway_identity.to_bytes(), TICKETS_TO_SPEND) .await?; - self.claim_coconut_bandwidth(prepared_credential.data) - .await?; - self.bandwidth_controller - .as_ref() - .unwrap() - .consume_credential(prepared_credential.credential_id, &gateway_id) - .await?; + match self.claim_ecash_bandwidth(prepared_credential.data).await { + Ok(_) => Ok(()), + Err(err) => { + error!("failed to claim ecash bandwidth with the gateway...: {err}"); + if err.is_ticket_replay() { + warn!("this was due to our ticket being replayed! have you messed with the database file?") + } else { + // TODO: tracing span + info!("attempting to revert ticket withdrawal..."); + self.unchecked_bandwidth_controller() + .attempt_revert_ticket_usage(prepared_credential.metadata) + .await?; + } - Ok(()) - } - - fn estimate_required_bandwidth(&self, packets: &[MixPacket]) -> i64 { - packets - .iter() - .map(|packet| packet.packet().len()) - .sum::() as i64 + Err(err) + } + } } pub async fn batch_send_mix_packets( &mut self, packets: Vec, - ) -> Result<(), GatewayClientError> { + ) -> Result<(), GatewayClientError> + where + C: DkgQueryClient + Send + Sync, + St: CredentialStorage, + ::StorageError: Send + Sync + 'static, + { debug!("Sending {} mix packets", packets.len()); if !self.authenticated { return Err(GatewayClientError::NotAuthenticated); } - if self.estimate_required_bandwidth(&packets) > self.bandwidth_remaining { - return Err(GatewayClientError::NotEnoughBandwidth( - self.estimate_required_bandwidth(&packets), - self.bandwidth_remaining, - )); + let bandwidth_remaining = self.bandwidth.remaining(); + if bandwidth_remaining < self.cfg.bandwidth.remaining_bandwidth_threshold { + self.cfg + .bandwidth + .ensure_above_cutoff(bandwidth_remaining)?; + self.claim_bandwidth().await?; } + if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } @@ -703,7 +680,7 @@ impl GatewayClient { .batch_send_websocket_messages_without_response(messages) .await { - if err.is_closed_connection() && self.should_reconnect_on_failure { + if err.is_closed_connection() && self.cfg.connection.should_reconnect_on_failure { self.attempt_reconnection().await } else { Err(err) @@ -718,7 +695,7 @@ impl GatewayClient { msg: Message, ) -> Result<(), GatewayClientError> { if let Err(err) = self.send_websocket_message_without_response(msg).await { - if err.is_closed_connection() && self.should_reconnect_on_failure { + if err.is_closed_connection() && self.cfg.connection.should_reconnect_on_failure { debug!("Going to attempt a reconnection"); self.attempt_reconnection().await } else { @@ -742,19 +719,23 @@ impl GatewayClient { } // TODO: possibly make responses optional - pub async fn send_mix_packet( - &mut self, - mix_packet: MixPacket, - ) -> Result<(), GatewayClientError> { + pub async fn send_mix_packet(&mut self, mix_packet: MixPacket) -> Result<(), GatewayClientError> + where + C: DkgQueryClient + Send + Sync, + St: CredentialStorage, + ::StorageError: Send + Sync + 'static, + { if !self.authenticated { return Err(GatewayClientError::NotAuthenticated); } - if (mix_packet.packet().len() as i64) > self.bandwidth_remaining { - return Err(GatewayClientError::NotEnoughBandwidth( - mix_packet.packet().len() as i64, - self.bandwidth_remaining, - )); + let bandwidth_remaining = self.bandwidth.remaining(); + if bandwidth_remaining < self.cfg.bandwidth.remaining_bandwidth_threshold { + self.cfg + .bandwidth + .ensure_above_cutoff(bandwidth_remaining)?; + self.claim_bandwidth().await?; } + if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } @@ -800,7 +781,7 @@ impl GatewayClient { let partially_delegated = match std::mem::replace(&mut self.connection, SocketState::Invalid) { SocketState::Available(conn) => { - PartiallyDelegated::split_and_listen_for_mixnet_messages( + PartiallyDelegatedHandle::split_and_listen_for_mixnet_messages( *conn, self.packet_router.clone(), Arc::clone( @@ -808,6 +789,7 @@ impl GatewayClient { .as_ref() .expect("no shared key present even though we're authenticated!"), ), + self.bandwidth.clone(), self.task_client.clone(), ) } @@ -848,10 +830,12 @@ impl GatewayClient { self.establish_connection().await?; } let shared_key = self.perform_initial_authentication().await?; - - if self.bandwidth_remaining < REMAINING_BANDWIDTH_THRESHOLD { - info!("Claiming more bandwidth for your tokens. This will use {} token(s) from your wallet. \ - Stop the process now if you don't want that to happen.", TOKENS_TO_BURN); + let bandwidth_remaining = self.bandwidth.remaining(); + if bandwidth_remaining < self.cfg.bandwidth.remaining_bandwidth_threshold { + self.cfg + .bandwidth + .ensure_above_cutoff(bandwidth_remaining)?; + info!("Claiming more bandwidth with existing credentials. Stop the process now if you don't want that to happen."); self.claim_bandwidth().await?; } @@ -886,20 +870,16 @@ impl GatewayClient { let packet_router = PacketRouter::new(ack_tx, mix_tx, task_client.clone()); GatewayClient { + cfg: GatewayClientConfig::default().with_disabled_credentials_mode(true), authenticated: false, - disabled_credentials_mode: true, - bandwidth_remaining: 0, + bandwidth: ClientBandwidth::new_empty(), gateway_address: gateway_listener.to_string(), gateway_identity, local_identity, shared_key: None, connection: SocketState::NotConnected, packet_router, - response_timeout_duration: DEFAULT_GATEWAY_RESPONSE_TIMEOUT, bandwidth_controller: None, - should_reconnect_on_failure: false, - reconnection_attempts: DEFAULT_RECONNECTION_ATTEMPTS, - reconnection_backoff: DEFAULT_RECONNECTION_BACKOFF, negotiated_protocol: None, task_client, } @@ -918,20 +898,16 @@ impl GatewayClient { assert!(self.shared_key.is_some()); GatewayClient { + cfg: self.cfg, authenticated: self.authenticated, - disabled_credentials_mode: self.disabled_credentials_mode, - bandwidth_remaining: self.bandwidth_remaining, + bandwidth: self.bandwidth, gateway_address: self.gateway_address, gateway_identity: self.gateway_identity, local_identity: self.local_identity, shared_key: self.shared_key, connection: self.connection, packet_router, - response_timeout_duration: self.response_timeout_duration, bandwidth_controller, - should_reconnect_on_failure: self.should_reconnect_on_failure, - reconnection_attempts: self.reconnection_attempts, - reconnection_backoff: self.reconnection_backoff, negotiated_protocol: self.negotiated_protocol, task_client, } diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs index f3fc089498..0e7757a46c 100644 --- a/common/client-libs/gateway-client/src/error.rs +++ b/common/client-libs/gateway-client/src/error.rs @@ -1,21 +1,26 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -#[cfg(target_arch = "wasm32")] -use gloo_utils::errors::JsError; use nym_gateway_requests::registration::handshake::error::HandshakeError; +use nym_gateway_requests::SimpleGatewayRequestsError; use std::io; use thiserror::Error; use tungstenite::Error as WsError; +#[cfg(target_arch = "wasm32")] +use gloo_utils::errors::JsError; + #[derive(Debug, Error)] pub enum GatewayClientError { #[error("Connection to the gateway is not established")] ConnectionNotEstablished, - #[error("Gateway returned an error response: {0}")] + #[error("gateway returned an error response: {0}")] GatewayError(String), + #[error("gateway returned an error response: {0}")] + TypedGatewayError(SimpleGatewayRequestsError), + #[error("There was a network error: {0}")] NetworkError(#[from] WsError), @@ -62,6 +67,12 @@ pub enum GatewayClientError { #[error("There are no more bandwidth credentials acquired. Please buy some more if you want to use the mixnet")] NoMoreBandwidthCredentials, + #[error("the current available bandwidth ({available_bi2}) is below the minimum cutoff threshold off {cutoff_bi2}")] + BandwidthBelowCutoffValue { + available_bi2: String, + cutoff_bi2: String, + }, + #[error("Received an unexpected response")] UnexpectedResponse, @@ -113,4 +124,11 @@ impl GatewayClientError { _ => false, } } + + pub fn is_ticket_replay(&self) -> bool { + match self { + GatewayClientError::TypedGatewayError(err) => err.is_ticket_replay(), + _ => false, + } + } } diff --git a/common/client-libs/gateway-client/src/lib.rs b/common/client-libs/gateway-client/src/lib.rs index bc1b22b887..e37cb3e4a4 100644 --- a/common/client-libs/gateway-client/src/lib.rs +++ b/common/client-libs/gateway-client/src/lib.rs @@ -6,7 +6,7 @@ use log::warn; use nym_gateway_requests::BinaryResponse; use tungstenite::{protocol::Message, Error as WsError}; -pub use client::{GatewayClient, GatewayConfig}; +pub use client::{config::GatewayClientConfig, GatewayClient, GatewayConfig}; pub use nym_gateway_requests::registration::handshake::SharedKeys; pub use packet_router::{ AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender, @@ -14,6 +14,7 @@ pub use packet_router::{ }; pub use traits::GatewayPacketRouter; +mod bandwidth; pub mod client; pub mod error; pub mod packet_router; @@ -51,10 +52,7 @@ pub(crate) fn try_decrypt_binary_message( BinaryResponse::PushedMixMessage(plaintext) => Some(plaintext), }, Err(err) => { - warn!( - "message received from the gateway was malformed! - {:?}", - 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 386ad5b147..9b9982f852 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -1,6 +1,7 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::bandwidth::ClientBandwidth; use crate::error::GatewayClientError; use crate::packet_router::PacketRouter; use crate::traits::GatewayPacketRouter; @@ -10,16 +11,13 @@ use futures::stream::{SplitSink, SplitStream}; use futures::{SinkExt, StreamExt}; use log::*; use nym_gateway_requests::registration::handshake::SharedKeys; -use nym_gateway_requests::ServerResponse; +use nym_gateway_requests::{ServerResponse, SimpleGatewayRequestsError}; use nym_task::TaskClient; -use si_scale::helpers::bibytes2; use std::os::raw::c_int as RawFd; -use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::Arc; -use std::time::Duration; -use time::OffsetDateTime; -use tungstenite::Message; +use tungstenite::{protocol::Message, Error as WsError}; +use si_scale::helpers::bibytes2; #[cfg(unix)] use std::os::fd::AsRawFd; #[cfg(not(target_arch = "wasm32"))] @@ -42,6 +40,7 @@ type WsConn = JSWebsocket; // by some other task, however, we can notify it to get the stream back. type SplitStreamReceiver = oneshot::Receiver, GatewayClientError>>; +type SplitStreamSender = oneshot::Sender, GatewayClientError>>; pub(crate) fn ws_fd(_conn: &WsConn) -> Option { #[cfg(unix)] @@ -53,92 +52,204 @@ pub(crate) fn ws_fd(_conn: &WsConn) -> Option { None } -// disgusting? absolutely, but does the trick for now -static LAST_LOGGED_BANDWIDTH_TS: AtomicI64 = AtomicI64::new(0); - -fn maybe_log_bandwidth(remaining: i64) { - // SAFETY: this value is always populated with valid timestamps - let last = - OffsetDateTime::from_unix_timestamp(LAST_LOGGED_BANDWIDTH_TS.load(Ordering::Relaxed)) - .unwrap(); - let now = OffsetDateTime::now_utc(); - if last + Duration::from_secs(10) < now { - log::info!("remaining bandwidth: {}", bibytes2(remaining as f64)); - LAST_LOGGED_BANDWIDTH_TS.store(now.unix_timestamp(), Ordering::Relaxed) - } -} - #[derive(Debug)] -pub(crate) struct PartiallyDelegated { +pub(crate) struct PartiallyDelegatedHandle { sink_half: SplitSink, + // this could have been simplified by a notify as opposed to oneshot, but let's not change what ain't broke delegated_stream: (SplitStreamReceiver, oneshot::Sender<()>), ws_fd: Option, } -impl PartiallyDelegated { - fn recover_received_plaintexts( - ws_msgs: Vec, - shared_key: &SharedKeys, - ) -> Result>, GatewayClientError> { - let mut plaintexts = Vec::with_capacity(ws_msgs.len()); - for ws_msg in ws_msgs { - match ws_msg { - Message::Binary(bin_msg) => { - // this function decrypts the request and checks the MAC - if let Some(plaintext) = try_decrypt_binary_message(bin_msg, shared_key) { - plaintexts.push(plaintext) +struct PartiallyDelegatedRouter { + packet_router: PacketRouter, + shared_key: Arc, + client_bandwidth: ClientBandwidth, + + stream_return: SplitStreamSender, + stream_return_requester: oneshot::Receiver<()>, +} + +impl PartiallyDelegatedRouter { + fn new( + packet_router: PacketRouter, + shared_key: Arc, + client_bandwidth: ClientBandwidth, + stream_return: SplitStreamSender, + stream_return_requester: oneshot::Receiver<()>, + ) -> PartiallyDelegatedRouter { + PartiallyDelegatedRouter { + packet_router, + shared_key, + client_bandwidth, + stream_return, + stream_return_requester, + } + } + + async fn run(mut self, mut split_stream: SplitStream, mut task_client: TaskClient) { + let mut chunked_stream = (&mut split_stream).ready_chunks(8); + let ret: Result<_, GatewayClientError> = loop { + tokio::select! { + biased; + // received system-wide shutdown + _ = task_client.recv() => { + log::trace!("GatewayClient listener: Received shutdown"); + log::debug!("GatewayClient listener: Exiting"); + return; + } + // received request to stop the task and return the stream + _ = &mut self.stream_return_requester => { + log::debug!("received request to return the split ws stream"); + break Ok(()) + } + socket_msgs = chunked_stream.next() => { + if let Err(err) = self.handle_socket_messages(socket_msgs) { + break Err(err) } } - // 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 - // not ordered (for some peculiar reason) we wouldn't lose anything. - // This would also require NOT discarding any text responses here. + } + }; - // TODO: those can return the "send confirmations" - perhaps it should be somehow worked around? - Message::Text(text) => { - trace!( + let return_res = match ret { + Err(err) => self.stream_return.send(Err(err)), + Ok(_) => { + self.packet_router.mark_as_success(); + task_client.mark_as_success(); + self.stream_return.send(Ok(split_stream)) + } + }; + + if return_res.is_err() { + warn!("failed to return the split stream back on the oneshot channel") + } + } + + fn handle_socket_messages( + &self, + msgs: Option>>, + ) -> Result<(), GatewayClientError> { + let ws_msgs = cleanup_socket_messages(msgs)?; + let plaintexts = self.recover_received_plaintexts(ws_msgs)?; + if !plaintexts.is_empty() { + self.packet_router.route_received(plaintexts)? + } + + Ok(()) + } + + fn handle_binary_message(&self, binary_msg: Vec) -> Result, GatewayClientError> { + // this function decrypts the request and checks the MAC + match try_decrypt_binary_message(binary_msg, &self.shared_key) { + Some(plaintext) => Ok(plaintext), + None => { + error!("failed to decrypt and verify received message!"); + Err(GatewayClientError::MalformedResponse) + } + } + } + + // only returns an error on **critical** failures + fn handle_text_message(&self, text: String) -> Result<(), GatewayClientError> { + // if we fail to deserialise the response, return a hard error. we can't handle garbage + match ServerResponse::try_from(text).map_err(|_| GatewayClientError::MalformedResponse)? { + ServerResponse::Send { + remaining_bandwidth, + } => { + self.client_bandwidth + .update_and_maybe_log(remaining_bandwidth); + Ok(()) + } + ServerResponse::Error { message } => { + error!("[1] gateway failure: {message}"); + Err(GatewayClientError::GatewayError(message)) + } + ServerResponse::TypedError { error } => { + match error { + SimpleGatewayRequestsError::OutOfBandwidth { + required, + available, + } => { + let available_bi2 = bibytes2(available as f64); + let required_bi2 = bibytes2(required as f64); + warn!("run out of bandwidth when attempting to send the message! we got {available_bi2} available, but needed at least {required_bi2} to send the previous message"); + self.client_bandwidth.update_and_log(available); + // UNIMPLEMENTED: we should stop sending messages until we recover bandwidth + Ok(()) + } + _ => { + error!("[2] gateway failure: {error}"); + Err(GatewayClientError::TypedGatewayError(error)) + } + } + } + other => { + let name = other.name(); + warn!("received illegal message of type '{name}' in an authenticated client"); + Ok(()) + } + } + } + + fn recover_received_plaintext( + &self, + message: Message, + ) -> Result>, GatewayClientError> { + match message { + Message::Binary(bin_msg) => { + let plaintext = self.handle_binary_message(bin_msg)?; + Ok(Some(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 + // not ordered (for some peculiar reason) we wouldn't lose anything. + // This would also require NOT discarding any text responses here. + + // TODO: those can return the "send confirmations" - perhaps it should be somehow worked around? + Message::Text(text) => { + trace!( "received a text message - probably a response to some previous query! - {text}", ); - match ServerResponse::try_from(text) - .map_err(|_| GatewayClientError::MalformedResponse)? - { - ServerResponse::Send { - remaining_bandwidth, - } => maybe_log_bandwidth(remaining_bandwidth), - ServerResponse::Error { message } => { - error!("gateway failure: {message}"); - return Err(GatewayClientError::GatewayError(message)); - } - other => { - warn!( - "received illegal message of type {} in an authenticated client", - other.name() - ) - } - } + self.handle_text_message(text)?; + Ok(None) + } + _ => { + debug!("received websocket message that's neither 'Binary' nor 'Text'. it's going to get ignored"); + Ok(None) + } + } + } - continue; - } - _ => continue, + fn recover_received_plaintexts( + &self, + messages: Vec, + ) -> Result>, GatewayClientError> { + let mut plaintexts = Vec::new(); + for ws_msg in messages { + if let Some(plaintext) = self.recover_received_plaintext(ws_msg)? { + plaintexts.push(plaintext) } } Ok(plaintexts) } - fn route_socket_messages( - ws_msgs: Vec, - packet_router: &PacketRouter, - shared_key: &SharedKeys, - ) -> Result<(), GatewayClientError> { - let plaintexts = Self::recover_received_plaintexts(ws_msgs, shared_key)?; - packet_router.route_received(plaintexts) - } + fn spawn(self, split_stream: SplitStream, task_client: TaskClient) { + let fut = async move { self.run(split_stream, task_client).await }; + #[cfg(target_arch = "wasm32")] + wasm_bindgen_futures::spawn_local(fut); + + #[cfg(not(target_arch = "wasm32"))] + tokio::spawn(fut); + } +} + +impl PartiallyDelegatedHandle { pub(crate) fn split_and_listen_for_mixnet_messages( conn: WsConn, - mut packet_router: PacketRouter, + packet_router: PacketRouter, shared_key: Arc, - mut shutdown: TaskClient, + client_bandwidth: ClientBandwidth, + shutdown: TaskClient, ) -> Self { // when called for, it NEEDS TO yield back the stream so that we could merge it and // read control request responses. @@ -146,58 +257,18 @@ impl PartiallyDelegated { let (stream_sender, stream_receiver) = oneshot::channel(); let ws_fd = ws_fd(&conn); + let (sink, stream) = conn.split(); - let (sink, mut stream) = conn.split(); + PartiallyDelegatedRouter::new( + packet_router, + shared_key, + client_bandwidth, + stream_sender, + notify_receiver, + ) + .spawn(stream, shutdown); - let mixnet_receiver_future = async move { - let mut notify_receiver = notify_receiver; - let mut chunk_stream = (&mut stream).ready_chunks(8); - - let ret_err = loop { - tokio::select! { - _ = shutdown.recv() => { - log::trace!("GatewayClient listener: Received shutdown"); - log::debug!("GatewayClient listener: Exiting"); - return; - } - _ = &mut notify_receiver => { - break Ok(()); - } - msgs = chunk_stream.next() => { - let ws_msgs = match cleanup_socket_messages(msgs) { - Err(err) => break Err(err), - Ok(msgs) => msgs - }; - - if let Err(err) = Self::route_socket_messages(ws_msgs, &packet_router, shared_key.as_ref()) { - log::error!("Route socket messages failed: {err}"); - break Err(err) - } - } - }; - }; - - if match ret_err { - Err(err) => stream_sender.send(Err(err)), - Ok(_) => { - packet_router.mark_as_success(); - shutdown.mark_as_success(); - stream_sender.send(Ok(stream)) - } - } - .is_err() - { - warn!("failed to send back `mixnet_receiver_future` result on the oneshot channel") - } - }; - - #[cfg(target_arch = "wasm32")] - wasm_bindgen_futures::spawn_local(mixnet_receiver_future); - - #[cfg(not(target_arch = "wasm32"))] - tokio::spawn(mixnet_receiver_future); - - PartiallyDelegated { + PartiallyDelegatedHandle { ws_fd, sink_half: sink, delegated_stream: (stream_receiver, notify_sender), @@ -266,7 +337,7 @@ impl PartiallyDelegated { #[derive(Debug)] pub(crate) enum SocketState { Available(Box), - PartiallyDelegated(PartiallyDelegated), + PartiallyDelegated(PartiallyDelegatedHandle), NotConnected, Invalid, } diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index 32f371eb23..a9a348a28e 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -17,6 +17,7 @@ nym-contracts-common = { path = "../../cosmwasm-smart-contracts/contracts-common nym-mixnet-contract-common = { path = "../../cosmwasm-smart-contracts/mixnet-contract" } nym-vesting-contract-common = { path = "../../cosmwasm-smart-contracts/vesting-contract" } nym-coconut-bandwidth-contract-common = { path = "../../cosmwasm-smart-contracts/coconut-bandwidth-contract" } +nym-ecash-contract-common = { path = "../../cosmwasm-smart-contracts/ecash-contract" } nym-multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" } nym-group-contract-common = { path = "../../cosmwasm-smart-contracts/group-contract" } serde = { workspace = true, features = ["derive"] } @@ -26,9 +27,10 @@ thiserror = { workspace = true } log = { workspace = true } url = { workspace = true, features = ["serde"] } tokio = { workspace = true, features = ["sync", "time"] } +time = { workspace = true, features = ["formatting"] } futures = { workspace = true } -nym-coconut = { path = "../../nymcoconut" } +nym-compact-ecash = { path = "../../nym_offline_compact_ecash" } nym-network-defaults = { path = "../../network-defaults" } nym-api-requests = { path = "../../../nym-api/nym-api-requests" } diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index b7b1e62c23..32cfd9ae2f 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -8,10 +8,14 @@ use crate::{ nym_api, DirectSigningReqwestRpcValidatorClient, QueryReqwestRpcValidatorClient, ReqwestRpcClient, ValidatorClientError, }; -use nym_api_requests::coconut::models::FreePassNonceResponse; -use nym_api_requests::coconut::{ - BlindSignRequestBody, BlindedSignatureResponse, FreePassRequest, VerifyCredentialBody, - VerifyCredentialResponse, +use nym_api_requests::ecash::models::{ + AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, + BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationResponse, + SpentCredentialsResponse, VerifyEcashTicketBody, +}; +use nym_api_requests::ecash::{ + BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse, + PartialExpirationDateSignatureResponse, VerificationKeyResponse, }; use nym_api_requests::models::{DescribedGateway, MixNodeBondAnnotated}; use nym_api_requests::models::{ @@ -19,8 +23,10 @@ use nym_api_requests::models::{ RewardEstimationResponse, StakeSaturationResponse, }; use nym_api_requests::nym_nodes::SkimmedNode; +use nym_coconut_dkg_common::types::EpochId; use nym_http_api_client::UserAgent; use nym_network_defaults::NymNetworkDetails; +use time::Date; use url::Url; pub use crate::nym_api::NymApiClientExt; @@ -29,7 +35,7 @@ pub use nym_mixnet_contract_common::{ }; // re-export the type to not break existing imports -pub use crate::coconut::CoconutApiClient; +pub use crate::coconut::EcashApiClient; #[cfg(feature = "http-client")] use crate::rpc::http_client; @@ -375,24 +381,73 @@ impl NymApiClient { Ok(self.nym_api.blind_sign(request_body).await?) } - pub async fn verify_bandwidth_credential( + pub async fn verify_ecash_ticket( &self, - request_body: &VerifyCredentialBody, - ) -> Result { + request_body: &VerifyEcashTicketBody, + ) -> Result { + Ok(self.nym_api.verify_ecash_ticket(request_body).await?) + } + + pub async fn batch_redeem_ecash_tickets( + &self, + request_body: &BatchRedeemTicketsBody, + ) -> Result { Ok(self .nym_api - .verify_bandwidth_credential(request_body) + .batch_redeem_ecash_tickets(request_body) .await?) } - pub async fn free_pass_nonce(&self) -> Result { - Ok(self.nym_api.free_pass_nonce().await?) + pub async fn spent_credentials_filter( + &self, + ) -> Result { + Ok(self.nym_api.double_spending_filter_v1().await?) } - pub async fn issue_free_pass_credential( + pub async fn partial_expiration_date_signatures( &self, - request: &FreePassRequest, - ) -> Result { - Ok(self.nym_api.free_pass(request).await?) + expiration_date: Option, + ) -> Result { + Ok(self + .nym_api + .partial_expiration_date_signatures(expiration_date) + .await?) + } + + pub async fn partial_coin_indices_signatures( + &self, + epoch_id: Option, + ) -> Result { + Ok(self + .nym_api + .partial_coin_indices_signatures(epoch_id) + .await?) + } + + pub async fn global_expiration_date_signatures( + &self, + expiration_date: Option, + ) -> Result { + Ok(self + .nym_api + .global_expiration_date_signatures(expiration_date) + .await?) + } + + pub async fn global_coin_indices_signatures( + &self, + epoch_id: Option, + ) -> Result { + Ok(self + .nym_api + .global_coin_indices_signatures(epoch_id) + .await?) + } + + pub async fn master_verification_key( + &self, + epoch_id: Option, + ) -> Result { + Ok(self.nym_api.master_verification_key(epoch_id).await?) } } diff --git a/common/client-libs/validator-client/src/coconut/mod.rs b/common/client-libs/validator-client/src/coconut/mod.rs index 66f5d0e05a..09ce560b1c 100644 --- a/common/client-libs/validator-client/src/coconut/mod.rs +++ b/common/client-libs/validator-client/src/coconut/mod.rs @@ -4,26 +4,40 @@ use crate::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient}; use crate::nyxd::error::NyxdError; use crate::NymApiClient; -use nym_coconut::{Base58, CoconutError, VerificationKey}; use nym_coconut_dkg_common::types::{EpochId, NodeIndex}; use nym_coconut_dkg_common::verification_key::ContractVKShare; +use nym_compact_ecash::error::CompactEcashError; +use nym_compact_ecash::{Base58, VerificationKeyAuth}; +use std::fmt::{Display, Formatter}; use thiserror::Error; use url::Url; // TODO: it really doesn't feel like this should live in this crate. #[derive(Clone)] -pub struct CoconutApiClient { +pub struct EcashApiClient { pub api_client: NymApiClient, - pub verification_key: VerificationKey, + pub verification_key: VerificationKeyAuth, pub node_id: NodeIndex, pub cosmos_address: cosmrs::AccountId, } +impl Display for EcashApiClient { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "[id: {}] {} @ {}", + self.node_id, + self.cosmos_address, + self.api_client.api_url() + ) + } +} + // TODO: this should be using the coconut error // (which is in different crate; perhaps this client should be moved there?) #[derive(Debug, Error)] -pub enum CoconutApiError { +pub enum EcashApiError { // TODO: ask @BN whether this is a correct error message #[error("the provided key share hasn't been verified")] UnverifiedShare, @@ -43,7 +57,7 @@ pub enum CoconutApiError { #[error("the provided verification key is malformed: {source}")] MalformedVerificationKey { #[from] - source: CoconutError, + source: CompactEcashError, }, #[error("the provided account address is malformed: {source}")] @@ -53,29 +67,29 @@ pub enum CoconutApiError { }, } -impl TryFrom for CoconutApiClient { - type Error = CoconutApiError; +impl TryFrom for EcashApiClient { + type Error = EcashApiError; fn try_from(share: ContractVKShare) -> Result { if !share.verified { - return Err(CoconutApiError::UnverifiedShare); + return Err(EcashApiError::UnverifiedShare); } let url_address = Url::parse(&share.announce_address)?; - Ok(CoconutApiClient { + Ok(EcashApiClient { api_client: NymApiClient::new(url_address), - verification_key: VerificationKey::try_from_bs58(&share.share)?, + verification_key: VerificationKeyAuth::try_from_bs58(&share.share)?, node_id: share.node_index, cosmos_address: share.owner.as_str().parse()?, }) } } -pub async fn all_coconut_api_clients( +pub async fn all_ecash_api_clients( client: &C, epoch_id: EpochId, -) -> Result, CoconutApiError> +) -> Result, EcashApiError> where C: DkgQueryClient + Sync + Send, { diff --git a/common/client-libs/validator-client/src/error.rs b/common/client-libs/validator-client/src/error.rs index d439a86848..11bdb3d745 100644 --- a/common/client-libs/validator-client/src/error.rs +++ b/common/client-libs/validator-client/src/error.rs @@ -7,7 +7,7 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum ValidatorClientError { - #[error("nym api request failed - {source}")] + #[error("nym api request failed: {source}")] NymAPIError { #[from] source: nym_api::error::NymAPIError, @@ -19,7 +19,7 @@ pub enum ValidatorClientError { #[error("One of the provided URLs was malformed - {0}")] MalformedUrlProvided(#[from] url::ParseError), - #[error("nyxd request failed - {0}")] + #[error("nyxd request failed: {0}")] NyxdError(#[from] crate::nyxd::error::NyxdError), #[error("No validator API url has been provided")] diff --git a/common/client-libs/validator-client/src/lib.rs b/common/client-libs/validator-client/src/lib.rs index 5af96ffe23..725b86450b 100644 --- a/common/client-libs/validator-client/src/lib.rs +++ b/common/client-libs/validator-client/src/lib.rs @@ -15,7 +15,7 @@ pub use crate::error::ValidatorClientError; pub use crate::rpc::reqwest::ReqwestRpcClient; pub use crate::signing::direct_wallet::DirectSecp256k1HdWallet; pub use client::NymApiClient; -pub use client::{Client, CoconutApiClient, Config}; +pub use client::{Client, Config, EcashApiClient}; pub use nym_api_requests::*; pub use nym_http_api_client::UserAgent; diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index 8c2f46d1d1..fa0bb2b7a9 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -2,16 +2,33 @@ // SPDX-License-Identifier: Apache-2.0 use crate::nym_api::error::NymAPIError; -use crate::nym_api::routes::{CORE_STATUS_COUNT, SINCE_ARG}; +use crate::nym_api::routes::{ecash, CORE_STATUS_COUNT, SINCE_ARG}; use async_trait::async_trait; +use nym_api_requests::ecash::models::{ + AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, + BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationResponse, + VerifyEcashTicketBody, +}; +use nym_api_requests::nym_nodes::{CachedNodesResponse, SkimmedNode}; +use nym_http_api_client::{ApiClient, NO_PARAMS}; +use nym_mixnet_contract_common::mixnode::MixNodeDetails; +use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId}; +use time::format_description::BorrowedFormatItem; +use time::Date; + +pub mod error; +pub mod routes; + +use nym_api_requests::ecash::VerificationKeyResponse; pub use nym_api_requests::{ - coconut::{ + ecash::{ models::{ - EpochCredentialsResponse, IssuedCredential, IssuedCredentialBody, - IssuedCredentialResponse, IssuedCredentialsResponse, + EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse, + IssuedTicketbook, IssuedTicketbookBody, SpentCredentialsResponse, }, BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody, - VerifyCredentialBody, VerifyCredentialResponse, + PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse, + VerifyEcashCredentialBody, }, models::{ ComputeRewardEstParam, DescribedGateway, GatewayBondAnnotated, GatewayCoreStatusResponse, @@ -22,18 +39,12 @@ pub use nym_api_requests::{ }, }; pub use nym_coconut_dkg_common::types::EpochId; -use nym_http_api_client::{ApiClient, NO_PARAMS}; -use nym_mixnet_contract_common::mixnode::MixNodeDetails; -use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId}; - -pub mod error; -pub mod routes; - -use nym_api_requests::coconut::models::FreePassNonceResponse; -use nym_api_requests::coconut::FreePassRequest; -use nym_api_requests::nym_nodes::{CachedNodesResponse, SkimmedNode}; pub use nym_http_api_client::Client; +pub fn rfc_3339_date() -> Vec> { + time::format_description::parse("[year]-[month]-[day]").unwrap() +} + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait NymApiClientExt: ApiClient { @@ -420,36 +431,6 @@ pub trait NymApiClientExt: ApiClient { .await } - async fn free_pass_nonce(&self) -> Result { - self.get_json( - &[ - routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_FREE_PASS_NONCE, - ], - NO_PARAMS, - ) - .await - } - - async fn free_pass( - &self, - request: &FreePassRequest, - ) -> Result { - self.post_json( - &[ - routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_FREE_PASS, - ], - NO_PARAMS, - request, - ) - .await - } - async fn blind_sign( &self, request_body: &BlindSignRequestBody, @@ -457,9 +438,8 @@ pub trait NymApiClientExt: ApiClient { self.post_json( &[ routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_BLIND_SIGN, + routes::ECASH_ROUTES, + routes::ECASH_BLIND_SIGN, ], NO_PARAMS, request_body, @@ -467,16 +447,15 @@ pub trait NymApiClientExt: ApiClient { .await } - async fn verify_bandwidth_credential( + async fn verify_ecash_ticket( &self, - request_body: &VerifyCredentialBody, - ) -> Result { + request_body: &VerifyEcashTicketBody, + ) -> Result { self.post_json( &[ routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_VERIFY_BANDWIDTH_CREDENTIAL, + routes::ECASH_ROUTES, + routes::VERIFY_ECASH_TICKET, ], NO_PARAMS, request_body, @@ -484,6 +463,139 @@ pub trait NymApiClientExt: ApiClient { .await } + async fn batch_redeem_ecash_tickets( + &self, + request_body: &BatchRedeemTicketsBody, + ) -> Result { + self.post_json( + &[ + routes::API_VERSION, + routes::ECASH_ROUTES, + routes::BATCH_REDEEM_ECASH_TICKETS, + ], + NO_PARAMS, + request_body, + ) + .await + } + + async fn double_spending_filter_v1(&self) -> Result { + self.get_json( + &[ + routes::API_VERSION, + routes::ECASH_ROUTES, + routes::DOUBLE_SPENDING_FILTER_V1, + ], + NO_PARAMS, + ) + .await + } + + async fn partial_expiration_date_signatures( + &self, + expiration_date: Option, + ) -> Result { + let params = match expiration_date { + None => Vec::new(), + Some(exp) => vec![( + ecash::EXPIRATION_DATE_PARAM, + exp.format(&rfc_3339_date()).unwrap(), + )], + }; + + self.get_json( + &[ + routes::API_VERSION, + routes::ECASH_ROUTES, + routes::PARTIAL_EXPIRATION_DATE_SIGNATURES, + ], + ¶ms, + ) + .await + } + + async fn partial_coin_indices_signatures( + &self, + epoch_id: Option, + ) -> Result { + let params = match epoch_id { + None => Vec::new(), + Some(epoch_id) => vec![(ecash::EPOCH_ID_PARAM, epoch_id.to_string())], + }; + + self.get_json( + &[ + routes::API_VERSION, + routes::ECASH_ROUTES, + routes::PARTIAL_COIN_INDICES_SIGNATURES, + ], + ¶ms, + ) + .await + } + + async fn global_expiration_date_signatures( + &self, + expiration_date: Option, + ) -> Result { + let params = match expiration_date { + None => Vec::new(), + Some(exp) => vec![( + ecash::EXPIRATION_DATE_PARAM, + exp.format(&rfc_3339_date()).unwrap(), + )], + }; + + self.get_json( + &[ + routes::API_VERSION, + routes::ECASH_ROUTES, + routes::GLOBAL_EXPIRATION_DATE_SIGNATURES, + ], + ¶ms, + ) + .await + } + + async fn global_coin_indices_signatures( + &self, + epoch_id: Option, + ) -> Result { + let params = match epoch_id { + None => Vec::new(), + Some(epoch_id) => vec![(ecash::EPOCH_ID_PARAM, epoch_id.to_string())], + }; + + self.get_json( + &[ + routes::API_VERSION, + routes::ECASH_ROUTES, + routes::GLOBAL_COIN_INDICES_SIGNATURES, + ], + ¶ms, + ) + .await + } + + async fn master_verification_key( + &self, + epoch_id: Option, + ) -> Result { + let params = match epoch_id { + None => Vec::new(), + Some(epoch_id) => vec![(ecash::EPOCH_ID_PARAM, epoch_id.to_string())], + }; + self.get_json( + &[ + routes::API_VERSION, + routes::ECASH_ROUTES, + routes::ecash::MASTER_VERIFICATION_KEY, + ], + ¶ms, + ) + .await + } + async fn epoch_credentials( &self, dkg_epoch: EpochId, @@ -491,9 +603,8 @@ pub trait NymApiClientExt: ApiClient { self.get_json( &[ routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_EPOCH_CREDENTIALS, + routes::ECASH_ROUTES, + routes::ECASH_EPOCH_CREDENTIALS, &dkg_epoch.to_string(), ], NO_PARAMS, @@ -508,9 +619,8 @@ pub trait NymApiClientExt: ApiClient { self.get_json( &[ routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_ISSUED_CREDENTIAL, + routes::ECASH_ROUTES, + routes::ECASH_ISSUED_CREDENTIAL, &credential_id.to_string(), ], NO_PARAMS, @@ -525,9 +635,8 @@ pub trait NymApiClientExt: ApiClient { self.post_json( &[ routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_ISSUED_CREDENTIALS, + routes::ECASH_ROUTES, + routes::ECASH_ISSUED_CREDENTIALS, ], NO_PARAMS, &CredentialsRequestBody { diff --git a/common/client-libs/validator-client/src/nym_api/routes.rs b/common/client-libs/validator-client/src/nym_api/routes.rs index cc2a17d018..c6a50db513 100644 --- a/common/client-libs/validator-client/src/nym_api/routes.rs +++ b/common/client-libs/validator-client/src/nym_api/routes.rs @@ -12,16 +12,27 @@ pub const DETAILED: &str = "detailed"; pub const DETAILED_UNFILTERED: &str = "detailed-unfiltered"; pub const ACTIVE: &str = "active"; pub const REWARDED: &str = "rewarded"; -pub const COCONUT_ROUTES: &str = "coconut"; -pub const BANDWIDTH: &str = "bandwidth"; +pub const DOUBLE_SPENDING_FILTER_V1: &str = "double-spending-filter-v1"; -pub const COCONUT_FREE_PASS: &str = "free-pass"; -pub const COCONUT_FREE_PASS_NONCE: &str = "free-pass-nonce"; -pub const COCONUT_BLIND_SIGN: &str = "blind-sign"; -pub const COCONUT_VERIFY_BANDWIDTH_CREDENTIAL: &str = "verify-bandwidth-credential"; -pub const COCONUT_EPOCH_CREDENTIALS: &str = "epoch-credentials"; -pub const COCONUT_ISSUED_CREDENTIAL: &str = "issued-credential"; -pub const COCONUT_ISSUED_CREDENTIALS: &str = "issued-credentials"; +pub const ECASH_ROUTES: &str = "ecash"; + +pub use ecash::*; +pub mod ecash { + pub const ECASH_BLIND_SIGN: &str = "blind-sign"; + pub const VERIFY_ECASH_TICKET: &str = "verify-ecash-ticket"; + pub const BATCH_REDEEM_ECASH_TICKETS: &str = "batch-redeem-ecash-tickets"; + pub const PARTIAL_EXPIRATION_DATE_SIGNATURES: &str = "partial-expiration-date-signatures"; + pub const GLOBAL_EXPIRATION_DATE_SIGNATURES: &str = "aggregated-expiration-date-signatures"; + pub const PARTIAL_COIN_INDICES_SIGNATURES: &str = "partial-coin-indices-signatures"; + pub const GLOBAL_COIN_INDICES_SIGNATURES: &str = "aggregated-coin-indices-signatures"; + pub const MASTER_VERIFICATION_KEY: &str = "master-verification-key"; + pub const ECASH_EPOCH_CREDENTIALS: &str = "epoch-credentials"; + pub const ECASH_ISSUED_CREDENTIAL: &str = "issued-credential"; + pub const ECASH_ISSUED_CREDENTIALS: &str = "issued-credentials"; + + pub const EXPIRATION_DATE_PARAM: &str = "expiration_date"; + pub const EPOCH_ID_PARAM: &str = "epoch_id"; +} pub const STATUS_ROUTES: &str = "status"; pub const MIXNODE: &str = "mixnode"; diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/coconut_bandwidth_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/coconut_bandwidth_query_client.rs deleted file mode 100644 index 1737edf613..0000000000 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/coconut_bandwidth_query_client.rs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::collect_paged; -use crate::nyxd::contract_traits::NymContractsProvider; -use crate::nyxd::error::NyxdError; -use crate::nyxd::CosmWasmClient; -use async_trait::async_trait; -use nym_coconut_bandwidth_contract_common::msg::QueryMsg as CoconutBandwidthQueryMsg; -use nym_coconut_bandwidth_contract_common::spend_credential::{ - PagedSpendCredentialResponse, SpendCredential, SpendCredentialResponse, -}; -use serde::Deserialize; - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait CoconutBandwidthQueryClient { - async fn query_coconut_bandwidth_contract( - &self, - query: CoconutBandwidthQueryMsg, - ) -> Result - where - for<'a> T: Deserialize<'a>; - - async fn get_spent_credential( - &self, - blinded_serial_number: String, - ) -> Result { - self.query_coconut_bandwidth_contract(CoconutBandwidthQueryMsg::GetSpentCredential { - blinded_serial_number, - }) - .await - } - - async fn get_all_spent_credential_paged( - &self, - start_after: Option, - limit: Option, - ) -> Result { - self.query_coconut_bandwidth_contract(CoconutBandwidthQueryMsg::GetAllSpentCredentials { - limit, - start_after, - }) - .await - } -} - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait PagedCoconutBandwidthQueryClient: CoconutBandwidthQueryClient { - async fn get_all_spent_credentials(&self) -> Result, NyxdError> { - collect_paged!(self, get_all_spent_credential_paged, spend_credentials) - } -} - -#[async_trait] -impl PagedCoconutBandwidthQueryClient for T where T: CoconutBandwidthQueryClient {} - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl CoconutBandwidthQueryClient for C -where - C: CosmWasmClient + NymContractsProvider + Send + Sync, -{ - async fn query_coconut_bandwidth_contract( - &self, - query: CoconutBandwidthQueryMsg, - ) -> Result - where - for<'a> T: Deserialize<'a>, - { - let coconut_bandwidth_contract_address = self - .coconut_bandwidth_contract_address() - .ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?; - self.query_contract_smart(coconut_bandwidth_contract_address, &query) - .await - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::nyxd::contract_traits::tests::IgnoreValue; - - // it's enough that this compiles and clippy is happy about it - #[allow(dead_code)] - fn all_query_variants_are_covered( - client: C, - msg: CoconutBandwidthQueryMsg, - ) { - match msg { - CoconutBandwidthQueryMsg::GetSpentCredential { - blinded_serial_number, - } => client.get_spent_credential(blinded_serial_number).ignore(), - CoconutBandwidthQueryMsg::GetAllSpentCredentials { limit, start_after } => client - .get_all_spent_credential_paged(start_after, limit) - .ignore(), - }; - } -} diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/coconut_bandwidth_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/coconut_bandwidth_signing_client.rs deleted file mode 100644 index 2402dc078f..0000000000 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/coconut_bandwidth_signing_client.rs +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright 2022 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::nyxd::contract_traits::NymContractsProvider; -use crate::nyxd::cosmwasm_client::types::ExecuteResult; -use crate::nyxd::error::NyxdError; -use crate::nyxd::{Coin, Fee, SigningCosmWasmClient}; -use crate::signing::signer::OfflineSigner; -use async_trait::async_trait; -use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialData; -use nym_coconut_bandwidth_contract_common::{ - deposit::DepositData, msg::ExecuteMsg as CoconutBandwidthExecuteMsg, -}; - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait CoconutBandwidthSigningClient { - async fn execute_coconut_bandwidth_contract( - &self, - fee: Option, - msg: CoconutBandwidthExecuteMsg, - memo: String, - funds: Vec, - ) -> Result; - - async fn deposit( - &self, - amount: Coin, - info: String, - verification_key: String, - encryption_key: String, - fee: Option, - ) -> Result { - let req = CoconutBandwidthExecuteMsg::DepositFunds { - data: DepositData::new(info, verification_key, encryption_key), - }; - self.execute_coconut_bandwidth_contract( - fee, - req, - "CoconutBandwidth::Deposit".to_string(), - vec![amount], - ) - .await - } - - async fn spend_credential( - &self, - funds: Coin, - blinded_serial_number: String, - gateway_cosmos_address: String, - fee: Option, - ) -> Result { - let req = CoconutBandwidthExecuteMsg::SpendCredential { - data: SpendCredentialData::new( - funds.into(), - blinded_serial_number, - gateway_cosmos_address, - ), - }; - self.execute_coconut_bandwidth_contract( - fee, - req, - "CoconutBandwidth::SpendCredential".to_string(), - vec![], - ) - .await - } - - async fn release_funds( - &self, - amount: Coin, - fee: Option, - ) -> Result { - self.execute_coconut_bandwidth_contract( - fee, - CoconutBandwidthExecuteMsg::ReleaseFunds { - funds: amount.into(), - }, - "CoconutBandwidth::ReleaseFunds".to_string(), - vec![], - ) - .await - } -} - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl CoconutBandwidthSigningClient for C -where - C: SigningCosmWasmClient + NymContractsProvider + Sync, - NyxdError: From<::Error>, -{ - async fn execute_coconut_bandwidth_contract( - &self, - fee: Option, - msg: CoconutBandwidthExecuteMsg, - memo: String, - funds: Vec, - ) -> Result { - let coconut_bandwidth_contract_address = self - .coconut_bandwidth_contract_address() - .ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?; - - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier()))); - let signer_address = &self.signer_addresses()?[0]; - - self.execute( - signer_address, - coconut_bandwidth_contract_address, - &msg, - fee, - memo, - funds, - ) - .await - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::nyxd::contract_traits::tests::{mock_coin, IgnoreValue}; - - // it's enough that this compiles and clippy is happy about it - #[allow(dead_code)] - fn all_execute_variants_are_covered( - client: C, - msg: CoconutBandwidthExecuteMsg, - ) { - match msg { - CoconutBandwidthExecuteMsg::DepositFunds { data } => client - .deposit( - mock_coin(), - data.deposit_info().to_string(), - data.identity_key().to_string(), - data.encryption_key().to_string(), - None, - ) - .ignore(), - CoconutBandwidthExecuteMsg::SpendCredential { data } => client - .spend_credential( - mock_coin(), - data.blinded_serial_number().to_string(), - data.gateway_cosmos_address().to_string(), - None, - ) - .ignore(), - CoconutBandwidthExecuteMsg::ReleaseFunds { funds } => { - client.release_funds(funds.into(), None).ignore() - } - }; - } -} diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs index c85f85c399..4d8fd3237c 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs @@ -51,6 +51,11 @@ pub trait DkgQueryClient { self.query_dkg_contract(request).await } + async fn get_epoch_threshold(&self, epoch_id: EpochId) -> Result, NyxdError> { + let request = DkgQueryMsg::GetEpochThreshold { epoch_id }; + self.query_dkg_contract(request).await + } + async fn get_registered_dealer_details( &self, address: &AccountId, @@ -256,6 +261,9 @@ mod tests { DkgQueryMsg::GetCurrentEpochThreshold {} => { client.get_current_epoch_threshold().ignore() } + DkgQueryMsg::GetEpochThreshold { epoch_id } => { + client.get_epoch_threshold(epoch_id).ignore() + } DkgQueryMsg::GetRegisteredDealer { dealer_address, epoch_id, diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_query_client.rs new file mode 100644 index 0000000000..7c4c6c6b4e --- /dev/null +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_query_client.rs @@ -0,0 +1,123 @@ +// Copyright 2022-2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::collect_paged; +use crate::nyxd::contract_traits::NymContractsProvider; +use crate::nyxd::error::NyxdError; +use crate::nyxd::CosmWasmClient; +use async_trait::async_trait; +use cosmwasm_std::Coin; +use nym_ecash_contract_common::msg::QueryMsg as EcashQueryMsg; +use serde::Deserialize; + +pub use nym_ecash_contract_common::blacklist::{ + BlacklistedAccount, BlacklistedAccountResponse, PagedBlacklistedAccountResponse, +}; +pub use nym_ecash_contract_common::deposit::{ + Deposit, DepositData, DepositId, DepositResponse, PagedDepositsResponse, +}; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait EcashQueryClient { + async fn query_ecash_contract(&self, query: EcashQueryMsg) -> Result + where + for<'a> T: Deserialize<'a>; + + async fn get_blacklisted_account( + &self, + public_key: String, + ) -> Result { + self.query_ecash_contract(EcashQueryMsg::GetBlacklistedAccount { public_key }) + .await + } + + async fn get_blacklist_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_ecash_contract(EcashQueryMsg::GetBlacklistPaged { start_after, limit }) + .await + } + + async fn get_required_deposit_amount(&self) -> Result { + self.query_ecash_contract(EcashQueryMsg::GetRequiredDepositAmount {}) + .await + } + + async fn get_deposit(&self, deposit_id: u32) -> Result { + self.query_ecash_contract(EcashQueryMsg::GetDeposit { deposit_id }) + .await + } + + async fn get_deposits_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_ecash_contract(EcashQueryMsg::GetDepositsPaged { start_after, limit }) + .await + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait PagedEcashQueryClient: EcashQueryClient { + async fn get_all_blacklisted_accounts(&self) -> Result, NyxdError> { + collect_paged!(self, get_blacklist_paged, accounts) + } + + async fn get_all_deposits(&self) -> Result, NyxdError> { + collect_paged!(self, get_deposits_paged, deposits) + } +} + +#[async_trait] +impl PagedEcashQueryClient for T where T: EcashQueryClient {} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl EcashQueryClient for C +where + C: CosmWasmClient + NymContractsProvider + Send + Sync, +{ + async fn query_ecash_contract(&self, query: EcashQueryMsg) -> Result + where + for<'a> T: Deserialize<'a>, + { + let ecash_contract_address = self + .ecash_contract_address() + .ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?; + self.query_contract_smart(ecash_contract_address, &query) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::nyxd::contract_traits::tests::IgnoreValue; + use nym_ecash_contract_common::msg::QueryMsg; + + // it's enough that this compiles and clippy is happy about it + #[allow(dead_code)] + fn all_query_variants_are_covered( + client: C, + msg: EcashQueryMsg, + ) { + match msg { + EcashQueryMsg::GetBlacklistedAccount { public_key } => { + client.get_blacklisted_account(public_key).ignore() + } + QueryMsg::GetBlacklistPaged { limit, start_after } => { + client.get_blacklist_paged(start_after, limit).ignore() + } + QueryMsg::GetDeposit { deposit_id } => client.get_deposit(deposit_id).ignore(), + QueryMsg::GetDepositsPaged { limit, start_after } => { + client.get_deposits_paged(start_after, limit).ignore() + } + QueryMsg::GetRequiredDepositAmount {} => client.get_required_deposit_amount().ignore(), + }; + } +} diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_signing_client.rs new file mode 100644 index 0000000000..4c40a2b2fb --- /dev/null +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_signing_client.rs @@ -0,0 +1,149 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::nyxd::contract_traits::NymContractsProvider; +use crate::nyxd::cosmwasm_client::types::ExecuteResult; +use crate::nyxd::error::NyxdError; +use crate::nyxd::{Coin, Fee, SigningCosmWasmClient}; +use crate::signing::signer::OfflineSigner; +use async_trait::async_trait; +use nym_ecash_contract_common::msg::ExecuteMsg as EcashExecuteMsg; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait EcashSigningClient { + async fn execute_ecash_contract( + &self, + fee: Option, + msg: EcashExecuteMsg, + memo: String, + funds: Vec, + ) -> Result; + + async fn make_ticketbook_deposit( + &self, + public_key: String, + deposit_amount: Coin, + fee: Option, + ) -> Result { + let req = EcashExecuteMsg::DepositTicketBookFunds { + identity_key: public_key, + }; + self.execute_ecash_contract(fee, req, "Ecash::Deposit".to_string(), vec![deposit_amount]) + .await + } + + async fn request_ticket_redemption( + &self, + commitment_bs58: String, + number_of_tickets: u16, + fee: Option, + ) -> Result { + let req = EcashExecuteMsg::RequestRedemption { + commitment_bs58, + number_of_tickets, + }; + self.execute_ecash_contract(fee, req, Default::default(), vec![]) + .await + } + + async fn update_admin( + &self, + admin: String, + fee: Option, + ) -> Result { + let req = EcashExecuteMsg::UpdateAdmin { admin }; + self.execute_ecash_contract(fee, req, "Ecash::UpdateAdmin".to_string(), vec![]) + .await + } + + async fn update_deposit_value( + &self, + new_deposit: Coin, + fee: Option, + ) -> Result { + let req = EcashExecuteMsg::UpdateDepositValue { + new_deposit: new_deposit.into(), + }; + self.execute_ecash_contract(fee, req, "Ecash::UpdateDepositValue".to_string(), vec![]) + .await + } + + async fn propose_for_blacklist( + &self, + public_key: String, + fee: Option, + ) -> Result { + let req = EcashExecuteMsg::ProposeToBlacklist { public_key }; + self.execute_ecash_contract(fee, req, "Ecash::ProposeToBlacklist".to_string(), vec![]) + .await + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl EcashSigningClient for C +where + C: SigningCosmWasmClient + NymContractsProvider + Sync, + NyxdError: From<::Error>, +{ + async fn execute_ecash_contract( + &self, + fee: Option, + msg: EcashExecuteMsg, + memo: String, + funds: Vec, + ) -> Result { + let ecash_contract_address = self + .ecash_contract_address() + .ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?; + + let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier()))); + let signer_address = &self.signer_addresses()?[0]; + + self.execute( + signer_address, + ecash_contract_address, + &msg, + fee, + memo, + funds, + ) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::nyxd::contract_traits::tests::{mock_coin, IgnoreValue}; + use nym_ecash_contract_common::msg::ExecuteMsg; + + // it's enough that this compiles and clippy is happy about it + #[allow(dead_code)] + fn all_execute_variants_are_covered( + client: C, + msg: EcashExecuteMsg, + ) { + match msg { + EcashExecuteMsg::DepositTicketBookFunds { identity_key } => client + .make_ticketbook_deposit(identity_key.to_string(), mock_coin(), None) + .ignore(), + EcashExecuteMsg::AddToBlacklist { public_key: _ } => unimplemented!(), //no add to blacklist method on client + EcashExecuteMsg::ProposeToBlacklist { public_key } => { + client.propose_for_blacklist(public_key, None).ignore() + } + ExecuteMsg::RequestRedemption { + commitment_bs58, + number_of_tickets, + } => client + .request_ticket_redemption(commitment_bs58, number_of_tickets, None) + .ignore(), + ExecuteMsg::RedeemTickets { .. } => unimplemented!(), // no redeem tickets method for the client + ExecuteMsg::UpdateAdmin { admin } => client.update_admin(admin, None).ignore(), + ExecuteMsg::UpdateDepositValue { new_deposit } => client + .update_deposit_value(new_deposit.into(), None) + .ignore(), + }; + } +} diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs index 95d692d592..47b67d8f16 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs @@ -42,6 +42,10 @@ pub trait MixnetQueryClient { // state/sys-params-related + async fn admin(&self) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::Admin {}).await + } + async fn get_mixnet_contract_version(&self) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetContractVersion {}) .await @@ -580,6 +584,7 @@ mod tests { msg: MixnetQueryMsg, ) -> u32 { match msg { + MixnetQueryMsg::Admin {} => client.admin().ignore(), MixnetQueryMsg::GetAllFamiliesPaged { limit, start_after } => client .get_all_family_members_paged(start_after, limit) .ignore(), diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs index 9b5761bed6..f84becb55f 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs @@ -31,6 +31,15 @@ pub trait MixnetSigningClient { // state/sys-params-related + async fn update_admin( + &self, + admin: String, + fee: Option, + ) -> Result { + self.execute_mixnet_contract(fee, MixnetExecuteMsg::UpdateAdmin { admin }, vec![]) + .await + } + async fn update_rewarding_validator_address( &self, address: AccountId, @@ -683,6 +692,24 @@ pub trait MixnetSigningClient { .await } + async fn migrate_vested_mixnode(&self, fee: Option) -> Result { + self.execute_mixnet_contract(fee, MixnetExecuteMsg::MigrateVestedMixNode {}, vec![]) + .await + } + + async fn migrate_vested_delegation( + &self, + mix_id: MixId, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::MigrateVestedDelegation { mix_id }, + vec![], + ) + .await + } + #[cfg(feature = "contract-testing")] async fn testing_resolve_all_pending_events( &self, @@ -742,6 +769,7 @@ mod tests { msg: MixnetExecuteMsg, ) { match msg { + MixnetExecuteMsg::UpdateAdmin { admin } => client.update_admin(admin, None).ignore(), MixnetExecuteMsg::AssignNodeLayer { mix_id, layer } => { client.assign_node_layer(mix_id, layer, None).ignore() } @@ -928,6 +956,12 @@ mod tests { MixnetExecuteMsg::WithdrawDelegatorRewardOnBehalf { mix_id, owner } => client .withdraw_delegator_reward_on_behalf(owner.parse().unwrap(), mix_id, None) .ignore(), + MixnetExecuteMsg::MigrateVestedMixNode { .. } => { + client.migrate_vested_mixnode(None).ignore() + } + MixnetExecuteMsg::MigrateVestedDelegation { mix_id } => { + client.migrate_vested_delegation(mix_id, None).ignore() + } #[cfg(feature = "contract-testing")] MixnetExecuteMsg::TestingResolveAllPendingEvents { .. } => { diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs index d58da6fbd3..8210c5b476 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs @@ -8,34 +8,32 @@ use std::str::FromStr; // TODO: all of those could/should be derived via a macro // query clients -pub mod coconut_bandwidth_query_client; pub mod dkg_query_client; +pub mod ecash_query_client; pub mod group_query_client; pub mod mixnet_query_client; pub mod multisig_query_client; pub mod vesting_query_client; // signing clients -pub mod coconut_bandwidth_signing_client; pub mod dkg_signing_client; +pub mod ecash_signing_client; pub mod group_signing_client; pub mod mixnet_signing_client; pub mod multisig_signing_client; pub mod vesting_signing_client; // re-export query traits -pub use coconut_bandwidth_query_client::{ - CoconutBandwidthQueryClient, PagedCoconutBandwidthQueryClient, -}; pub use dkg_query_client::{DkgQueryClient, PagedDkgQueryClient}; +pub use ecash_query_client::{EcashQueryClient, PagedEcashQueryClient}; pub use group_query_client::{GroupQueryClient, PagedGroupQueryClient}; pub use mixnet_query_client::{MixnetQueryClient, PagedMixnetQueryClient}; pub use multisig_query_client::{MultisigQueryClient, PagedMultisigQueryClient}; pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient}; // re-export signing traits -pub use coconut_bandwidth_signing_client::CoconutBandwidthSigningClient; pub use dkg_signing_client::DkgSigningClient; +pub use ecash_signing_client::EcashSigningClient; pub use group_signing_client::GroupSigningClient; pub use mixnet_signing_client::MixnetSigningClient; pub use multisig_signing_client::MultisigSigningClient; @@ -48,7 +46,7 @@ pub trait NymContractsProvider { fn vesting_contract_address(&self) -> Option<&AccountId>; // coconut-related - fn coconut_bandwidth_contract_address(&self) -> Option<&AccountId>; + fn ecash_contract_address(&self) -> Option<&AccountId>; fn dkg_contract_address(&self) -> Option<&AccountId>; fn group_contract_address(&self) -> Option<&AccountId>; fn multisig_contract_address(&self) -> Option<&AccountId>; @@ -59,7 +57,7 @@ pub struct TypedNymContracts { pub mixnet_contract_address: Option, pub vesting_contract_address: Option, - pub coconut_bandwidth_contract_address: Option, + pub ecash_contract_address: Option, pub group_contract_address: Option, pub multisig_contract_address: Option, pub coconut_dkg_contract_address: Option, @@ -78,8 +76,8 @@ impl TryFrom for TypedNymContracts { .vesting_contract_address .map(|addr| addr.parse()) .transpose()?, - coconut_bandwidth_contract_address: value - .coconut_bandwidth_contract_address + ecash_contract_address: value + .ecash_contract_address .map(|addr| addr.parse()) .transpose()?, group_contract_address: value diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_query_client.rs index 57c6ce2bf7..6094219401 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_query_client.rs @@ -6,7 +6,7 @@ use crate::nyxd::error::NyxdError; use crate::nyxd::CosmWasmClient; use async_trait::async_trait; use cw3::{ - ProposalListResponse, ProposalResponse, VoteListResponse, VoteResponse, VoterDetail, + ProposalListResponse, ProposalResponse, VoteInfo, VoteListResponse, VoteResponse, VoterDetail, VoterListResponse, VoterResponse, }; use cw_utils::ThresholdResponse; @@ -134,6 +134,28 @@ pub trait PagedMultisigQueryClient: MultisigQueryClient { Ok(voters) } + + async fn get_all_votes(&self, proposal_id: u64) -> Result, NyxdError> { + let mut votes = Vec::new(); + let mut start_after = None; + + loop { + let mut paged_response = self + .list_votes(proposal_id, start_after.take(), None) + .await?; + + let last_voter = paged_response.votes.last().map(|vote| vote.voter.clone()); + votes.append(&mut paged_response.votes); + + if let Some(start_after_res) = last_voter { + start_after = Some(start_after_res) + } else { + break; + } + } + + Ok(votes) + } } #[async_trait] diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_signing_client.rs index f7249f02ea..ae28cf0ef3 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_signing_client.rs @@ -31,15 +31,15 @@ pub trait MultisigSigningClient: NymContractsProvider { voucher_value: Coin, fee: Option, ) -> Result { - let coconut_bandwidth_contract_address = self - .coconut_bandwidth_contract_address() + let ecash_contract_address = self + .ecash_contract_address() .ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?; let release_funds_req = CoconutBandwidthExecuteMsg::ReleaseFunds { funds: voucher_value.into(), }; let release_funds_msg = CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: coconut_bandwidth_contract_address.to_string(), + contract_addr: ecash_contract_address.to_string(), msg: to_binary(&release_funds_req)?, funds: vec![], }); diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs index 11c449fccd..07472c4262 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs @@ -437,6 +437,7 @@ where mod tests { use super::*; use crate::nyxd::contract_traits::tests::{mock_coin, IgnoreValue}; + use nym_vesting_contract_common::ExecuteMsg; // it's enough that this compiles and clippy is happy about it #[allow(dead_code)] @@ -560,6 +561,9 @@ mod tests { VestingExecuteMsg::UpdateLockedPledgeCap { address, cap } => client .update_locked_pledge_cap(address.parse().unwrap(), cap, None) .ignore(), + // those will never be manually called by clients + ExecuteMsg::TrackMigratedMixnode { .. } => "explicitly_ignored".ignore(), + ExecuteMsg::TrackMigratedDelegation { .. } => "explicitly_ignored".ignore(), }; } } diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs index 2a26c97155..8d0bb0fb4f 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs @@ -2,31 +2,33 @@ // SPDX-License-Identifier: Apache-2.0 use crate::nyxd::cosmwasm_client::client_traits::CosmWasmClient; -use crate::nyxd::cosmwasm_client::helpers::{compress_wasm_code, CheckResponse}; +use crate::nyxd::cosmwasm_client::helpers::{ + compress_wasm_code, parse_msg_responses, CheckResponse, +}; use crate::nyxd::cosmwasm_client::logs::parse_raw_logs; use crate::nyxd::cosmwasm_client::types::*; use crate::nyxd::error::NyxdError; use crate::nyxd::fee::{Fee, DEFAULT_SIMULATED_GAS_MULTIPLIER}; +use crate::nyxd::helpers::find_tx_attribute; use crate::nyxd::{Coin, GasAdjustable, GasPrice, TxResponse}; use crate::signing::signer::OfflineSigner; use crate::signing::tx_signer::TxSigner; use crate::signing::SignerData; use async_trait::async_trait; use cosmrs::bank::MsgSend; +use cosmrs::cosmwasm::{MsgClearAdmin, MsgUpdateAdmin}; use cosmrs::distribution::MsgWithdrawDelegatorReward; use cosmrs::feegrant::{ AllowedMsgAllowance, BasicAllowance, MsgGrantAllowance, MsgRevokeAllowance, }; use cosmrs::proto::cosmos::tx::signing::v1beta1::SignMode; use cosmrs::staking::{MsgDelegate, MsgUndelegate}; -use cosmrs::tendermint::abci::{Event, EventAttribute}; use cosmrs::tx::{self, Msg}; use cosmrs::{cosmwasm, AccountId, Any, Tx}; use log::debug; use serde::Serialize; use sha2::Digest; use sha2::Sha256; - use std::time::SystemTime; use tendermint_rpc::endpoint::broadcast; @@ -52,20 +54,6 @@ fn single_unspecified_signer_auth( } .auth_info(empty_fee()) } -// Searches in events for an event of the given event type which contains an -// attribute for with the given key. -fn find_attribute<'a>( - events: &'a [Event], - event_type: &str, - attr_key: &str, -) -> Option<&'a EventAttribute> { - events - .iter() - .find(|attr| attr.kind == event_type)? - .attributes - .iter() - .find(|attr| attr.key == attr_key) -} #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -132,8 +120,7 @@ where .await? .check_response()?; - let logs = parse_raw_logs(tx_res.tx_result.log)?; - let events = tx_res.tx_result.events; + let logs = parse_raw_logs(&tx_res.tx_result.log)?; let gas_info = GasInfo { gas_wanted: tx_res.tx_result.gas_wanted.try_into().unwrap_or_default(), gas_used: tx_res.tx_result.gas_used.try_into().unwrap_or_default(), @@ -143,9 +130,8 @@ where // the reason I think unwrap here is fine is that if the transaction succeeded and those // fields do not exist or code_id is not a number, there's no way we can recover, we're probably connected // to wrong validator or something - let code_id = find_attribute(&events, "store_code", "code_id") + let code_id = find_tx_attribute(&tx_res, "store_code", "code_id") .unwrap() - .value .parse() .unwrap(); @@ -156,7 +142,7 @@ where compressed_checksum, code_id, logs, - events, + events: tx_res.tx_result.events, transaction_hash: tx_res.hash, gas_info, }) @@ -198,8 +184,7 @@ where .await? .check_response()?; - let logs = parse_raw_logs(tx_res.tx_result.log)?; - let events = tx_res.tx_result.events; + let logs = parse_raw_logs(&tx_res.tx_result.log)?; let gas_info = GasInfo { gas_wanted: tx_res.tx_result.gas_wanted.try_into().unwrap_or_default(), gas_used: tx_res.tx_result.gas_used.try_into().unwrap_or_default(), @@ -208,16 +193,15 @@ where // the reason I think unwrap here is fine is that if the transaction succeeded and those // fields do not exist or address is malformed, there's no way we can recover, we're probably connected // to wrong validator or something - let contract_address = find_attribute(&events, "instantiate", "_contract_address") + let contract_address = find_tx_attribute(&tx_res, "instantiate", "_contract_address") .unwrap() - .value .parse() .unwrap(); Ok(InstantiateResult { contract_address, logs, - events, + events: tx_res.tx_result.events, transaction_hash: tx_res.hash, gas_info, }) @@ -231,7 +215,7 @@ where fee: Fee, memo: impl Into + Send + 'static, ) -> Result { - let change_admin_msg = sealed::cosmwasm::MsgUpdateAdmin { + let change_admin_msg = MsgUpdateAdmin { sender: sender_address.clone(), new_admin: new_admin.clone(), contract: contract_address.clone(), @@ -263,7 +247,7 @@ where fee: Fee, memo: impl Into + Send + 'static, ) -> Result { - let change_admin_msg = sealed::cosmwasm::MsgClearAdmin { + let change_admin_msg = MsgClearAdmin { sender: sender_address.clone(), contract: contract_address.clone(), } @@ -355,10 +339,11 @@ where gas_wanted: tx_res.tx_result.gas_wanted.try_into().unwrap_or_default(), gas_used: tx_res.tx_result.gas_used.try_into().unwrap_or_default(), }; + Ok(ExecuteResult { logs: parse_raw_logs(tx_res.tx_result.log)?, + msg_responses: parse_msg_responses(tx_res.tx_result.data), events: tx_res.tx_result.events, - data: tx_res.tx_result.data.into(), transaction_hash: tx_res.hash, gas_info, }) @@ -401,8 +386,8 @@ where }; Ok(ExecuteResult { logs: parse_raw_logs(tx_res.tx_result.log)?, + msg_responses: parse_msg_responses(tx_res.tx_result.data), events: tx_res.tx_result.events, - data: tx_res.tx_result.data.into(), transaction_hash: tx_res.hash, gas_info, }) @@ -731,167 +716,3 @@ where )?) } } - -// a temporary bypass until https://github.com/cosmos/cosmos-rust/pull/419 is merged -mod sealed { - pub mod cosmwasm { - use cosmrs::{proto, tx::Msg, AccountId, ErrorReport, Result}; - - /// MsgUpdateAdmin sets a new admin for a smart contract - #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] - pub struct MsgUpdateAdmin { - /// Sender is the that actor that signed the messages - pub sender: AccountId, - - /// NewAdmin address to be set - pub new_admin: AccountId, - - /// Contract is the address of the smart contract - pub contract: AccountId, - } - - impl Msg for MsgUpdateAdmin { - type Proto = proto::cosmwasm::wasm::v1::MsgUpdateAdmin; - } - - impl TryFrom for MsgUpdateAdmin { - type Error = ErrorReport; - - fn try_from( - proto: proto::cosmwasm::wasm::v1::MsgUpdateAdmin, - ) -> Result { - MsgUpdateAdmin::try_from(&proto) - } - } - - impl TryFrom<&proto::cosmwasm::wasm::v1::MsgUpdateAdmin> for MsgUpdateAdmin { - type Error = ErrorReport; - - fn try_from( - proto: &proto::cosmwasm::wasm::v1::MsgUpdateAdmin, - ) -> Result { - Ok(MsgUpdateAdmin { - sender: proto.sender.parse()?, - new_admin: proto.new_admin.parse()?, - contract: proto.contract.parse()?, - }) - } - } - - impl From for proto::cosmwasm::wasm::v1::MsgUpdateAdmin { - fn from(msg: MsgUpdateAdmin) -> proto::cosmwasm::wasm::v1::MsgUpdateAdmin { - proto::cosmwasm::wasm::v1::MsgUpdateAdmin::from(&msg) - } - } - - impl From<&MsgUpdateAdmin> for proto::cosmwasm::wasm::v1::MsgUpdateAdmin { - fn from(msg: &MsgUpdateAdmin) -> proto::cosmwasm::wasm::v1::MsgUpdateAdmin { - proto::cosmwasm::wasm::v1::MsgUpdateAdmin { - sender: msg.sender.to_string(), - new_admin: msg.new_admin.to_string(), - contract: msg.contract.to_string(), - } - } - } - - /// MsgUpdateAdminResponse returns empty data - #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] - pub struct MsgUpdateAdminResponse {} - - impl Msg for MsgUpdateAdminResponse { - type Proto = proto::cosmwasm::wasm::v1::MsgUpdateAdminResponse; - } - - impl TryFrom for MsgUpdateAdminResponse { - type Error = ErrorReport; - - fn try_from( - _proto: proto::cosmwasm::wasm::v1::MsgUpdateAdminResponse, - ) -> Result { - Ok(MsgUpdateAdminResponse {}) - } - } - - impl From for proto::cosmwasm::wasm::v1::MsgUpdateAdminResponse { - fn from( - _msg: MsgUpdateAdminResponse, - ) -> proto::cosmwasm::wasm::v1::MsgUpdateAdminResponse { - proto::cosmwasm::wasm::v1::MsgUpdateAdminResponse {} - } - } - - /// MsgClearAdmin removes any admin stored for a smart contract - #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] - pub struct MsgClearAdmin { - /// Sender is the that actor that signed the messages - pub sender: AccountId, - - /// Contract is the address of the smart contract - pub contract: AccountId, - } - - impl Msg for MsgClearAdmin { - type Proto = proto::cosmwasm::wasm::v1::MsgClearAdmin; - } - - impl TryFrom for MsgClearAdmin { - type Error = ErrorReport; - - fn try_from(proto: proto::cosmwasm::wasm::v1::MsgClearAdmin) -> Result { - MsgClearAdmin::try_from(&proto) - } - } - - impl TryFrom<&proto::cosmwasm::wasm::v1::MsgClearAdmin> for MsgClearAdmin { - type Error = ErrorReport; - - fn try_from(proto: &proto::cosmwasm::wasm::v1::MsgClearAdmin) -> Result { - Ok(MsgClearAdmin { - sender: proto.sender.parse()?, - contract: proto.contract.parse()?, - }) - } - } - - impl From for proto::cosmwasm::wasm::v1::MsgClearAdmin { - fn from(msg: MsgClearAdmin) -> proto::cosmwasm::wasm::v1::MsgClearAdmin { - proto::cosmwasm::wasm::v1::MsgClearAdmin::from(&msg) - } - } - - impl From<&MsgClearAdmin> for proto::cosmwasm::wasm::v1::MsgClearAdmin { - fn from(msg: &MsgClearAdmin) -> proto::cosmwasm::wasm::v1::MsgClearAdmin { - proto::cosmwasm::wasm::v1::MsgClearAdmin { - sender: msg.sender.to_string(), - contract: msg.contract.to_string(), - } - } - } - - /// MsgClearAdminResponse returns empty data - #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] - pub struct MsgClearAdminResponse {} - - impl Msg for MsgClearAdminResponse { - type Proto = proto::cosmwasm::wasm::v1::MsgClearAdminResponse; - } - - impl TryFrom for MsgClearAdminResponse { - type Error = ErrorReport; - - fn try_from( - _proto: proto::cosmwasm::wasm::v1::MsgClearAdminResponse, - ) -> Result { - Ok(MsgClearAdminResponse {}) - } - } - - impl From for proto::cosmwasm::wasm::v1::MsgClearAdminResponse { - fn from( - _msg: MsgClearAdminResponse, - ) -> proto::cosmwasm::wasm::v1::MsgClearAdminResponse { - proto::cosmwasm::wasm::v1::MsgClearAdminResponse {} - } - } - } -} diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs index 1718ab2f02..2212aa4cff 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs @@ -1,10 +1,88 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::nyxd::cosmwasm_client::types::ExecuteResult; use crate::nyxd::error::NyxdError; +use cosmrs::abci::TxMsgData; +use cosmrs::cosmwasm::MsgExecuteContractResponse; use cosmrs::proto::cosmos::base::query::v1beta1::{PageRequest, PageResponse}; +use log::error; +use prost::bytes::Bytes; use tendermint_rpc::endpoint::broadcast; +pub use cosmrs::abci::MsgResponse; + +pub fn parse_msg_responses(data: Bytes) -> Vec { + // it seems that currently, on wasmd 0.43 + tendermint-rs 0.37 + cosmrs 0.17.0-pre + // the data is left in undecoded base64 form, but I'd imagine this might change so if the decoding fails, + // use the bytes directly instead + let data = if let Ok(decoded) = base64::decode(&data) { + decoded + } else { + error!("failed to base64-decode the 'data' field of the TxResponse - has the chain been upgraded and introduced some breaking changes?"); + data.into() + }; + + match TxMsgData::try_from(data) { + Ok(tx_msg_data) => tx_msg_data.msg_responses, + Err(err) => { + error!("failed to parse tx responses - has the chain been upgraded and introduced some breaking changes? the error was {err}"); + Vec::new() + } + } +} + +// requires there's a single response message +pub trait ToSingletonContractData: Sized { + fn parse_singleton_u32_contract_data(&self) -> Result { + let b = self.to_singleton_contract_data()?; + if b.len() != 4 { + return Err(NyxdError::MalformedResponseData { + got: b.len(), + expected: 4, + }); + } + Ok(u32::from_be_bytes([b[0], b[1], b[2], b[3]])) + } + + fn parse_singleton_u64_contract_data(&self) -> Result { + let b = self.to_singleton_contract_data()?; + if b.len() != 8 { + return Err(NyxdError::MalformedResponseData { + got: b.len(), + expected: 8, + }); + } + Ok(u64::from_be_bytes([ + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], + ])) + } + + fn to_singleton_contract_data(&self) -> Result, NyxdError>; +} + +impl ToSingletonContractData for ExecuteResult { + fn to_singleton_contract_data(&self) -> Result, NyxdError> { + if self.msg_responses.len() != 1 { + return Err(NyxdError::UnexpectedNumberOfMsgResponses { + got: self.msg_responses.len(), + }); + } + + self.msg_responses[0].to_contract_response_data() + } +} + +pub trait ToContractResponseData: Sized { + fn to_contract_response_data(&self) -> Result, NyxdError>; +} + +impl ToContractResponseData for MsgResponse { + fn to_contract_response_data(&self) -> Result, NyxdError> { + Ok(self.try_decode_as::()?.data) + } +} + pub(crate) trait CheckResponse: Sized { fn check_response(self) -> Result; } diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/logs.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/logs.rs index 24180d8466..3d5ecab961 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/logs.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/logs.rs @@ -3,10 +3,11 @@ use crate::nyxd::error::NyxdError; use itertools::Itertools; +use nym_ecash_contract_common::events::PROPOSAL_ID_ATTRIBUTE_NAME; use serde::{Deserialize, Serialize}; -pub use nym_coconut_bandwidth_contract_common::event_attributes::*; pub use nym_coconut_dkg_common::event_attributes::*; +pub use nym_ecash_contract_common::event_attributes::*; // it seems that currently validators just emit stringified events (which are also returned as part of deliverTx response) // as their logs @@ -20,7 +21,8 @@ pub struct Log { /// Searches in logs for the first event of the given event type and in that event /// for the first attribute with the given attribute key. -pub fn find_attribute<'a>( +#[deprecated] +pub fn find_attribute_in_logs<'a>( logs: &'a [Log], event_type: &str, attribute_key: &str, @@ -33,6 +35,26 @@ pub fn find_attribute<'a>( .find(|attr| attr.key == attribute_key) } +/// Search for the proposal id in the given log. It'll be in the LAST wasm event, with attribute key "proposal_id" +#[deprecated] +pub fn find_proposal_id(logs: &[Log]) -> Result { + let maybe_attributes = logs + .iter() + .rev() + .flat_map(|log| log.events.iter()) + .find(|event| event.ty == "wasm") + .ok_or(NyxdError::ComswasmEventNotFound)? + .attributes + .iter() + .find(|attr| attr.key == PROPOSAL_ID_ATTRIBUTE_NAME); + let attribute = maybe_attributes.ok_or(NyxdError::ComswasmAttributeNotFound)?; + + attribute + .value + .parse::() + .map_err(|_| NyxdError::DeserializationError("proposal_id".into())) +} + // these two functions were separated so that the internal logic could actually be tested fn parse_raw_str_logs(raw: &str) -> Result, NyxdError> { // From Cosmos SDK > 0.50 onwards, log field is not populated @@ -49,7 +71,7 @@ fn parse_raw_str_logs(raw: &str) -> Result, NyxdError> { Ok(logs) } -pub fn parse_raw_logs(raw: String) -> Result, NyxdError> { +pub fn parse_raw_logs>(raw: S) -> Result, NyxdError> { parse_raw_str_logs(raw.as_ref()) } diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs index 544f6aaf0d..3ededcb929 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs @@ -23,6 +23,8 @@ use tendermint_rpc::endpoint::*; use tendermint_rpc::query::Query; use tendermint_rpc::{Error as TendermintRpcError, Order, Paging, SimpleRequest}; +pub use helpers::{ToContractResponseData, ToSingletonContractData}; + #[cfg(feature = "http-client")] use crate::http_client; #[cfg(feature = "http-client")] diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/types.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/types.rs index b10f9b65ec..564a17441e 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/types.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/types.rs @@ -30,6 +30,7 @@ use prost::Message; use serde::Serialize; pub use cosmrs::abci::GasInfo; +pub use cosmrs::abci::MsgResponse; pub type ContractCodeId = u64; @@ -240,7 +241,7 @@ pub struct UploadResult { pub gas_info: GasInfo, } -#[derive(Debug)] +#[derive(Debug, Default)] pub struct InstantiateOptions { /// The funds that are transferred from the sender to the newly created contract. /// The funds are transferred as part of the message execution after the contract address is @@ -262,6 +263,11 @@ impl InstantiateOptions { admin, } } + + pub fn with_admin(mut self, admin: AccountId) -> Self { + self.admin = Some(admin); + self + } } #[derive(Debug, Serialize)] @@ -307,7 +313,7 @@ pub struct MigrateResult { pub struct ExecuteResult { pub logs: Vec, - pub data: Vec, + pub msg_responses: Vec, pub events: Vec, diff --git a/common/client-libs/validator-client/src/nyxd/error.rs b/common/client-libs/validator-client/src/nyxd/error.rs index 905d484b72..c71ed596ad 100644 --- a/common/client-libs/validator-client/src/nyxd/error.rs +++ b/common/client-libs/validator-client/src/nyxd/error.rs @@ -32,6 +32,12 @@ pub enum NyxdError { #[error("There was an issue on the cosmrs side: {0}")] CosmrsErrorReport(#[from] cosmrs::ErrorReport), + #[error("cosmwasm event not found")] + ComswasmEventNotFound, + + #[error("cosmwasm attribute not found")] + ComswasmAttributeNotFound, + #[error("Failed to derive account address")] AccountDerivationError, @@ -142,6 +148,12 @@ pub enum NyxdError { #[error("Account had an unexpected bech32 prefix. Expected: {expected}, got: {got}")] UnexpectedBech32Prefix { got: String, expected: String }, + + #[error("the transaction returned unexpected, {got}, number of MsgResponse. Expected to receive a single one")] + UnexpectedNumberOfMsgResponses { got: usize }, + + #[error("the response data has invalid size. got {got} bytes, but expected {expected} bytes instead")] + MalformedResponseData { got: usize, expected: usize }, } // The purpose of parsing the abci query result is that we want to generate the `pretty_log` if diff --git a/common/client-libs/validator-client/src/nyxd/helpers.rs b/common/client-libs/validator-client/src/nyxd/helpers.rs index 85999eac6f..3fbfdaaaf3 100644 --- a/common/client-libs/validator-client/src/nyxd/helpers.rs +++ b/common/client-libs/validator-client/src/nyxd/helpers.rs @@ -1,13 +1,50 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::nyxd::cosmwasm_client::logs::Log; use crate::nyxd::TxResponse; +use cosmrs::tendermint::abci; +pub use abci::Event; + +// Searches in events for an event of the given event type which contains an +// attribute for with the given key. pub fn find_tx_attribute(tx: &TxResponse, event_type: &str, attribute_key: &str) -> Option { - let event = tx.tx_result.events.iter().find(|e| e.kind == event_type)?; - let attribute = event - .attributes - .iter() - .find(|attr| attr.key == attribute_key)?; - Some(attribute.value.clone()) + find_event_attribute(&tx.tx_result.events, event_type, attribute_key) +} + +pub fn find_event_attribute( + events: &[Event], + event_type: &str, + attribute_key: &str, +) -> Option { + let event = events.iter().find(|e| e.kind == event_type)?; + let attribute = event.attributes.iter().find(|&attr| { + if let Ok(key_str) = attr.key_str() { + key_str == attribute_key + } else { + false + } + })?; + Some(attribute.value_str().ok().map(|str| str.to_string())).flatten() +} + +pub fn find_attribute_value_in_logs_or_events( + logs: &[Log], + events: &[Event], + event_type: &str, + attribute_key: &str, +) -> Option { + // if logs are empty, i.e. we're using post 0.50 code, parse the events instead + if !logs.is_empty() { + #[allow(deprecated)] + return crate::nyxd::cosmwasm_client::logs::find_attribute_in_logs( + logs, + event_type, + attribute_key, + ) + .map(|attr| attr.value.clone()); + } + + find_event_attribute(events, event_type, attribute_key) } diff --git a/common/client-libs/validator-client/src/nyxd/mod.rs b/common/client-libs/validator-client/src/nyxd/mod.rs index 608fb2b2b0..fd3e40c379 100644 --- a/common/client-libs/validator-client/src/nyxd/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/mod.rs @@ -245,8 +245,8 @@ impl NyxdClient { self.config.contracts.vesting_contract_address = Some(address); } - pub fn set_coconut_bandwidth_contract_address(&mut self, address: AccountId) { - self.config.contracts.coconut_bandwidth_contract_address = Some(address); + pub fn set_ecash_contract_address(&mut self, address: AccountId) { + self.config.contracts.ecash_contract_address = Some(address); } pub fn set_multisig_contract_address(&mut self, address: AccountId) { @@ -267,11 +267,8 @@ impl NymContractsProvider for NyxdClient { self.config.contracts.vesting_contract_address.as_ref() } - fn coconut_bandwidth_contract_address(&self) -> Option<&AccountId> { - self.config - .contracts - .coconut_bandwidth_contract_address - .as_ref() + fn ecash_contract_address(&self) -> Option<&AccountId> { + self.config.contracts.ecash_contract_address.as_ref() } fn dkg_contract_address(&self) -> Option<&AccountId> { @@ -384,6 +381,14 @@ where } } + pub fn mix_coin(&self, amount: u128) -> Coin { + Coin::new(amount, &self.config.chain_details.mix_denom.base) + } + + pub fn mix_coins(&self, amount: u128) -> Vec { + vec![self.mix_coin(amount)] + } + pub fn cw_address(&self) -> Addr { // the call to unchecked is fine here as we're converting directly from `AccountId` // which must have been a valid bech32 address diff --git a/common/commands/Cargo.toml b/common/commands/Cargo.toml index 0649e4e876..6ff825dcbc 100644 --- a/common/commands/Cargo.toml +++ b/common/commands/Cargo.toml @@ -7,9 +7,10 @@ license.workspace = true [dependencies] anyhow = { workspace = true } -base64 = "0.13.0" +base64 = { workspace = true } bip39 = { workspace = true } bs58 = { workspace = true } +colored = { workspace = true } comfy-table = { workspace = true } cfg-if = { workspace = true } clap = { workspace = true, features = ["derive"] } @@ -21,13 +22,13 @@ humantime-serde = { workspace = true } inquire = { workspace = true } k256 = { workspace = true, features = ["ecdsa", "sha256"] } log = { workspace = true } -rand = {version = "0.6", features = ["std"] } +rand = { workspace = true, features = ["std"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } thiserror = { workspace = true } time = { workspace = true, features = ["parsing", "formatting"] } -tokio = { workspace = true, features = ["sync"]} -toml = "0.5.6" +tokio = { workspace = true, features = ["sync"] } +toml = { workspace = true } url = { workspace = true } tap = { workspace = true } zeroize = { workspace = true } @@ -43,9 +44,9 @@ nym-contracts-common = { path = "../cosmwasm-smart-contracts/contracts-common" } nym-bandwidth-controller = { path = "../../common/bandwidth-controller" } nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" } nym-vesting-contract-common = { path = "../cosmwasm-smart-contracts/vesting-contract" } -nym-coconut-bandwidth-contract-common = { path = "../cosmwasm-smart-contracts/coconut-bandwidth-contract" } nym-coconut-dkg-common = { path = "../cosmwasm-smart-contracts/coconut-dkg" } nym-multisig-contract-common = { path = "../cosmwasm-smart-contracts/multisig-contract" } +nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" } nym-sphinx = { path = "../../common/nymsphinx" } nym-client-core = { path = "../../common/client-core" } nym-config = { path = "../../common/config" } diff --git a/common/commands/src/coconut/generate_freepass.rs b/common/commands/src/coconut/generate_freepass.rs deleted file mode 100644 index 6ea535ebfe..0000000000 --- a/common/commands/src/coconut/generate_freepass.rs +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::context::SigningClient; -use anyhow::{anyhow, bail}; -use clap::ArgGroup; -use clap::Parser; -use futures::StreamExt; -use log::{error, info}; -use nym_coconut_dkg_common::types::EpochId; -use nym_credential_utils::utils::block_until_coconut_is_available; -use nym_credentials::coconut::bandwidth::freepass::MAX_FREE_PASS_VALIDITY; -use nym_credentials::{ - obtain_aggregate_verification_key, IssuanceBandwidthCredential, IssuedBandwidthCredential, -}; -use nym_credentials_interface::VerificationKey; -use nym_validator_client::coconut::all_coconut_api_clients; -use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, NymContractsProvider}; -use nym_validator_client::nyxd::CosmWasmClient; -use nym_validator_client::signing::AccountData; -use nym_validator_client::CoconutApiClient; -use std::fs::File; -use std::io::Write; -use std::path::PathBuf; -use std::sync::Arc; -use time::format_description::well_known::Rfc3339; -use time::OffsetDateTime; -use zeroize::Zeroizing; - -fn parse_rfc3339_expiration_date(raw: &str) -> Result { - OffsetDateTime::parse(raw, &Rfc3339) -} - -#[derive(Debug, Parser)] -#[clap(group(ArgGroup::new("expiration").required(true)))] -pub struct Args { - /// Specifies the expiration date of the free pass(es) - /// Can't be set to more than a week into the future. - #[clap(long, group = "expiration", value_parser = parse_rfc3339_expiration_date)] - pub(crate) expiration_date: Option, - - /// The expiration of the free pass(es) expresses as unix timestamp. - /// Can't be set to more than a week into the future. - #[clap(long, group = "expiration")] - pub(crate) expiration_timestamp: Option, - - /// The number of free passes to issue - #[clap(long, default_value = "1")] - pub(crate) amount: u64, - - /// Path to the output directory for generated free passes. - #[clap(long)] - pub(crate) output_dir: PathBuf, -} - -async fn get_freepass( - api_clients: Vec, - aggregate_vk: &VerificationKey, - threshold: u64, - epoch_id: EpochId, - signing_account: &AccountData, - expiration_date: OffsetDateTime, -) -> anyhow::Result { - let issuance_pass = IssuanceBandwidthCredential::new_freepass(Some(expiration_date)); - let signing_data = issuance_pass.prepare_for_signing(); - - let credential_shares = Arc::new(tokio::sync::Mutex::new(Vec::new())); - - futures::stream::iter(api_clients) - .for_each_concurrent(None, |client| async { - // move the client into the block - let client = client; - let api_url = client.api_client.api_url(); - - info!("contacting {api_url} for blinded free pass"); - - match issuance_pass - .obtain_partial_freepass_credential( - &client.api_client, - signing_account, - &client.verification_key, - signing_data.clone(), - ) - .await - { - Ok(partial_credential) => { - credential_shares - .lock() - .await - .push((partial_credential, client.node_id).into()); - } - Err(err) => { - error!("failed to obtain partial free pass from {api_url}: {err}") - } - } - }) - .await; - - // SAFETY: the futures have completed, so we MUST have the only arc reference - #[allow(clippy::unwrap_used)] - let credential_shares = Arc::into_inner(credential_shares).unwrap().into_inner(); - - if credential_shares.len() < threshold as usize { - bail!("we managed to obtain only {} partial credentials while the minimum threshold is {threshold}", credential_shares.len()); - } - - let signature = issuance_pass.aggregate_signature_shares(aggregate_vk, &credential_shares)?; - Ok(issuance_pass.into_issued_credential(signature, epoch_id)) -} - -pub async fn execute(args: Args, client: SigningClient) -> anyhow::Result<()> { - let address = client.address(); - - if !args.output_dir.is_dir() { - bail!("the provided output directory is not a directory!"); - } - - if args.output_dir.read_dir()?.next().is_some() { - bail!("the provided output directory is not empty!"); - } - - let Some(bandwidth_contract) = client.coconut_bandwidth_contract_address() else { - bail!("the bandwidth contract address is not set") - }; - - let Some(bandwidth_admin) = client - .get_contract(bandwidth_contract) - .await - .map(|c| c.contract_info.admin)? - else { - bail!("the bandwidth contract doesn't have any admin set") - }; - - // sanity checks since nym-apis will reject invalid requests anyway - if address != bandwidth_admin { - bail!("the provided mnemonic does not correspond to the current admin of the bandwidth contract") - } - - let expiration_date = match args.expiration_date { - Some(date) => date, - // SAFETY: one of those arguments must have been set - None => OffsetDateTime::from_unix_timestamp(args.expiration_timestamp.unwrap())?, - }; - - let now = OffsetDateTime::now_utc(); - - if expiration_date > now + MAX_FREE_PASS_VALIDITY { - bail!("the provided free pass request has too long expiry (expiry is set to on {expiration_date})") - } - - if expiration_date < now { - bail!("the provided free pass expiry is set in the past!") - } - - // issuance start - block_until_coconut_is_available(&client).await?; - - let signing_account = client.signing_account()?; - - let epoch_id = client.get_current_epoch().await?.epoch_id; - let threshold = client - .get_current_epoch_threshold() - .await? - .ok_or(anyhow!("no threshold available"))?; - let api_clients = all_coconut_api_clients(&client, epoch_id).await?; - - if api_clients.len() < threshold as usize { - bail!( - "we have only {} api clients available while the minimum threshold is {threshold}", - api_clients.len() - ) - } - let aggregate_vk = obtain_aggregate_verification_key(&api_clients)?; - - for i in 0..args.amount { - let human_index = i + 1; - info!("trying to obtain free pass {human_index}/{}", args.amount); - let free_pass = get_freepass( - api_clients.clone(), - &aggregate_vk, - threshold, - epoch_id, - &signing_account, - expiration_date, - ) - .await?; - let credential_data = Zeroizing::new(free_pass.pack_v1()); - let output = args.output_dir.join(format!("freepass_{i}.nym")); - info!("saving the freepass to '{}'", output.display()); - File::create(output)?.write_all(&credential_data)?; - } - - Ok(()) -} diff --git a/common/commands/src/coconut/issue_credentials.rs b/common/commands/src/coconut/issue_credentials.rs deleted file mode 100644 index 9533af614e..0000000000 --- a/common/commands/src/coconut/issue_credentials.rs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::context::SigningClient; -use crate::utils::CommonConfigsWrapper; -use anyhow::bail; -use clap::Parser; -use nym_credential_storage::initialise_persistent_storage; -use nym_credential_utils::utils; -use nym_validator_client::nyxd::Coin; -use std::path::PathBuf; - -#[derive(Debug, Parser)] -pub struct Args { - /// Config file of the client that is supposed to use the credential. - #[clap(long)] - pub(crate) client_config: PathBuf, - - /// The amount of utokens the credential will hold. - #[clap(long, default_value = "0")] - pub(crate) amount: u64, - - /// Path to a directory used to store recovery files for unconsumed deposits - #[clap(long)] - pub(crate) recovery_dir: PathBuf, -} - -pub async fn execute(args: Args, client: SigningClient) -> anyhow::Result<()> { - if args.amount == 0 { - bail!("did not specify credential amount") - } - - let loaded = CommonConfigsWrapper::try_load(args.client_config)?; - - if let Ok(id) = loaded.try_get_id() { - println!("loaded config file for client '{id}'"); - } - - let Ok(credentials_store) = loaded.try_get_credentials_store() else { - bail!("the loaded config does not have a credentials store information") - }; - - println!( - "using credentials store at '{}'", - credentials_store.display() - ); - - let denom = &client.current_chain_details().mix_denom.base; - let coin = Coin::new(args.amount as u128, denom); - - let persistent_storage = initialise_persistent_storage(credentials_store).await; - utils::issue_credential(&client, coin, &persistent_storage, args.recovery_dir).await?; - - Ok(()) -} diff --git a/common/commands/src/coconut/mod.rs b/common/commands/src/coconut/mod.rs deleted file mode 100644 index 700c7d521f..0000000000 --- a/common/commands/src/coconut/mod.rs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2022 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use clap::{Args, Subcommand}; - -pub mod generate_freepass; -pub mod import_credential; -pub mod issue_credentials; -pub mod recover_credentials; - -#[derive(Debug, Args)] -#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] -pub struct Coconut { - #[clap(subcommand)] - pub command: CoconutCommands, -} - -#[derive(Debug, Subcommand)] -pub enum CoconutCommands { - GenerateFreepass(generate_freepass::Args), - IssueCredentials(issue_credentials::Args), - RecoverCredentials(recover_credentials::Args), - ImportCredential(import_credential::Args), -} diff --git a/common/commands/src/ecash/generate_ticket.rs b/common/commands/src/ecash/generate_ticket.rs new file mode 100644 index 0000000000..67a85ab093 --- /dev/null +++ b/common/commands/src/ecash/generate_ticket.rs @@ -0,0 +1,178 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::utils::CommonConfigsWrapper; +use anyhow::{anyhow, bail}; +use clap::Parser; +use colored::Colorize; +use comfy_table::Table; +use nym_credential_storage::initialise_persistent_storage; +use nym_credential_storage::storage::Storage; +use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise; +use std::path::PathBuf; + +#[derive(Debug, Parser)] +pub struct Args { + /// Specify the index of the ticket to retrieve from the ticketbook. + /// By default, the current unspent value is used. + #[clap(long, group = "output")] + pub(crate) ticket_index: Option, + + /// Specify whether we should display payments for ALL available tickets + #[clap(long, group = "output")] + pub(crate) full: bool, + + /// Base58-encoded identity of the provider (must be 32 bytes long) + #[clap(long)] + pub(crate) provider: String, + + /// Config file of the client that is supposed to use the credential. + #[clap(long, group = "source")] + pub(crate) client_config: Option, + + /// Path to the dedicated credential storage database + #[clap(long, group = "source")] + pub(crate) credential_storage: Option, +} + +pub async fn execute(args: Args) -> anyhow::Result<()> { + let credentials_store = if let Some(explicit) = args.credential_storage { + explicit + } else { + // SAFETY: at least one of them MUST HAVE been specified + let cfg = args.client_config.unwrap(); + + let loaded = CommonConfigsWrapper::try_load(cfg)?; + + if let Ok(id) = loaded.try_get_id() { + println!("loaded config file for client '{id}'"); + } + + let Ok(credentials_store) = loaded.try_get_credentials_store() else { + bail!("the loaded config does not have a credentials store information") + }; + credentials_store + }; + + let decoded_provider = bs58::decode(&args.provider).into_vec()?; + if decoded_provider.len() != 32 { + bail!("the provided provider information is malformed") + } + let provider_arr: [u8; 32] = decoded_provider.try_into().unwrap(); + + let persistent_storage = initialise_persistent_storage(&credentials_store).await; + let Some(mut next_ticketbook) = persistent_storage + .get_next_unspent_usable_ticketbook(0) + .await? + else { + bail!( + "there are no valid ticketbooks in the storage at {}", + credentials_store.display() + ) + }; + + let epoch_id = next_ticketbook.ticketbook.epoch_id(); + let expiration_date = next_ticketbook.ticketbook.expiration_date(); + + let verification_key = persistent_storage + .get_master_verification_key(epoch_id) + .await? + .ok_or_else(|| { + anyhow!("ticketbook got incorrectly imported - the master verification key is missing") + })?; + let expiration_signatures = persistent_storage + .get_expiration_date_signatures(expiration_date) + .await? + .ok_or_else(|| { + anyhow!( + "ticketbook got incorrectly imported - the expiration date signatures are missing" + ) + })?; + let coin_indices_signatures = persistent_storage + .get_coin_index_signatures(epoch_id) + .await? + .ok_or_else(|| { + anyhow!("ticketbook got incorrectly imported - the coin index signatures are missing") + })?; + + let ticketbook_data = next_ticketbook.ticketbook.pack(); + + let next_ticket = args + .ticket_index + .unwrap_or(next_ticketbook.ticketbook.spent_tickets()); + let pay_info = next_ticketbook.ticketbook.generate_pay_info(provider_arr); + + println!("{}", "TICKETBOOK DATA:".bold()); + println!("{}", bs58::encode(&ticketbook_data.data).into_string()); + println!(); + + // display it only for a single ticket + if !args.full { + println!("attempting to generate payment for ticket {next_ticket}..."); + println!(); + next_ticketbook.ticketbook.update_spent_tickets(next_ticket); + + let req = next_ticketbook.ticketbook.prepare_for_spending( + &verification_key, + pay_info.into(), + &coin_indices_signatures, + &expiration_signatures, + 1, + )?; + + let payment = req.payment; + + println!("{}", format!("PAYMENT FOR TICKET {next_ticket}: ").bold()); + println!("{}", bs58::encode(&payment.to_bytes()).into_string()); + return Ok(()); + } + + println!( + "generating payment information for {} tickets. this might take a while!...", + next_ticketbook.ticketbook.params_total_tickets() + ); + + // otherwise generate all the payments + let last_spent = next_ticketbook.ticketbook.spent_tickets(); + + let mut table = Table::new(); + table.set_header(vec!["index", "binary data", "spend status"]); + + for i in 0..next_ticketbook.ticketbook.params_total_tickets() { + let status = if i < last_spent { + "SPENT".red() + } else { + "NOT SPENT".green() + }; + + next_ticketbook.ticketbook.update_spent_tickets(i); + + let req = next_ticketbook.ticketbook.prepare_for_spending( + &verification_key, + pay_info.into(), + &coin_indices_signatures, + &expiration_signatures, + 1, + )?; + + let payment = req.payment; + let payment_bytes = payment.to_bytes(); + let len = payment_bytes.len(); + let display_size = 100; + let remaining = len - display_size; + + table.add_row(vec![ + i.to_string(), + format!( + "{}…{remaining}bytes remaining", + bs58::encode(&payment_bytes[..display_size]).into_string() + ), + status.to_string(), + ]); + } + + println!("{}", "AVAILABLE TICKETS".bold()); + println!("{table}"); + + Ok(()) +} diff --git a/common/commands/src/coconut/import_credential.rs b/common/commands/src/ecash/import_ticket_book.rs similarity index 81% rename from common/commands/src/coconut/import_credential.rs rename to common/commands/src/ecash/import_ticket_book.rs index 852f94e34a..2edf281a04 100644 --- a/common/commands/src/coconut/import_credential.rs +++ b/common/commands/src/ecash/import_ticket_book.rs @@ -9,9 +9,17 @@ use nym_credential_storage::initialise_persistent_storage; use nym_id::import_credential; use std::fs; use std::path::PathBuf; +use std::str::FromStr; -fn parse_encoded_credential_data(raw: &str) -> bs58::decode::Result> { - bs58::decode(raw).into_vec() +#[derive(Debug, Clone)] +pub struct CredentialDataWrapper(Vec); + +impl FromStr for CredentialDataWrapper { + type Err = bs58::decode::Error; + + fn from_str(s: &str) -> Result { + bs58::decode(s).into_vec().map(CredentialDataWrapper) + } } #[derive(Debug, Parser)] @@ -22,8 +30,8 @@ pub struct Args { pub(crate) client_config: PathBuf, /// Explicitly provide the encoded credential data (as base58) - #[clap(long, group = "cred_data", value_parser = parse_encoded_credential_data)] - pub(crate) credential_data: Option>, + #[clap(long, group = "cred_data")] + pub(crate) credential_data: Option, /// Specifies the path to file containing binary credential data #[clap(long, group = "cred_data")] @@ -52,7 +60,7 @@ pub async fn execute(args: Args) -> anyhow::Result<()> { let credentials_store = initialise_persistent_storage(credentials_store).await; let raw_credential = match args.credential_data { - Some(data) => data, + Some(data) => data.0, None => { // SAFETY: one of those arguments must have been set fs::read(args.credential_path.unwrap())? diff --git a/common/commands/src/ecash/issue_ticket_book.rs b/common/commands/src/ecash/issue_ticket_book.rs new file mode 100644 index 0000000000..eb63f7b322 --- /dev/null +++ b/common/commands/src/ecash/issue_ticket_book.rs @@ -0,0 +1,98 @@ +// Copyright 2022-2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use crate::utils::CommonConfigsWrapper; +use anyhow::bail; +use clap::Parser; +use nym_credential_storage::initialise_persistent_storage; +use nym_credential_utils::utils; +use nym_credentials_interface::TicketType; +use nym_crypto::asymmetric::identity; +use rand::rngs::OsRng; +use rand::RngCore; +use std::fs::create_dir_all; +use std::path::PathBuf; + +#[derive(Debug, Parser)] +pub struct Args { + /// Specify which type of ticketbook should be issued + #[clap(long, default_value_t = TicketType::default())] + pub(crate) ticketbook_type: TicketType, + + /// Config file of the client that is supposed to use the credential. + #[clap(long, group = "output")] + pub(crate) client_config: Option, + + /// Path to the dedicated credential storage database + #[clap(long, group = "output")] + pub(crate) credential_storage: Option, +} + +async fn issue_client_ticketbook( + cfg: PathBuf, + typ: TicketType, + client: SigningClient, +) -> anyhow::Result<()> { + let loaded = CommonConfigsWrapper::try_load(cfg)?; + + if let Ok(id) = loaded.try_get_id() { + println!("loaded config file for client '{id}'"); + } + + let Ok(credentials_store) = loaded.try_get_credentials_store() else { + bail!("the loaded config does not have a credentials store information") + }; + + let Ok(private_id_key) = loaded.try_get_private_id_key() else { + bail!("the loaded config does not have a public id key information") + }; + + println!( + "using credentials store at '{}'", + credentials_store.display() + ); + + let persistent_storage = initialise_persistent_storage(credentials_store).await; + let private_id_key: identity::PrivateKey = nym_pemstore::load_key(private_id_key)?; + utils::issue_credential( + &client, + &persistent_storage, + &private_id_key.to_bytes(), + typ, + ) + .await?; + + Ok(()) +} + +async fn issue_standalone_ticketbook( + credentials_store: PathBuf, + typ: TicketType, + client: SigningClient, +) -> anyhow::Result<()> { + println!("attempting to issue a standalone ticketbook"); + + let mut rng = OsRng; + let mut random_seed = [0u8; 32]; + rng.fill_bytes(&mut random_seed); + + if let Some(parent) = credentials_store.parent() { + create_dir_all(parent)?; + } + + let persistent_storage = initialise_persistent_storage(credentials_store).await; + utils::issue_credential(&client, &persistent_storage, &random_seed, typ).await?; + + Ok(()) +} + +pub async fn execute(args: Args, client: SigningClient) -> anyhow::Result<()> { + match (args.client_config, args.credential_storage) { + (Some(cfg), None) => issue_client_ticketbook(cfg, args.ticketbook_type, client).await, + (None, Some(storage)) => { + issue_standalone_ticketbook(storage, args.ticketbook_type, client).await + } + _ => unreachable!("clap should have made this branch impossible to reach!"), + } +} diff --git a/common/commands/src/ecash/mod.rs b/common/commands/src/ecash/mod.rs new file mode 100644 index 0000000000..77ca858366 --- /dev/null +++ b/common/commands/src/ecash/mod.rs @@ -0,0 +1,24 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod generate_ticket; +pub mod import_ticket_book; +pub mod issue_ticket_book; +pub mod recover_ticket_book; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct Ecash { + #[clap(subcommand)] + pub command: EcashCommands, +} + +#[derive(Debug, Subcommand)] +pub enum EcashCommands { + IssueTicketBook(issue_ticket_book::Args), + RecoverTicketBook(recover_ticket_book::Args), + ImportTicketBook(import_ticket_book::Args), + GenerateTicket(generate_ticket::Args), +} diff --git a/common/commands/src/coconut/recover_credentials.rs b/common/commands/src/ecash/recover_ticket_book.rs similarity index 69% rename from common/commands/src/coconut/recover_credentials.rs rename to common/commands/src/ecash/recover_ticket_book.rs index 025ea68c2c..8bd5c8c960 100644 --- a/common/commands/src/coconut/recover_credentials.rs +++ b/common/commands/src/ecash/recover_ticket_book.rs @@ -6,7 +6,7 @@ use crate::utils::CommonConfigsWrapper; use anyhow::bail; use clap::Parser; use nym_credential_storage::initialise_persistent_storage; -use nym_credential_utils::{recovery_storage, utils}; +use nym_credential_utils::utils; use std::path::PathBuf; #[derive(Debug, Parser)] @@ -14,10 +14,6 @@ pub struct Args { /// Config file of the client that is supposed to use the credential. #[clap(long)] pub(crate) client_config: PathBuf, - - /// Path to a directory used to store recovery files for unconsumed deposits - #[clap(long)] - pub(crate) recovery_dir: PathBuf, } pub async fn execute(args: Args, client: QueryClient) -> anyhow::Result<()> { @@ -37,12 +33,9 @@ pub async fn execute(args: Args, client: QueryClient) -> anyhow::Result<()> { ); let persistent_storage = initialise_persistent_storage(credentials_store).await; - let recovery_storage = recovery_storage::RecoveryStorage::new(args.recovery_dir)?; - let recovered = - utils::recover_credentials(&client, &recovery_storage, &persistent_storage).await?; + let recovered = utils::recover_deposits(&client, &persistent_storage).await?; - // TODO: denom? - println!("recovered {recovered} worth of credentials"); + println!("recovered {recovered} ticketbooks"); Ok(()) } diff --git a/common/commands/src/lib.rs b/common/commands/src/lib.rs index afca2be0f7..e3670ac572 100644 --- a/common/commands/src/lib.rs +++ b/common/commands/src/lib.rs @@ -1,7 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -pub mod coconut; pub mod context; +pub mod ecash; pub mod utils; pub mod validator; diff --git a/common/commands/src/utils.rs b/common/commands/src/utils.rs index 25797c71e9..0d6e40ffbc 100644 --- a/common/commands/src/utils.rs +++ b/common/commands/src/utils.rs @@ -123,6 +123,21 @@ impl CommonConfigsWrapper { } } + pub(crate) fn try_get_private_id_key(&self) -> anyhow::Result { + match self { + CommonConfigsWrapper::NymClients(cfg) => Ok(cfg + .storage_paths + .inner + .keys + .private_identity_key_file + .clone()), + CommonConfigsWrapper::NymApi(_cfg) => { + todo!() //SW this will depend on the new network monitor structure. Ping @Drazen + } + CommonConfigsWrapper::Unknown(cfg) => cfg.try_get_private_id_key(), + } + } + pub(crate) fn try_get_credentials_store(&self) -> anyhow::Result { match self { CommonConfigsWrapper::NymClients(cfg) => { @@ -225,4 +240,17 @@ impl UnknownConfigWrapper { bail!("no 'credentials_database_path' field present in the config") } } + + pub(crate) fn try_get_private_id_key(&self) -> anyhow::Result { + let id_val = self + .find_value("keys.private_identity_key_file") + .ok_or_else(|| { + anyhow!("no 'keys.private_identity_key_file' field present in the config") + })?; + if let toml::Value::String(pub_id_key) = id_val { + Ok(pub_id_key.parse()?) + } else { + bail!("no 'keys.private_identity_key_file' field present in the config") + } + } } diff --git a/common/commands/src/validator/cosmwasm/generators/coconut_bandwidth.rs b/common/commands/src/validator/cosmwasm/generators/ecash_bandwidth.rs similarity index 60% rename from common/commands/src/validator/cosmwasm/generators/coconut_bandwidth.rs rename to common/commands/src/validator/cosmwasm/generators/ecash_bandwidth.rs index 3deb1efe01..f13bb65151 100644 --- a/common/commands/src/validator/cosmwasm/generators/coconut_bandwidth.rs +++ b/common/commands/src/validator/cosmwasm/generators/ecash_bandwidth.rs @@ -4,21 +4,25 @@ use std::str::FromStr; use clap::Parser; +use cosmwasm_std::Coin; use log::{debug, info}; -use nym_coconut_bandwidth_contract_common::msg::InstantiateMsg; +use nym_ecash_contract_common::msg::InstantiateMsg; use nym_validator_client::nyxd::AccountId; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub pool_addr: String, + pub group_addr: Option, #[clap(long)] pub multisig_addr: Option, #[clap(long)] - pub mix_denom: Option, + pub holding_account: AccountId, + + #[clap(long, default_value = "75000000unym")] + pub deposit_amount: Coin, } pub async fn generate(args: Args) { @@ -26,21 +30,25 @@ pub async fn generate(args: Args) { debug!("Received arguments: {:?}", args); - let multisig_addr = args.multisig_addr.unwrap_or_else(|| { - let address = std::env::var(nym_network_defaults::var_names::REWARDING_VALIDATOR_ADDRESS) + let group_addr = args.group_addr.unwrap_or_else(|| { + let address = std::env::var(nym_network_defaults::var_names::GROUP_CONTRACT_ADDRESS) .expect("Multisig address has to be set"); AccountId::from_str(address.as_str()) .expect("Failed converting multisig address to AccountId") }); - let mix_denom = args.mix_denom.unwrap_or_else(|| { - std::env::var(nym_network_defaults::var_names::MIX_DENOM).expect("Mix denom has to be set") + let multisig_addr = args.multisig_addr.unwrap_or_else(|| { + let address = std::env::var(nym_network_defaults::var_names::MULTISIG_CONTRACT_ADDRESS) + .expect("Multisig address has to be set"); + AccountId::from_str(address.as_str()) + .expect("Failed converting multisig address to AccountId") }); let instantiate_msg = InstantiateMsg { - pool_addr: args.pool_addr, + holding_account: args.holding_account.to_string(), + group_addr: group_addr.to_string(), multisig_addr: multisig_addr.to_string(), - mix_denom, + deposit_amount: args.deposit_amount, }; debug!("instantiate_msg: {:?}", instantiate_msg); diff --git a/common/commands/src/validator/cosmwasm/generators/mixnet.rs b/common/commands/src/validator/cosmwasm/generators/mixnet.rs index f942aa52bd..afd21fde9a 100644 --- a/common/commands/src/validator/cosmwasm/generators/mixnet.rs +++ b/common/commands/src/validator/cosmwasm/generators/mixnet.rs @@ -1,15 +1,26 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use clap::Parser; -use log::{debug, info}; - use cosmwasm_std::Decimal; -use nym_mixnet_contract_common::{InitialRewardingParams, InstantiateMsg, Percent}; -use nym_validator_client::nyxd::AccountId; +use log::{debug, info}; +use nym_mixnet_contract_common::{ + InitialRewardingParams, InstantiateMsg, OperatingCostRange, Percent, ProfitMarginRange, +}; +use nym_network_defaults::mainnet::MIX_DENOM; +use nym_network_defaults::TOTAL_SUPPLY; +use nym_validator_client::nyxd::{AccountId, Coin}; use std::str::FromStr; use std::time::Duration; +pub fn default_maximum_operating_cost() -> Coin { + Coin::new(TOTAL_SUPPLY, MIX_DENOM.base) +} + +pub fn default_minimum_operating_cost() -> Coin { + Coin::new(0, MIX_DENOM.base) +} + #[derive(Debug, Parser)] pub struct Args { #[clap(long)] @@ -50,6 +61,18 @@ pub struct Args { #[clap(long, default_value_t = 240)] pub active_set_size: u32, + + #[clap(long, default_value_t = Percent::zero())] + pub minimum_profit_margin_percent: Percent, + + #[clap(long, default_value_t = Percent::hundred())] + pub maximum_profit_margin_percent: Percent, + + #[clap(long, default_value_t = default_minimum_operating_cost())] + pub minimum_interval_operating_cost: Coin, + + #[clap(long, default_value_t = default_maximum_operating_cost())] + pub maximum_interval_operating_cost: Coin, } pub async fn generate(args: Args) { @@ -97,6 +120,10 @@ pub async fn generate(args: Args) { .expect("Rewarding (mix) denom has to be set") }); + if args.minimum_interval_operating_cost.denom != args.maximum_interval_operating_cost.denom { + panic!("different denoms for operating cost bounds") + } + let instantiate_msg = InstantiateMsg { rewarding_validator_address: rewarding_validator_address.to_string(), vesting_contract_address: vesting_contract_address.to_string(), @@ -104,6 +131,14 @@ pub async fn generate(args: Args) { epochs_in_interval: args.epochs_in_interval, epoch_duration: Duration::from_secs(args.epoch_duration), initial_rewarding_params, + profit_margin: ProfitMarginRange { + minimum: args.minimum_profit_margin_percent, + maximum: args.maximum_profit_margin_percent, + }, + interval_operating_cost: OperatingCostRange { + minimum: args.minimum_interval_operating_cost.amount.into(), + maximum: args.maximum_interval_operating_cost.amount.into(), + }, }; debug!("instantiate_msg: {:?}", instantiate_msg); diff --git a/common/commands/src/validator/cosmwasm/generators/mod.rs b/common/commands/src/validator/cosmwasm/generators/mod.rs index 85bd5a93e2..8829ec60fe 100644 --- a/common/commands/src/validator/cosmwasm/generators/mod.rs +++ b/common/commands/src/validator/cosmwasm/generators/mod.rs @@ -3,8 +3,8 @@ use clap::{Args, Subcommand}; -pub mod coconut_bandwidth; pub mod coconut_dkg; +pub mod ecash_bandwidth; pub mod mixnet; pub mod multisig; pub mod vesting; @@ -18,7 +18,7 @@ pub struct GenerateMessage { #[derive(Debug, Subcommand)] pub enum GenerateMessageCommands { - CoconutBandwidth(coconut_bandwidth::Args), + EcashBandwidth(ecash_bandwidth::Args), CoconutDKG(coconut_dkg::Args), Mixnet(mixnet::Args), Multisig(multisig::Args), diff --git a/common/commands/src/validator/cosmwasm/generators/multisig.rs b/common/commands/src/validator/cosmwasm/generators/multisig.rs index 37f05a5e71..90abae9e28 100644 --- a/common/commands/src/validator/cosmwasm/generators/multisig.rs +++ b/common/commands/src/validator/cosmwasm/generators/multisig.rs @@ -22,7 +22,7 @@ pub struct Args { pub max_voting_period: u64, #[clap(long)] - pub coconut_bandwidth_contract_address: Option, + pub ecash_contract_address: Option, #[clap(long)] pub coconut_dkg_contract_address: Option, @@ -33,14 +33,12 @@ pub async fn generate(args: Args) { debug!("Received arguments: {:?}", args); - let coconut_bandwidth_contract_address = - args.coconut_bandwidth_contract_address.unwrap_or_else(|| { - let address = - std::env::var(nym_network_defaults::var_names::COCONUT_BANDWIDTH_CONTRACT_ADDRESS) - .expect("Coconut bandwidth contract address has to be set"); - AccountId::from_str(address.as_str()) - .expect("Failed converting bandwidth contract address to AccountId") - }); + let ecash_contract_address = args.ecash_contract_address.unwrap_or_else(|| { + let address = std::env::var(nym_network_defaults::var_names::ECASH_CONTRACT_ADDRESS) + .expect("Coconut bandwidth contract address has to be set"); + AccountId::from_str(address.as_str()) + .expect("Failed converting bandwidth contract address to AccountId") + }); let coconut_dkg_contract_address = args.coconut_dkg_contract_address.unwrap_or_else(|| { let address = std::env::var(nym_network_defaults::var_names::COCONUT_DKG_CONTRACT_ADDRESS) @@ -58,7 +56,7 @@ pub async fn generate(args: Args) { max_voting_period: Duration::Time(args.max_voting_period), executor: None, proposal_deposit: None, - coconut_bandwidth_contract_address: coconut_bandwidth_contract_address.to_string(), + coconut_bandwidth_contract_address: ecash_contract_address.to_string(), coconut_dkg_contract_address: coconut_dkg_contract_address.to_string(), }; diff --git a/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs b/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs new file mode 100644 index 0000000000..8523e63639 --- /dev/null +++ b/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs @@ -0,0 +1,42 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use nym_mixnet_contract_common::MixId; +use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub mix_id: Option, + + #[clap(long)] + pub identity_key: Option, +} + +pub async fn migrate_vested_delegation(args: Args, client: SigningClient) { + let mix_id = match args.mix_id { + Some(mix_id) => mix_id, + None => { + let identity_key = args + .identity_key + .expect("either mix_id or mix_identity has to be specified"); + let node_details = client + .get_mixnode_details_by_identity(identity_key) + .await + .expect("contract query failed") + .mixnode_details + .expect("mixnode with the specified identity doesnt exist"); + node_details.mix_id() + } + }; + + let res = client + .migrate_vested_delegation(mix_id, None) + .await + .expect("failed to migrate delegation!"); + + info!("migration result: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/delegators/mod.rs b/common/commands/src/validator/mixnet/delegators/mod.rs index f2cf156c0c..17a5f9fa5c 100644 --- a/common/commands/src/validator/mixnet/delegators/mod.rs +++ b/common/commands/src/validator/mixnet/delegators/mod.rs @@ -7,6 +7,7 @@ pub mod rewards; pub mod delegate_to_mixnode; pub mod delegate_to_multiple_mixnodes; +pub mod migrate_vested_delegation; pub mod query_for_delegations; pub mod undelegate_from_mixnode; pub mod vesting_delegate_to_mixnode; @@ -35,4 +36,6 @@ pub enum MixnetDelegatorsCommands { DelegateVesting(vesting_delegate_to_mixnode::Args), /// Undelegate from a mixnode (when originally using locked tokens) UndelegateVesting(vesting_undelegate_from_mixnode::Args), + /// Migrate the delegation to use liquid tokens + MigrateVestedDelegation(migrate_vested_delegation::Args), } diff --git a/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs b/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs index f29dae6db3..b1a223b603 100644 --- a/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs +++ b/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs @@ -96,6 +96,7 @@ async fn print_delegation_events(events: Vec, client: &Signin mix_id, amount, proxy, + .. } => { if owner.as_str() == client.nyxd.address().as_ref() { table.add_row(vec![ @@ -111,6 +112,7 @@ async fn print_delegation_events(events: Vec, client: &Signin owner, mix_id, proxy, + .. } => { if owner.as_str() == client.nyxd.address().as_ref() { table.add_row(vec![ diff --git a/common/commands/src/validator/mixnet/operators/gateway/gateway_bonding_sign_payload.rs b/common/commands/src/validator/mixnet/operators/gateway/gateway_bonding_sign_payload.rs index 3845a6c7ef..0747dcac3c 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/gateway_bonding_sign_payload.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/gateway_bonding_sign_payload.rs @@ -6,9 +6,9 @@ use crate::utils::{account_id_to_cw_addr, DataWrapper}; use clap::Parser; use cosmwasm_std::Coin; use nym_bin_common::output_format::OutputFormat; -use nym_mixnet_contract_common::construct_gateway_bonding_sign_payload; +use nym_mixnet_contract_common::construct_legacy_gateway_bonding_sign_payload; use nym_network_defaults::{DEFAULT_CLIENT_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT}; -use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, NymContractsProvider}; +use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; #[derive(Debug, Parser)] pub struct Args { @@ -39,10 +39,6 @@ pub struct Args { )] pub amount: u128, - /// Indicates whether the gateway is going to get bonded via a vesting account - #[arg(long)] - pub with_vesting_account: bool, - #[clap(short, long, default_value_t = OutputFormat::default())] output: OutputFormat, } @@ -74,15 +70,8 @@ pub async fn create_payload(args: Args, client: SigningClient) { }; let address = account_id_to_cw_addr(&client.address()); - let proxy = if args.with_vesting_account { - Some(account_id_to_cw_addr( - client.vesting_contract_address().unwrap(), - )) - } else { - None - }; - let payload = construct_gateway_bonding_sign_payload(nonce, address, proxy, coin, gateway); + let payload = construct_legacy_gateway_bonding_sign_payload(nonce, address, coin, gateway); let wrapper = DataWrapper::new(payload.to_base58_string().unwrap()); println!("{}", args.output.format(&wrapper)) } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/create_family.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/create_family.rs index 6acaca80d2..6e2c664a35 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/create_family.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/families/create_family.rs @@ -5,33 +5,21 @@ use crate::context::SigningClient; use clap::Parser; use log::info; use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; -use nym_validator_client::nyxd::contract_traits::VestingSigningClient; #[derive(Debug, Parser)] pub struct Args { /// Label that is going to be used for creating the family #[arg(long)] pub family_label: String, - - /// Indicates whether the family is going to get created via a vesting account - #[arg(long)] - pub with_vesting_account: bool, } pub async fn create_family(args: Args, client: SigningClient) { info!("Create family"); - let res = if args.with_vesting_account { - client - .vesting_create_family(args.family_label, None) - .await - .expect("failed to create family with vesting account") - } else { - client - .create_family(args.family_label, None) - .await - .expect("failed to create family") - }; + let res = client + .create_family(args.family_label, None) + .await + .expect("failed to create family"); info!("Family creation result: {:?}", res); } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/create_family_join_permit_sign_payload.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/create_family_join_permit_sign_payload.rs index c81dc35f69..ac397150ea 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/create_family_join_permit_sign_payload.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/families/create_family_join_permit_sign_payload.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::context::QueryClient; -use crate::utils::{account_id_to_cw_addr, DataWrapper}; +use crate::utils::DataWrapper; use clap::Parser; use cosmrs::AccountId; use log::info; @@ -10,7 +10,7 @@ use nym_bin_common::output_format::OutputFormat; use nym_crypto::asymmetric::identity; use nym_mixnet_contract_common::construct_family_join_permit; use nym_mixnet_contract_common::families::FamilyHead; -use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, NymContractsProvider}; +use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; #[derive(Debug, Parser)] pub struct Args { @@ -18,10 +18,6 @@ pub struct Args { #[arg(long)] pub address: AccountId, - /// Indicates whether the member joining the family is going to use the vesting account for joining. - #[arg(long)] - pub with_vesting_account: bool, - // might as well validate the value when parsing the arguments /// Identity of the member for whom we're issuing the permit #[arg(long)] @@ -68,18 +64,9 @@ pub async fn create_family_join_permit_sign_payload(args: Args, client: QueryCli } }; - // let address = account_id_to_cw_addr(&args.address); - let proxy = if args.with_vesting_account { - Some(account_id_to_cw_addr( - client.vesting_contract_address().unwrap(), - )) - } else { - None - }; - let head = FamilyHead::new(mixnode.bond_information.identity()); - let payload = construct_family_join_permit(nonce, head, proxy, args.member.to_base58_string()); + let payload = construct_family_join_permit(nonce, head, args.member.to_base58_string()); let wrapper = DataWrapper::new(payload.to_base58_string().unwrap()); println!("{}", args.output.format(&wrapper)) } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/join_family.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/join_family.rs index 411a8412b7..08b4c471c0 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/join_family.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/families/join_family.rs @@ -8,7 +8,6 @@ use nym_contracts_common::signing::MessageSignature; use nym_crypto::asymmetric::identity; use nym_mixnet_contract_common::families::FamilyHead; use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; -use nym_validator_client::nyxd::contract_traits::VestingSigningClient; #[derive(Debug, Parser)] pub struct Args { @@ -16,10 +15,6 @@ pub struct Args { #[arg(long)] pub family_head: identity::PublicKey, - /// Indicates whether the member joining the family is going to do so via the vesting contract - #[arg(long)] - pub with_vesting_account: bool, - /// Permission, as provided by the family head, for joining the family #[arg(long)] pub join_permit: MessageSignature, @@ -30,17 +25,10 @@ pub async fn join_family(args: Args, client: SigningClient) { let family_head = FamilyHead::new(args.family_head.to_base58_string()); - let res = if args.with_vesting_account { - client - .vesting_join_family(args.join_permit, family_head, None) - .await - .expect("failed to join family with vesting account") - } else { - client - .join_family(args.join_permit, family_head, None) - .await - .expect("failed to join family") - }; + let res = client + .join_family(args.join_permit, family_head, None) + .await + .expect("failed to join family"); info!("Family join result: {:?}", res); } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/leave_family.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/leave_family.rs index 0673be508a..d9c31e3933 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/leave_family.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/families/leave_family.rs @@ -7,17 +7,12 @@ use log::info; use nym_crypto::asymmetric::identity; use nym_mixnet_contract_common::families::FamilyHead; use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; -use nym_validator_client::nyxd::contract_traits::VestingSigningClient; #[derive(Debug, Parser)] pub struct Args { /// The head of the family that we intend to leave #[arg(long)] pub family_head: identity::PublicKey, - - /// Indicates whether we joined the family via the vesting contract - #[arg(long)] - pub with_vesting_account: bool, } pub async fn leave_family(args: Args, client: SigningClient) { @@ -25,17 +20,10 @@ pub async fn leave_family(args: Args, client: SigningClient) { let family_head = FamilyHead::new(args.family_head.to_base58_string()); - let res = if args.with_vesting_account { - client - .vesting_leave_family(family_head, None) - .await - .expect("failed to leave family with vesting account") - } else { - client - .leave_family(family_head, None) - .await - .expect("failed to leave family") - }; + let res = client + .leave_family(family_head, None) + .await + .expect("failed to leave family"); info!("Family leave result: {:?}", res); } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/keys/decode_mixnode_key.rs b/common/commands/src/validator/mixnet/operators/mixnode/keys/decode_mixnode_key.rs index 21b026c85e..fc16a067d2 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/keys/decode_mixnode_key.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/keys/decode_mixnode_key.rs @@ -10,7 +10,11 @@ pub struct Args { } pub fn decode_mixnode_key(args: Args) { - let b64_decoded = base64::decode(args.key).expect("failed to decode base64 string"); + use base64::{engine::general_purpose::STANDARD, Engine as _}; + + let b64_decoded = STANDARD + .decode(args.key) + .expect("failed to decode base64 string"); let b58_encoded = bs58::encode(&b64_decoded).into_string(); println!("{b58_encoded}") diff --git a/common/commands/src/validator/mixnet/operators/mixnode/migrate_vested_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/migrate_vested_mixnode.rs new file mode 100644 index 0000000000..95bd9d0573 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/migrate_vested_mixnode.rs @@ -0,0 +1,19 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; + +#[derive(Debug, Parser)] +pub struct Args {} + +pub async fn migrate_vested_mixnode(_args: Args, client: SigningClient) { + let res = client + .migrate_vested_mixnode(None) + .await + .expect("failed to migrate mixnode!"); + + info!("migration result: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/mixnode_bonding_sign_payload.rs b/common/commands/src/validator/mixnet/operators/mixnode/mixnode_bonding_sign_payload.rs index 332de7614e..3eda67a2e9 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/mixnode_bonding_sign_payload.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/mixnode_bonding_sign_payload.rs @@ -7,11 +7,13 @@ use clap::Parser; use cosmwasm_std::{Coin, Uint128}; use nym_bin_common::output_format::OutputFormat; use nym_contracts_common::Percent; -use nym_mixnet_contract_common::{construct_mixnode_bonding_sign_payload, MixNodeCostParams}; +use nym_mixnet_contract_common::{ + construct_legacy_mixnode_bonding_sign_payload, MixNodeCostParams, +}; use nym_network_defaults::{ DEFAULT_HTTP_API_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT, }; -use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, NymContractsProvider}; +use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; use nym_validator_client::nyxd::CosmWasmCoin; #[derive(Debug, Parser)] @@ -52,10 +54,6 @@ pub struct Args { )] pub amount: u128, - /// Indicates whether the mixnode is going to get bonded via a vesting account - #[arg(long)] - pub with_vesting_account: bool, - #[clap(short, long, default_value_t = OutputFormat::default())] output: OutputFormat, } @@ -100,16 +98,9 @@ pub async fn create_payload(args: Args, client: SigningClient) { }; let address = account_id_to_cw_addr(&client.address()); - let proxy = if args.with_vesting_account { - Some(account_id_to_cw_addr( - client.vesting_contract_address().unwrap(), - )) - } else { - None - }; let payload = - construct_mixnode_bonding_sign_payload(nonce, address, proxy, coin, mixnode, cost_params); + construct_legacy_mixnode_bonding_sign_payload(nonce, address, coin, mixnode, cost_params); let wrapper = DataWrapper::new(payload.to_base58_string().unwrap()); println!("{}", args.output.format(&wrapper)) } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/mod.rs b/common/commands/src/validator/mixnet/operators/mixnode/mod.rs index 6e5283d77e..abb5060e9b 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/mod.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/mod.rs @@ -7,6 +7,7 @@ pub mod bond_mixnode; pub mod decrease_pledge; pub mod families; pub mod keys; +pub mod migrate_vested_mixnode; pub mod mixnode_bonding_sign_payload; pub mod pledge_more; pub mod rewards; @@ -52,4 +53,6 @@ pub enum MixnetOperatorsMixnodeCommands { DecreasePledge(decrease_pledge::Args), /// Decrease pledge with locked tokens DecreasePledgeVesting(vesting_decrease_pledge::Args), + /// Migrate the mixnode to use liquid tokens + MigrateVestedNode(migrate_vested_mixnode::Args), } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/settings/update_cost_params.rs b/common/commands/src/validator/mixnet/operators/mixnode/settings/update_cost_params.rs index 0616a81d33..7c09ba2946 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/settings/update_cost_params.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/settings/update_cost_params.rs @@ -6,14 +6,14 @@ use clap::Parser; use cosmwasm_std::Uint128; use log::info; use nym_mixnet_contract_common::{MixNodeCostParams, Percent}; -use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; +use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; use nym_validator_client::nyxd::CosmWasmCoin; #[derive(Debug, Parser)] pub struct Args { #[clap( long, - help = "input your profit margin as follows; (so it would be 10, rather than 0.1)" + help = "input your profit margin as follows; (so it would be 20, rather than 0.2)" )] pub profit_margin_percent: Option, @@ -27,11 +27,48 @@ pub struct Args { pub async fn update_cost_params(args: Args, client: SigningClient) { let denom = client.current_chain_details().mix_denom.base.as_str(); + fn convert_to_percent(value: u64) -> Percent { + Percent::from_percentage_value(value).expect("Invalid value") + } + + let default_profit_margin: Percent = convert_to_percent(20); + + let mixownership_response = match client.get_owned_mixnode(&client.address()).await { + Ok(response) => response, + Err(_) => { + eprintln!("Failed to obtain owned mixnode"); + return; + } + }; + + let mix_id = match mixownership_response.mixnode_details { + Some(details) => details.bond_information.mix_id, + None => { + eprintln!("Failed to obtain mixnode details"); + return; + } + }; + + let rewarding_response = match client.get_mixnode_rewarding_details(mix_id).await { + Ok(details) => details, + Err(_) => { + eprintln!("Failed to obtain rewarding details"); + return; + } + }; + + let profit_margin_percent = rewarding_response + .rewarding_details + .map(|rd| rd.cost_params.profit_margin_percent) + .unwrap_or(default_profit_margin); + + let profit_margin_value = args + .profit_margin_percent + .map(|pm| convert_to_percent(pm as u64)) + .unwrap_or(profit_margin_percent); + let cost_params = MixNodeCostParams { - profit_margin_percent: Percent::from_percentage_value( - args.profit_margin_percent.unwrap_or(10) as u64, - ) - .unwrap(), + profit_margin_percent: profit_margin_value, interval_operating_cost: CosmWasmCoin { denom: denom.into(), amount: Uint128::new(args.interval_operating_cost.unwrap_or(40_000_000)), diff --git a/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/events.rs b/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/events.rs index 450ecd2543..5204f6a97e 100644 --- a/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/events.rs +++ b/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/events.rs @@ -4,9 +4,6 @@ // event types pub const DEPOSITED_FUNDS_EVENT_TYPE: &str = "deposited-funds"; -// a 'wasm-' prefix is added to all cosmwasm events -pub const COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE: &str = "wasm-deposited-funds"; - // attributes that are used in multiple places pub const DEPOSIT_VALUE: &str = "deposit-value"; pub const DEPOSIT_INFO: &str = "deposit-info"; diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs index 0b3da8a552..f9d1dc0bb0 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs @@ -87,6 +87,9 @@ pub enum QueryMsg { #[cfg_attr(feature = "schema", returns(u64))] GetCurrentEpochThreshold {}, + #[cfg_attr(feature = "schema", returns(u64))] + GetEpochThreshold { epoch_id: EpochId }, + #[cfg_attr(feature = "schema", returns(StateAdvanceResponse))] CanAdvanceState {}, diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/events.rs b/common/cosmwasm-smart-contracts/contracts-common/src/events.rs index 39d5c0007a..a8fdf9b816 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/events.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/events.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use cosmwasm_std::Event; +use std::str::FromStr; /// Looks up value of particular attribute in the provided event. If it fails to find it, /// the function panics. @@ -31,6 +32,23 @@ pub fn may_find_attribute(event: &Event, key: &str) -> Option { None } +pub fn try_find_attribute( + events: &[Event], + event_name: &str, + key: &str, +) -> Option> +where + T: FromStr, +{ + for event in events { + if event.ty == event_name { + let value = may_find_attribute(event, key)?; + return Some(value.parse()); + } + } + None +} + pub trait OptionallyAddAttribute { fn add_optional_attribute( self, diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/signing/mod.rs b/common/cosmwasm-smart-contracts/contracts-common/src/signing/mod.rs index 23018c9249..228e188b84 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/signing/mod.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/signing/mod.rs @@ -218,7 +218,6 @@ where #[derive(Serialize)] pub struct ContractMessageContent { pub sender: Addr, - pub proxy: Option, pub funds: Vec, pub data: T, } @@ -233,27 +232,47 @@ where } impl ContractMessageContent { - pub fn new(sender: Addr, proxy: Option, funds: Vec, data: T) -> Self { + pub fn new(sender: Addr, funds: Vec, data: T) -> Self { ContractMessageContent { sender, - proxy, funds, data, } } pub fn new_with_info(info: MessageInfo, signer: Addr, data: T) -> Self { - let proxy = if info.sender == signer { - None - } else { - Some(info.sender) - }; - ContractMessageContent { sender: signer, - proxy, funds: info.funds, data, } } } + +impl From> for LegacyContractMessageContent { + fn from(value: ContractMessageContent) -> Self { + LegacyContractMessageContent { + sender: value.sender, + proxy: None, + funds: value.funds, + data: value.data, + } + } +} + +#[derive(Serialize)] +pub struct LegacyContractMessageContent { + pub sender: Addr, + pub proxy: Option, + pub funds: Vec, + pub data: T, +} + +impl SigningPurpose for LegacyContractMessageContent +where + T: SigningPurpose, +{ + fn message_type() -> MessageType { + T::message_type() + } +} diff --git a/common/cosmwasm-smart-contracts/ecash-contract/Cargo.toml b/common/cosmwasm-smart-contracts/ecash-contract/Cargo.toml new file mode 100644 index 0000000000..54bc53fe18 --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "nym-ecash-contract-common" +version = "0.1.0" +edition = "2021" +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bs58.workspace = true +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw2 = { workspace = true, optional = true } +nym-multisig-contract-common = { path = "../multisig-contract" } +thiserror.workspace = true +cw-utils = { workspace = true } +cw-controllers = { workspace = true } + + +[features] +schema = ["cw2"] diff --git a/common/cosmwasm-smart-contracts/ecash-contract/src/blacklist.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/blacklist.rs new file mode 100644 index 0000000000..00b6dfc6ae --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/src/blacklist.rs @@ -0,0 +1,71 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_schema::cw_serde; + +#[cw_serde] +pub struct BlacklistedAccount { + pub public_key: String, + pub info: Blacklisting, +} + +impl From<(String, Blacklisting)> for BlacklistedAccount { + fn from((public_key, info): (String, Blacklisting)) -> Self { + BlacklistedAccount { public_key, info } + } +} + +#[cw_serde] +pub struct Blacklisting { + pub proposal_id: u64, + pub finalized_at_height: Option, +} + +impl Blacklisting { + pub fn new(proposal_id: u64) -> Self { + Blacklisting { + proposal_id, + finalized_at_height: None, + } + } +} + +impl BlacklistedAccount { + pub fn public_key(&self) -> &str { + &self.public_key + } +} + +#[cw_serde] +pub struct PagedBlacklistedAccountResponse { + pub accounts: Vec, + pub per_page: usize, + + /// Field indicating paging information for the following queries if the caller wishes to get further entries. + pub start_next_after: Option, +} + +impl PagedBlacklistedAccountResponse { + pub fn new( + accounts: Vec, + per_page: usize, + start_next_after: Option, + ) -> Self { + PagedBlacklistedAccountResponse { + accounts, + per_page, + start_next_after, + } + } +} + +#[cw_serde] +pub struct BlacklistedAccountResponse { + pub account: Option, +} + +impl BlacklistedAccountResponse { + pub fn new(account: Option) -> Self { + BlacklistedAccountResponse { account } + } +} diff --git a/common/cosmwasm-smart-contracts/ecash-contract/src/counters.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/counters.rs new file mode 100644 index 0000000000..e3b9d01752 --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/src/counters.rs @@ -0,0 +1,11 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Coin; + +#[cw_serde] +pub struct PoolCounters { + pub total_deposited: Coin, + pub total_redeemed: Coin, +} diff --git a/common/cosmwasm-smart-contracts/ecash-contract/src/deposit.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/deposit.rs new file mode 100644 index 0000000000..86efdbd5fa --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/src/deposit.rs @@ -0,0 +1,76 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::EcashContractError; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{StdError, StdResult}; + +pub type DepositId = u32; + +#[cw_serde] +pub struct Deposit { + pub bs58_encoded_ed25519_pubkey: String, +} + +impl Deposit { + pub fn new(bs58_encoded_ed25519_pubkey: String) -> Self { + Deposit { + bs58_encoded_ed25519_pubkey, + } + } + + pub fn get_ed25519_pubkey_bytes(raw: &str) -> Result<[u8; 32], EcashContractError> { + let mut ed25519_pubkey_bytes = [0u8; 32]; + bs58::decode(raw) + .onto(&mut ed25519_pubkey_bytes) + .map_err(|_| EcashContractError::MalformedEd25519Identity)?; + + Ok(ed25519_pubkey_bytes) + } + + pub fn encode_pubkey_bytes(raw: &[u8]) -> String { + bs58::encode(raw).into_string() + } + + pub fn to_bytes(&self) -> Result<[u8; 32], EcashContractError> { + Self::get_ed25519_pubkey_bytes(&self.bs58_encoded_ed25519_pubkey) + } + + pub fn try_from_bytes(bytes: &[u8]) -> StdResult { + if bytes.len() != 32 { + return Err(StdError::generic_err("malformed deposit data")); + } + + Ok(Deposit { + bs58_encoded_ed25519_pubkey: Self::encode_pubkey_bytes(bytes), + }) + } +} + +#[cw_serde] +pub struct DepositResponse { + pub id: DepositId, + + pub deposit: Option, +} + +#[cw_serde] +pub struct DepositData { + pub id: DepositId, + + pub deposit: Deposit, +} + +impl From<(DepositId, Deposit)> for DepositData { + fn from((id, deposit): (DepositId, Deposit)) -> Self { + DepositData { id, deposit } + } +} + +#[cw_serde] +pub struct PagedDepositsResponse { + pub deposits: Vec, + + /// Field indicating paging information for the following queries if the caller wishes to get further entries. + pub start_next_after: Option, +} diff --git a/common/cosmwasm-smart-contracts/ecash-contract/src/error.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/error.rs new file mode 100644 index 0000000000..3aec9dd6e7 --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/src/error.rs @@ -0,0 +1,68 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{Coin, StdError}; +use cw_controllers::AdminError; +use cw_utils::PaymentError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum EcashContractError { + #[error(transparent)] + Std(#[from] StdError), + + #[error("Invalid deposit")] + InvalidDeposit(#[from] PaymentError), + + #[error("received wrong amount for deposit. got: {received}. required: {amount}")] + WrongAmount { received: Coin, amount: Coin }, + + #[error("There aren't enough funds in the contract")] + NotEnoughFunds, + + #[error(transparent)] + Admin(#[from] AdminError), + + #[error("could not find proposal id inside the multisig reply SubMsg")] + MissingProposalId, + + // realistically this should NEVER be thrown + #[error("the proposal id returned by the multisig contract could not be parsed into an u64")] + MalformedProposalId, + + #[error("Group contract invalid address '{addr}'")] + InvalidGroup { addr: String }, + + #[error("Unauthorized")] + Unauthorized, + + #[error("Failed to parse {value} into a valid SemVer version: {error_message}")] + SemVerFailure { + value: String, + error_message: String, + }, + + #[error("received an invalid reply id: {id}. it does not correspond to any sent SubMsg")] + InvalidReplyId { id: u64 }, + + #[error("reached the maximum of 255 different deposit types")] + MaximumDepositTypesReached, + + #[error("compressed deposit info {typ} does not corresponds to any known type")] + UnknownCompressedDepositInfoType { typ: u8 }, + + #[error("deposit info {typ} does not corresponds to any previously seen type")] + UnknownDepositInfoType { typ: String }, + + #[error("the provided ed25519 identity was malformed")] + MalformedEd25519Identity, + + #[error("the ticket book size has changed since the contract was created! This was not expected! It used to be {at_init} but it's {current} now! Please let the developers know ASAP!")] + TicketBookSizeChanged { at_init: u64, current: u64 }, + + #[error("the provided tickets redemption commitment is malformed")] + MalformedRedemptionCommitment, + + #[error("the account blacklisting hasn't been fully implemented yet")] + UnimplementedBlacklisting, +} diff --git a/common/cosmwasm-smart-contracts/ecash-contract/src/event_attributes.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/event_attributes.rs new file mode 100644 index 0000000000..80f5daf68a --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-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/ecash-contract/src/events.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/events.rs new file mode 100644 index 0000000000..a1f243b151 --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/src/events.rs @@ -0,0 +1,9 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// event types +pub const DEPOSITED_FUNDS_EVENT_TYPE: &str = "deposited-funds"; + +pub const DEPOSIT_ID: &str = "deposit-id"; +pub const WASM_EVENT_NAME: &str = "wasm"; +pub const PROPOSAL_ID_ATTRIBUTE_NAME: &str = "proposal_id"; diff --git a/common/cosmwasm-smart-contracts/ecash-contract/src/lib.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/lib.rs new file mode 100644 index 0000000000..3c05433349 --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/src/lib.rs @@ -0,0 +1,13 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod blacklist; +pub mod counters; +pub mod deposit; +pub mod error; +pub mod event_attributes; +pub mod events; +pub mod msg; +pub mod redeem_credential; + +pub use error::EcashContractError; diff --git a/common/cosmwasm-smart-contracts/ecash-contract/src/msg.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/msg.rs new file mode 100644 index 0000000000..43bd4ca823 --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/src/msg.rs @@ -0,0 +1,84 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Coin; + +#[cfg(feature = "schema")] +use crate::blacklist::{BlacklistedAccountResponse, PagedBlacklistedAccountResponse}; +#[cfg(feature = "schema")] +use crate::deposit::{DepositResponse, PagedDepositsResponse}; +#[cfg(feature = "schema")] +use cosmwasm_schema::QueryResponses; + +#[cw_serde] +pub struct InstantiateMsg { + pub holding_account: String, + pub multisig_addr: String, + pub group_addr: String, + pub deposit_amount: Coin, +} + +#[cw_serde] +pub enum ExecuteMsg { + /// Used by clients to request ticket books from the signers + DepositTicketBookFunds { + identity_key: String, + }, + + /// Used by gateways to batch redeem tokens from the spent tickets + RequestRedemption { + commitment_bs58: String, + number_of_tickets: u16, + }, + + /// The actual message that gets executed, after multisig votes, that transfers the ticket tokens into gateway's (and the holding) account + RedeemTickets { + n: u16, + gw: String, + }, + + UpdateAdmin { + admin: String, + }, + + UpdateDepositValue { + new_deposit: Coin, + }, + + // TODO: properly implement + ProposeToBlacklist { + public_key: String, + }, + AddToBlacklist { + public_key: String, + }, +} + +#[cw_serde] +#[cfg_attr(feature = "schema", derive(QueryResponses))] +pub enum QueryMsg { + #[cfg_attr(feature = "schema", returns(BlacklistedAccountResponse))] + GetBlacklistedAccount { public_key: String }, + + #[cfg_attr(feature = "schema", returns(PagedBlacklistedAccountResponse))] + GetBlacklistPaged { + limit: Option, + start_after: Option, + }, + + #[cfg_attr(feature = "schema", returns(Coin))] + GetRequiredDepositAmount {}, + + #[cfg_attr(feature = "schema", returns(DepositResponse))] + GetDeposit { deposit_id: u32 }, + + #[cfg_attr(feature = "schema", returns(PagedDepositsResponse))] + GetDepositsPaged { + limit: Option, + start_after: Option, + }, +} + +#[cw_serde] +pub struct MigrateMsg {} diff --git a/common/cosmwasm-smart-contracts/ecash-contract/src/redeem_credential.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/redeem_credential.rs new file mode 100644 index 0000000000..76bf86f5a8 --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/src/redeem_credential.rs @@ -0,0 +1,5 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// TODO: to be moved to multisig +pub const BATCH_REDEMPTION_PROPOSAL_TITLE: &str = "ecash-redemption"; diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml index 31e422a49c..8968f1c8d6 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml @@ -12,6 +12,7 @@ repository = { workspace = true } bs58 = "0.4.0" cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } +cw-controllers = { workspace = true } cw2 = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } serde_repr = { workspace = true } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs index 3a7cdd7702..f30dc63a73 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs @@ -65,7 +65,6 @@ impl Delegation { cumulative_reward_ratio: Decimal, amount: Coin, height: u64, - proxy: Option, ) -> Self { assert!( amount.amount <= TOKEN_SUPPLY, @@ -78,7 +77,7 @@ impl Delegation { cumulative_reward_ratio, amount, height, - proxy, + proxy: None, } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs index ec6e79c65f..12f2030acc 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs @@ -1,9 +1,11 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{EpochEventId, EpochState, IdentityKey, MixId}; +use crate::{EpochEventId, EpochState, IdentityKey, MixId, OperatingCostRange, ProfitMarginRange}; use contracts_common::signing::verifier::ApiVerifierError; +use contracts_common::Percent; use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; +use cw_controllers::AdminError; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -11,6 +13,9 @@ pub enum MixnetContractError { #[error("could not perform contract migration: {comment}")] FailedMigration { comment: String }, + #[error(transparent)] + Admin(#[from] AdminError), + #[error("{source}")] StdErr { #[from] @@ -76,21 +81,11 @@ pub enum MixnetContractError { #[error("Received multiple coin types during staking")] MultipleDenoms, - #[error("Proxy address mismatch, expected {existing}, got {incoming}")] - ProxyMismatch { existing: String, incoming: String }, - #[error("Proxy address ({received}) is not set to the vesting contract ({vesting_contract})")] ProxyIsNotVestingContract { received: Addr, vesting_contract: Addr, }, - #[error( - "Sender of this message ({received}) is not the vesting contract ({vesting_contract})" - )] - SenderIsNotVestingContract { - received: Addr, - vesting_contract: Addr, - }, #[error("Failed to recover ed25519 public key from its base58 representation - {0}")] MalformedEd25519IdentityKey(String), @@ -239,6 +234,30 @@ pub enum MixnetContractError { #[from] source: ApiVerifierError, }, + + #[error("this operation is no longer allowed to be performed with vesting tokens. please move them to your liquid balance and try again")] + DisabledVestingOperation, + + #[error( + "this mixnode has not been bonded with the vesting tokens or has already been migrated" + )] + NotAVestingMixnode, + + #[error("this delegation has not been performed with the vesting tokens or has already been migrated")] + NotAVestingDelegation, + + #[error("the provided profit margin ({provided}) is outside the allowed range: {range}")] + ProfitMarginOutsideRange { + provided: Percent, + range: ProfitMarginRange, + }, + + #[error("the provided interval operating cost ({provided}{denom}) is outside the allowed range: {range}")] + OperatingCostOutsideRange { + denom: String, + provided: Uint128, + range: OperatingCostRange, + }, } impl MixnetContractError { diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs index 365adf9df3..7ebb604225 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs @@ -103,7 +103,6 @@ impl Display for MixnetEventType { // attributes that are used in multiple places pub const OWNER_KEY: &str = "owner"; pub const AMOUNT_KEY: &str = "amount"; -pub const PROXY_KEY: &str = "proxy"; // event-specific attributes @@ -163,7 +162,6 @@ pub const NEW_EPOCHS_IN_INTERVAL: &str = "new_epochs_in_interval"; pub fn new_delegation_event( created_at: BlockHeight, delegator: &Addr, - proxy: &Option, amount: &Coin, mix_id: MixId, unit_reward: Decimal, @@ -171,58 +169,34 @@ pub fn new_delegation_event( Event::new(MixnetEventType::Delegation) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) .add_attribute(DELEGATOR_KEY, delegator) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(AMOUNT_KEY, amount.to_string()) .add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string()) .add_attribute(UNIT_REWARD_KEY, unit_reward.to_string()) } -pub fn new_delegation_on_unbonded_node_event( - delegator: &Addr, - proxy: &Option, - mix_id: MixId, -) -> Event { +pub fn new_delegation_on_unbonded_node_event(delegator: &Addr, mix_id: MixId) -> Event { Event::new(MixnetEventType::Delegation) .add_attribute(DELEGATOR_KEY, delegator) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string()) } -pub fn new_pending_delegation_event( - delegator: &Addr, - proxy: &Option, - amount: &Coin, - mix_id: MixId, -) -> Event { +pub fn new_pending_delegation_event(delegator: &Addr, amount: &Coin, mix_id: MixId) -> Event { Event::new(MixnetEventType::PendingDelegation) .add_attribute(DELEGATOR_KEY, delegator) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(AMOUNT_KEY, amount.to_string()) .add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string()) } -pub fn new_withdraw_operator_reward_event( - owner: &Addr, - proxy: &Option, - amount: Coin, - mix_id: MixId, -) -> Event { +pub fn new_withdraw_operator_reward_event(owner: &Addr, amount: Coin, mix_id: MixId) -> Event { Event::new(MixnetEventType::WithdrawOperatorReward) .add_attribute(OWNER_KEY, owner.as_str()) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(AMOUNT_KEY, amount.to_string()) .add_attribute(MIX_ID_KEY, mix_id.to_string()) } -pub fn new_withdraw_delegator_reward_event( - delegator: &Addr, - proxy: &Option, - amount: Coin, - mix_id: MixId, -) -> Event { +pub fn new_withdraw_delegator_reward_event(delegator: &Addr, amount: Coin, mix_id: MixId) -> Event { Event::new(MixnetEventType::WithdrawDelegatorReward) .add_attribute(DELEGATOR_KEY, delegator) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(AMOUNT_KEY, amount.to_string()) .add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string()) } @@ -278,59 +252,43 @@ pub fn new_pending_rewarding_params_update_event( ) } -pub fn new_undelegation_event( - created_at: BlockHeight, - delegator: &Addr, - proxy: &Option, - mix_id: MixId, -) -> Event { +pub fn new_undelegation_event(created_at: BlockHeight, delegator: &Addr, mix_id: MixId) -> Event { Event::new(MixnetEventType::Undelegation) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) .add_attribute(DELEGATOR_KEY, delegator) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(MIX_ID_KEY, mix_id.to_string()) } -pub fn new_pending_undelegation_event( - delegator: &Addr, - proxy: &Option, - mix_id: MixId, -) -> Event { +pub fn new_pending_undelegation_event(delegator: &Addr, mix_id: MixId) -> Event { Event::new(MixnetEventType::PendingUndelegation) .add_attribute(DELEGATOR_KEY, delegator) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(MIX_ID_KEY, mix_id.to_string()) } pub fn new_gateway_bonding_event( owner: &Addr, - proxy: &Option, amount: &Coin, identity: IdentityKeyRef<'_>, ) -> Event { Event::new(MixnetEventType::GatewayBonding) .add_attribute(OWNER_KEY, owner) .add_attribute(NODE_IDENTITY_KEY, identity) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(AMOUNT_KEY, amount.to_string()) } pub fn new_gateway_unbonding_event( owner: &Addr, - proxy: &Option, amount: &Coin, identity: IdentityKeyRef<'_>, ) -> Event { Event::new(MixnetEventType::GatewayUnbonding) .add_attribute(OWNER_KEY, owner) .add_attribute(NODE_IDENTITY_KEY, identity) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(AMOUNT_KEY, amount.to_string()) } pub fn new_mixnode_bonding_event( owner: &Addr, - proxy: &Option, amount: &Coin, identity: IdentityKeyRef<'_>, mix_id: MixId, @@ -341,7 +299,6 @@ pub fn new_mixnode_bonding_event( .add_attribute(MIX_ID_KEY, mix_id.to_string()) .add_attribute(NODE_IDENTITY_KEY, identity) .add_attribute(OWNER_KEY, owner) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(ASSIGNED_LAYER_KEY, assigned_layer) .add_attribute(AMOUNT_KEY, amount.to_string()) } @@ -380,7 +337,6 @@ pub fn new_mixnode_unbonding_event(created_at: BlockHeight, mix_id: MixId) -> Ev pub fn new_pending_mixnode_unbonding_event( owner: &Addr, - proxy: &Option, identity: IdentityKeyRef<'_>, mix_id: MixId, ) -> Event { @@ -388,43 +344,33 @@ pub fn new_pending_mixnode_unbonding_event( .add_attribute(MIX_ID_KEY, mix_id.to_string()) .add_attribute(NODE_IDENTITY_KEY, identity) .add_attribute(OWNER_KEY, owner) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) } pub fn new_mixnode_config_update_event( mix_id: MixId, owner: &Addr, - proxy: &Option, update: &MixNodeConfigUpdate, ) -> Event { Event::new(MixnetEventType::MixnodeConfigUpdate) .add_attribute(MIX_ID_KEY, mix_id.to_string()) .add_attribute(OWNER_KEY, owner) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(UPDATED_MIXNODE_CONFIG_KEY, update.to_inline_json()) } -pub fn new_gateway_config_update_event( - owner: &Addr, - proxy: &Option, - update: &GatewayConfigUpdate, -) -> Event { +pub fn new_gateway_config_update_event(owner: &Addr, update: &GatewayConfigUpdate) -> Event { Event::new(MixnetEventType::GatewayConfigUpdate) .add_attribute(OWNER_KEY, owner) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(UPDATED_GATEWAY_CONFIG_KEY, update.to_inline_json()) } pub fn new_mixnode_pending_cost_params_update_event( mix_id: MixId, owner: &Addr, - proxy: &Option, new_costs: &MixNodeCostParams, ) -> Event { Event::new(MixnetEventType::PendingMixnodeCostParamsUpdate) .add_attribute(MIX_ID_KEY, mix_id.to_string()) .add_attribute(OWNER_KEY, owner) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(UPDATED_MIXNODE_COST_PARAMS_KEY, new_costs.to_inline_json()) } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/families.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/families.rs index 5b44483c83..58f74f4e08 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/families.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/families.rs @@ -3,7 +3,6 @@ use crate::{IdentityKey, IdentityKeyRef}; use cosmwasm_schema::cw_serde; -use cosmwasm_std::Addr; use schemars::JsonSchema; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::fmt::{Display, Formatter}; @@ -84,10 +83,10 @@ impl FamilyHead { } impl Family { - pub fn new(head: FamilyHead, proxy: Option, label: String) -> Self { + pub fn new(head: FamilyHead, label: String) -> Self { Family { head, - proxy: proxy.map(|p| p.to_string()), + proxy: None, label, } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs index fd29090374..ff991853fb 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs @@ -55,19 +55,13 @@ pub struct GatewayBond { } impl GatewayBond { - pub fn new( - pledge_amount: Coin, - owner: Addr, - block_height: u64, - gateway: Gateway, - proxy: Option, - ) -> Self { + pub fn new(pledge_amount: Coin, owner: Addr, block_height: u64, gateway: Gateway) -> Self { GatewayBond { pledge_amount, owner, block_height, gateway, - proxy, + proxy: None, } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index a09d21dd2d..8772763150 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -10,7 +10,10 @@ use crate::helpers::IntoBaseDecimal; use crate::reward_params::{NodeRewardParams, RewardingParams}; use crate::rewarding::helpers::truncate_reward; use crate::rewarding::RewardDistribution; -use crate::{Delegation, EpochEventId, EpochId, IdentityKey, MixId, Percent, SphinxKey}; +use crate::{ + Delegation, EpochEventId, EpochId, IdentityKey, MixId, OperatingCostRange, Percent, + ProfitMarginRange, SphinxKey, +}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128}; use schemars::JsonSchema; @@ -152,6 +155,16 @@ impl MixNodeRewarding { }) } + pub fn normalise_profit_margin(&mut self, allowed_range: ProfitMarginRange) { + self.cost_params.profit_margin_percent = + allowed_range.normalise(self.cost_params.profit_margin_percent) + } + + pub fn normalise_operating_cost(&mut self, allowed_range: OperatingCostRange) { + self.cost_params.interval_operating_cost.amount = + allowed_range.normalise(self.cost_params.interval_operating_cost.amount) + } + /// Determines whether this node is still bonded. This is performed via a simple check, /// if there are no tokens left associated with the operator, it means they have unbonded /// and those params only exist for the purposes of calculating rewards for delegators that @@ -518,7 +531,6 @@ impl MixNodeBond { original_pledge: Coin, layer: Layer, mix_node: MixNode, - proxy: Option, bonding_height: u64, ) -> Self { MixNodeBond { @@ -527,7 +539,7 @@ impl MixNodeBond { original_pledge, layer, mix_node, - proxy, + proxy: None, bonding_height, is_unbonding: false, } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index ca154e231b..863fdc0327 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -1,4 +1,4 @@ -// Copyright 2021-2023 - Nym Technologies SA +// Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::delegation::{self, OwnerProxySubKey}; @@ -12,6 +12,7 @@ use crate::reward_params::{ IntervalRewardParams, IntervalRewardingParamsUpdate, Performance, RewardingParams, }; use crate::types::{ContractStateParams, LayerAssignment, MixId}; +use crate::{OperatingCostRange, ProfitMarginRange}; use contracts_common::{signing::MessageSignature, IdentityKey, Percent}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Coin, Decimal}; @@ -57,6 +58,12 @@ pub struct InstantiateMsg { pub epochs_in_interval: u32, pub epoch_duration: Duration, pub initial_rewarding_params: InitialRewardingParams, + + #[serde(default)] + pub profit_margin: ProfitMarginRange, + + #[serde(default)] + pub interval_operating_cost: OperatingCostRange, } #[cw_serde] @@ -103,6 +110,11 @@ impl InitialRewardingParams { #[cw_serde] pub enum ExecuteMsg { + /// Change the admin + UpdateAdmin { + admin: String, + }, + AssignNodeLayer { mix_id: MixId, layer: Layer, @@ -269,6 +281,12 @@ pub enum ExecuteMsg { owner: String, }, + // vesting migration: + MigrateVestedMixNode {}, + MigrateVestedDelegation { + mix_id: MixId, + }, + // testing-only #[cfg(feature = "contract-testing")] TestingResolveAllPendingEvents { @@ -279,6 +297,7 @@ pub enum ExecuteMsg { impl ExecuteMsg { pub fn default_memo(&self) -> String { match self { + ExecuteMsg::UpdateAdmin { admin } => format!("updating contract admin to {admin}"), ExecuteMsg::AssignNodeLayer { mix_id, layer } => { format!("assigning mix {mix_id} for layer {layer:?}") } @@ -381,6 +400,9 @@ impl ExecuteMsg { ExecuteMsg::WithdrawDelegatorRewardOnBehalf { mix_id, .. } => { format!("withdrawing delegator reward from mixnode {mix_id} on behalf") } + ExecuteMsg::MigrateVestedMixNode { .. } => "migrate vested mixnode".into(), + ExecuteMsg::MigrateVestedDelegation { .. } => "migrate vested delegation".to_string(), + #[cfg(feature = "contract-testing")] ExecuteMsg::TestingResolveAllPendingEvents { .. } => { "resolving all pending events".into() @@ -392,6 +414,9 @@ impl ExecuteMsg { #[cw_serde] #[cfg_attr(feature = "schema", derive(QueryResponses))] pub enum QueryMsg { + #[cfg_attr(feature = "schema", returns(cw_controllers::AdminResponse))] + Admin {}, + // families /// Gets the list of families registered in this contract. #[cfg_attr(feature = "schema", returns(PagedFamiliesResponse))] diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs index 8a60af94a4..8d97e20494 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs @@ -38,6 +38,7 @@ pub enum PendingEpochEventKind { /// Request to create a delegation towards particular mixnode. /// Note that if such delegation already exists, it will get updated with the provided token amount. #[serde(alias = "Delegate")] + #[non_exhaustive] Delegate { /// The address of the owner of the delegation. owner: Addr, @@ -55,6 +56,7 @@ pub enum PendingEpochEventKind { /// Request to remove delegation from particular mixnode. #[serde(alias = "Undelegate")] + #[non_exhaustive] Undelegate { /// The address of the owner of the delegation. owner: Addr, @@ -109,6 +111,23 @@ impl PendingEpochEventKind { kind: self, } } + + pub fn new_delegate(owner: Addr, mix_id: MixId, amount: Coin) -> Self { + PendingEpochEventKind::Delegate { + owner, + mix_id, + amount, + proxy: None, + } + } + + pub fn new_undelegate(owner: Addr, mix_id: MixId) -> Self { + PendingEpochEventKind::Undelegate { + owner, + mix_id, + proxy: None, + } + } } impl From<(EpochEventId, PendingEpochEventData)> for PendingEpochEvent { diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/simulated_node.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/simulated_node.rs index 5af272366e..12a9e967bd 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/simulated_node.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/simulated_node.rs @@ -47,7 +47,6 @@ impl SimulatedNode { self.rewarding_details.total_unit_reward, delegation, 42, - None, ); self.delegations.insert(delegator, delegation); diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs index 57ad49dc96..261fa99ff0 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs @@ -4,13 +4,18 @@ use crate::families::FamilyHead; use crate::{Gateway, IdentityKey, MixNode, MixNodeCostParams}; use contracts_common::signing::{ - ContractMessageContent, MessageType, Nonce, SignableMessage, SigningPurpose, + ContractMessageContent, LegacyContractMessageContent, MessageType, Nonce, SignableMessage, + SigningPurpose, }; use cosmwasm_std::{Addr, Coin}; use serde::Serialize; pub type SignableMixNodeBondingMsg = SignableMessage>; pub type SignableGatewayBondingMsg = SignableMessage>; +pub type SignableLegacyMixNodeBondingMsg = + SignableMessage>; +pub type SignableLegacyGatewayBondingMsg = + SignableMessage>; pub type SignableFamilyJoinPermitMsg = SignableMessage; #[derive(Serialize)] @@ -37,13 +42,26 @@ impl SigningPurpose for MixnodeBondingPayload { pub fn construct_mixnode_bonding_sign_payload( nonce: Nonce, sender: Addr, - proxy: Option, pledge: Coin, mix_node: MixNode, cost_params: MixNodeCostParams, ) -> SignableMixNodeBondingMsg { let payload = MixnodeBondingPayload::new(mix_node, cost_params); - let content = ContractMessageContent::new(sender, proxy, vec![pledge], payload); + let content = ContractMessageContent::new(sender, vec![pledge], payload); + + SignableMessage::new(nonce, content) +} + +pub fn construct_legacy_mixnode_bonding_sign_payload( + nonce: Nonce, + sender: Addr, + pledge: Coin, + mix_node: MixNode, + cost_params: MixNodeCostParams, +) -> SignableLegacyMixNodeBondingMsg { + let payload = MixnodeBondingPayload::new(mix_node, cost_params); + let content: LegacyContractMessageContent<_> = + ContractMessageContent::new(sender, vec![pledge], payload).into(); SignableMessage::new(nonce, content) } @@ -68,12 +86,24 @@ impl SigningPurpose for GatewayBondingPayload { pub fn construct_gateway_bonding_sign_payload( nonce: Nonce, sender: Addr, - proxy: Option, pledge: Coin, gateway: Gateway, ) -> SignableGatewayBondingMsg { let payload = GatewayBondingPayload::new(gateway); - let content = ContractMessageContent::new(sender, proxy, vec![pledge], payload); + let content = ContractMessageContent::new(sender, vec![pledge], payload); + + SignableMessage::new(nonce, content) +} + +pub fn construct_legacy_gateway_bonding_sign_payload( + nonce: Nonce, + sender: Addr, + pledge: Coin, + gateway: Gateway, +) -> SignableLegacyGatewayBondingMsg { + let payload = GatewayBondingPayload::new(gateway); + let content: LegacyContractMessageContent<_> = + ContractMessageContent::new(sender, vec![pledge], payload).into(); SignableMessage::new(nonce, content) } @@ -82,17 +112,14 @@ pub fn construct_gateway_bonding_sign_payload( pub struct FamilyJoinPermit { // the granter of this permit family_head: FamilyHead, - // whether the **member** will want to join via the proxy (i.e. vesting contract) - proxy: Option, // the actual member we want to permit to join member_node: IdentityKey, } impl FamilyJoinPermit { - pub fn new(family_head: FamilyHead, proxy: Option, member_node: IdentityKey) -> Self { + pub fn new(family_head: FamilyHead, member_node: IdentityKey) -> Self { Self { family_head, - proxy, member_node, } } @@ -107,10 +134,9 @@ impl SigningPurpose for FamilyJoinPermit { pub fn construct_family_join_permit( nonce: Nonce, family_head: FamilyHead, - proxy: Option, member_node: IdentityKey, ) -> SignableFamilyJoinPermitMsg { - let payload = FamilyJoinPermit::new(family_head, proxy, member_node); + let payload = FamilyJoinPermit::new(family_head, member_node); // note: we're NOT wrapping it in `ContractMessageContent` because the family head is not going to be the one // sending the message to the contract diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index e1ff854bb9..f95c15eb34 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -3,9 +3,11 @@ use crate::error::MixnetContractError; use crate::Layer; +use contracts_common::Percent; use cosmwasm_schema::cw_serde; -use cosmwasm_std::Addr; use cosmwasm_std::Coin; +use cosmwasm_std::{Addr, Uint128}; +use std::fmt::{Display, Formatter}; use std::ops::Index; // type aliases for better reasoning about available data @@ -15,6 +17,65 @@ pub type SphinxKeyRef<'a> = &'a str; pub type MixId = u32; pub type BlockHeight = u64; +#[cw_serde] +pub struct RangedValue { + pub minimum: T, + pub maximum: T, +} + +impl Copy for RangedValue where T: Copy {} + +pub type ProfitMarginRange = RangedValue; +pub type OperatingCostRange = RangedValue; + +impl Display for RangedValue +where + T: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{} - {}", self.minimum, self.maximum) + } +} + +impl Default for ProfitMarginRange { + fn default() -> Self { + ProfitMarginRange { + minimum: Percent::zero(), + maximum: Percent::hundred(), + } + } +} + +impl Default for OperatingCostRange { + fn default() -> Self { + OperatingCostRange { + minimum: Uint128::zero(), + + // 1 billion (native tokens, i.e. 1 billion * 1'000'000 base tokens) - the total supply + maximum: Uint128::new(1_000_000_000_000_000), + } + } +} + +impl RangedValue +where + T: Copy + PartialOrd + PartialEq, +{ + pub fn normalise(&self, value: T) -> T { + if value < self.minimum { + self.minimum + } else if value > self.maximum { + self.maximum + } else { + value + } + } + + pub fn within_range(&self, value: T) -> bool { + value >= self.minimum && value <= self.maximum + } +} + /// Specifies layer assignment for the given mixnode. #[cw_serde] pub struct LayerAssignment { @@ -126,7 +187,11 @@ impl Index for LayerDistribution { #[cw_serde] pub struct ContractState { /// Address of the contract owner. - pub owner: Addr, + #[deprecated( + note = "use explicit ADMIN instead. this field will be removed in future release" + )] + #[serde(default)] + pub owner: Option, /// Address of "rewarding validator" (nym-api) that's allowed to send any rewarding-related transactions. pub rewarding_validator_address: Addr, @@ -154,4 +219,14 @@ pub struct ContractStateParams { /// Minimum amount a gateway must pledge to get into the system. pub minimum_gateway_pledge: Coin, + + /// Defines the allowed profit margin range of operators. + /// default: 0% - 100% + #[serde(default)] + pub profit_margin: ProfitMarginRange, + + /// Defines the allowed interval operating cost range of operators. + /// default: 0 - 1'000'000'000'000'000 (1 Billion native tokens - the total supply) + #[serde(default)] + pub interval_operating_cost: OperatingCostRange, } diff --git a/common/cosmwasm-smart-contracts/multisig-contract/src/error.rs b/common/cosmwasm-smart-contracts/multisig-contract/src/error.rs index ead941cad0..39ba25b38c 100644 --- a/common/cosmwasm-smart-contracts/multisig-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/multisig-contract/src/error.rs @@ -47,4 +47,10 @@ pub enum ContractError { #[error("{0}")] Deposit(#[from] DepositError), + + #[error("the provided redemption digest does not have valid base58 encoding or is not 32 bytes long")] + MalformedRedemptionDigest, + + #[error("the provided redemption proposal data is malformed and can't be decoded")] + MalformedRedemptionProposalData, } diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs index d7dc757956..b957adbe10 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs @@ -167,3 +167,11 @@ pub fn new_track_undelegation_event() -> Event { pub fn new_track_reward_event() -> Event { Event::new(TRACK_REWARD_EVENT_TYPE) } + +pub fn new_track_migrate_mixnode_event() -> Event { + Event::new("track_migrate_vesting_mixnode") +} + +pub fn new_track_migrate_delegation_event() -> Event { + Event::new("track_migrate_vesting_delegation") +} diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs index 6593926445..eefe07e9f1 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs @@ -136,6 +136,14 @@ pub enum ExecuteMsg { address: String, cap: PledgeCap, }, + TrackMigratedMixnode { + owner: String, + }, + // no need to track migrated gateways as there are no vesting gateways on mainnet + TrackMigratedDelegation { + owner: String, + mix_id: MixId, + }, } impl ExecuteMsg { @@ -171,6 +179,10 @@ impl ExecuteMsg { ExecuteMsg::TransferOwnership { .. } => "VestingExecuteMsg::TransferOwnership", ExecuteMsg::UpdateStakingAddress { .. } => "VestingExecuteMsg::UpdateStakingAddress", ExecuteMsg::UpdateLockedPledgeCap { .. } => "VestingExecuteMsg::UpdateLockedPledgeCap", + ExecuteMsg::TrackMigratedMixnode { .. } => "VestingExecuteMsg::TrackMigratedMixnode", + ExecuteMsg::TrackMigratedDelegation { .. } => { + "VestingExecuteMsg::TrackMigratedDelegation" + } } } } diff --git a/common/credential-storage/Cargo.toml b/common/credential-storage/Cargo.toml index 056d94b12b..b822d49db6 100644 --- a/common/credential-storage/Cargo.toml +++ b/common/credential-storage/Cargo.toml @@ -8,22 +8,31 @@ license.workspace = true [dependencies] async-trait = { workspace = true } +bincode = { workspace = true, optional = true } log = { workspace = true } thiserror = { workspace = true } -tokio = { workspace = true, features = ["sync"]} +serde = { workspace = true, features = ["derive"], optional = true } +tokio = { workspace = true, features = ["sync"] } zeroize = { workspace = true, features = ["zeroize_derive"] } +nym-credentials = { path = "../credentials" } +nym-compact-ecash = { path = "../nym_offline_compact_ecash" } +nym-ecash-time = { path = "../ecash-time" } + [target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] workspace = true -features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] +features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"] [target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio] workspace = true -features = [ "rt-multi-thread", "net", "signal", "fs" ] +features = ["rt-multi-thread", "net", "signal", "fs"] [build-dependencies] sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } + +[features] +persistent-storage = ["bincode", "serde"] \ No newline at end of file diff --git a/common/credential-storage/migrations/20241104120001_add_ecash_tables.sql b/common/credential-storage/migrations/20241104120001_add_ecash_tables.sql new file mode 100644 index 0000000000..d2f879bfee --- /dev/null +++ b/common/credential-storage/migrations/20241104120001_add_ecash_tables.sql @@ -0,0 +1,69 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: Apache-2.0 + */ + +DROP TABLE coconut_credentials; + +CREATE TABLE master_verification_key ( + epoch_id INTEGER PRIMARY KEY NOT NULL, + + serialised_key BLOB NOT NULL +); + +CREATE TABLE coin_indices_signatures +( + epoch_id INTEGER PRIMARY KEY NOT NULL, + + serialised_signatures BLOB NOT NULL +); + +CREATE TABLE expiration_date_signatures ( + expiration_date DATE NOT NULL UNIQUE PRIMARY KEY, + + epoch_id INTEGER NOT NULL, + + -- combined signatures for all tuples issued for given day + serialised_signatures BLOB NOT NULL +); + + +CREATE TABLE ecash_ticketbook +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + + -- introduce a way for us to introduce breaking changes in serialization of data + serialization_revision INTEGER NOT NULL, + + -- the type of the associated ticketbook + ticketbook_type TEXT NOT NULL, + + -- the actual crypto data of the ticketbook (wallet, keys, etc.) + ticketbook_data BLOB NOT NULL UNIQUE, + + -- for each ticketbook we MUST have corresponding expiration date signatures + expiration_date DATE NOT NULL REFERENCES expiration_date_signatures(expiration_date), + + -- for each ticketbook we MUST have corresponding coin index signatures + epoch_id INTEGER NOT NULL REFERENCES coin_indices_signatures(epoch_id), + + -- the initial number of tickets the wallet has been created for + total_tickets INTEGER NOT NULL, + + -- how many tickets have been used so far (the `l` value of the wallet) + used_tickets INTEGER NOT NULL +); + +-- data for ticketbooks that have an associated deposit, but failed to get issued +CREATE TABLE pending_issuance +( + deposit_id INTEGER NOT NULL PRIMARY KEY, + + -- introduce a way for us to introduce breaking changes in serialization of data + serialization_revision INTEGER NOT NULL, + + pending_ticketbook_data BLOB NOT NULL UNIQUE, + + -- for each ticketbook we MUST have corresponding expiration date signatures + expiration_date DATE NOT NULL REFERENCES expiration_date_signatures(expiration_date) +); \ No newline at end of file diff --git a/common/credential-storage/src/backends/memory.rs b/common/credential-storage/src/backends/memory.rs index 78d18f3b88..71aea890b0 100644 --- a/common/credential-storage/src/backends/memory.rs +++ b/common/credential-storage/src/backends/memory.rs @@ -1,23 +1,34 @@ // Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::models::{CredentialUsage, StoredIssuedCredential}; +use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook}; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use nym_compact_ecash::VerificationKeyAuth; +use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise; +use nym_credentials::{IssuanceTicketBook, IssuedTicketBook}; +use nym_ecash_time::Date; +use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; +use zeroize::Zeroizing; #[derive(Clone)] -pub struct CoconutCredentialManager { - inner: Arc>, +pub struct MemoryEcachTicketbookManager { + inner: Arc>, } #[derive(Default)] -struct CoconutCredentialManagerInner { - credentials: Vec, - credential_usage: Vec, +struct EcashCredentialManagerInner { + ticketbooks: HashMap, + pending: HashMap, + master_vk: HashMap, + coin_indices_sigs: HashMap>, + expiration_date_sigs: HashMap>, _next_id: i64, } -impl CoconutCredentialManagerInner { +impl EcashCredentialManagerInner { fn next_id(&mut self) -> i64 { let next = self._next_id; self._next_id += 1; @@ -25,108 +36,210 @@ impl CoconutCredentialManagerInner { } } -impl CoconutCredentialManager { +// hehe, that's hacky AF, but it works as a **TEMPORARY** workaround +fn hack_clone_ticketbook(book: &IssuedTicketBook) -> IssuedTicketBook { + let ser = book.pack(); + let data = Zeroizing::new(ser.data); + IssuedTicketBook::try_unpack(&data, None).unwrap() +} + +impl MemoryEcachTicketbookManager { /// Creates new empty instance of the `CoconutCredentialManager`. pub fn new() -> Self { - CoconutCredentialManager { + MemoryEcachTicketbookManager { inner: Default::default(), } } - pub async fn insert_issued_credential( - &self, - credential_type: String, - serialization_revision: u8, - credential_data: &[u8], - epoch_id: u32, - ) { - let mut inner = self.inner.write().await; - let id = inner.next_id(); - inner.credentials.push(StoredIssuedCredential { - id, - serialization_revision, - credential_data: credential_data.to_vec(), - credential_type, - epoch_id, - expired: false, - }) - } - - async fn bandwidth_voucher_spent(&self, id: i64) -> bool { - self.inner - .read() - .await - .credential_usage - .iter() - .any(|c| c.credential_id == id) - } - - async fn freepass_spent(&self, id: i64, gateway_id: &str) -> bool { - self.inner - .read() - .await - .credential_usage - .iter() - .any(|c| c.credential_id == id && c.gateway_id_bs58 == gateway_id) - } - - /// Tries to retrieve one of the stored, unused credentials. - pub async fn get_next_unspect_bandwidth_voucher(&self) -> Option { - let guard = self.inner.read().await; - for credential in guard - .credentials - .iter() - .filter(|c| c.credential_type == "BandwidthVoucher") - { - if !self.bandwidth_voucher_spent(credential.id).await { - return Some(credential.clone()); - } - } - None - } - - pub async fn get_next_unspect_freepass( - &self, - gateway_id: &str, - ) -> Option { - let guard = self.inner.read().await; - for credential in guard - .credentials - .iter() - .filter(|c| c.credential_type == "FreeBandwidthPass") - { - if credential.expired { - continue; - } - if !self.freepass_spent(credential.id, gateway_id).await { - return Some(credential.clone()); - } - } - None - } - - /// Consumes in the database the specified credential. - /// - /// # Arguments - /// - /// * `id`: Database id. - pub async fn consume_coconut_credential(&self, id: i64, gateway_id: &str) { + pub(crate) async fn cleanup_expired(&self) { let mut guard = self.inner.write().await; - guard.credential_usage.push(CredentialUsage { - credential_id: id, - gateway_id_bs58: gateway_id.to_string(), - }); + + let mut to_remove = Vec::new(); + + for t in guard.ticketbooks.values() { + if t.ticketbook.expired() { + to_remove.push(t.ticketbook_id); + } + } + + for id in to_remove { + guard.ticketbooks.remove(&id); + } } - /// Marks the specified credential as expired - /// - /// # Arguments - /// - /// * `id`: Id of the credential to mark as expired. - pub async fn mark_expired(&self, id: i64) { - let mut creds = self.inner.write().await; - if let Some(cred) = creds.credentials.get_mut(id as usize) { - cred.expired = true; + pub async fn get_next_unspent_ticketbook_and_update( + &self, + tickets: u32, + ) -> Option { + let mut guard = self.inner.write().await; + + for t in guard.ticketbooks.values_mut() { + if !t.ticketbook.expired() + && t.ticketbook.spent_tickets() + tickets as u64 + <= t.ticketbook.params_total_tickets() + { + t.ticketbook + .update_spent_tickets(t.ticketbook.spent_tickets() + tickets as u64); + return Some(RetrievedTicketbook { + ticketbook_id: t.ticketbook_id, + ticketbook: hack_clone_ticketbook(&t.ticketbook), + }); + } } + + None + } + + pub(crate) async fn revert_ticketbook_withdrawal( + &self, + ticketbook_id: i64, + withdrawn: u32, + expected_current_total_spent: u32, + ) -> bool { + let mut guard = self.inner.write().await; + + let Some(book) = guard.ticketbooks.get_mut(&ticketbook_id) else { + return false; + }; + + if book.ticketbook.spent_tickets() == expected_current_total_spent as u64 { + book.ticketbook + .update_spent_tickets(book.ticketbook.spent_tickets() - withdrawn as u64); + true + } else { + false + } + } + + pub(crate) async fn insert_pending_ticketbook(&self, ticketbook: &IssuanceTicketBook) { + let mut guard = self.inner.write().await; + + let ser = ticketbook.pack(); + let data = Zeroizing::new(ser.data); + let id = ticketbook.deposit_id() as i64; + guard.pending.insert( + id, + RetrievedPendingTicketbook { + pending_id: ticketbook.deposit_id() as i64, + pending_ticketbook: IssuanceTicketBook::try_unpack(&data, None).unwrap(), + }, + ); + } + + pub(crate) async fn get_pending_ticketbooks(&self) -> Vec { + let guard = self.inner.read().await; + + let mut pending = Vec::new(); + + for p in guard.pending.values() { + // 🫠 + let ser = p.pending_ticketbook.pack(); + let data = Zeroizing::new(ser.data); + pending.push(RetrievedPendingTicketbook { + pending_id: p.pending_id, + pending_ticketbook: IssuanceTicketBook::try_unpack(&data, None).unwrap(), + }) + } + + pending + } + + pub(crate) async fn remove_pending_ticketbook(&self, pending_id: i64) { + let mut guard = self.inner.write().await; + + guard.pending.remove(&pending_id); + } + + pub(crate) async fn insert_new_ticketbook(&self, ticketbook: &IssuedTicketBook) { + let mut guard = self.inner.write().await; + let id = guard.next_id(); + + // hehe, that's hacky AF, but it works as a **TEMPORARY** workaround + let ser = ticketbook.pack(); + let data = Zeroizing::new(ser.data); + guard.ticketbooks.insert( + id, + RetrievedTicketbook { + ticketbook_id: id, + ticketbook: IssuedTicketBook::try_unpack(&data, None).unwrap(), + }, + ); + } + + pub(crate) async fn get_ticketbooks_info(&self) -> Vec { + let guard = self.inner.read().await; + + guard + .ticketbooks + .values() + .map(|t| BasicTicketbookInformation { + id: t.ticketbook_id, + expiration_date: t.ticketbook.expiration_date(), + ticketbook_type: t.ticketbook.ticketbook_type().to_string(), + epoch_id: t.ticketbook.epoch_id() as u32, + total_tickets: t.ticketbook.spent_tickets() as u32, + used_tickets: t.ticketbook.params_total_tickets() as u32, + }) + .collect() + } + + pub(crate) async fn get_master_verification_key( + &self, + epoch_id: u64, + ) -> Option { + let guard = self.inner.read().await; + + guard.master_vk.get(&epoch_id).cloned() + } + + pub(crate) async fn insert_master_verification_key( + &self, + epoch_id: u64, + key: &VerificationKeyAuth, + ) { + let mut guard = self.inner.write().await; + + guard.master_vk.insert(epoch_id, key.clone()); + } + + pub(crate) async fn get_coin_index_signatures( + &self, + epoch_id: u64, + ) -> Option> { + let guard = self.inner.read().await; + + guard.coin_indices_sigs.get(&epoch_id).cloned() + } + + pub(crate) async fn insert_coin_index_signatures( + &self, + epoch_id: u64, + sigs: &[AnnotatedCoinIndexSignature], + ) { + let mut guard = self.inner.write().await; + + guard.coin_indices_sigs.insert(epoch_id, sigs.to_vec()); + } + + pub(crate) async fn get_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Option> { + let guard = self.inner.read().await; + + guard.expiration_date_sigs.get(&expiration_date).cloned() + } + + pub(crate) async fn insert_expiration_date_signatures( + &self, + _epoch_id: u64, + expiration_date: Date, + sigs: &[AnnotatedExpirationDateSignature], + ) { + let mut guard = self.inner.write().await; + + guard + .expiration_date_sigs + .insert(expiration_date, sigs.to_vec()); } } diff --git a/common/credential-storage/src/backends/mod.rs b/common/credential-storage/src/backends/mod.rs index 2cd1334af3..bdb8eb6d9c 100644 --- a/common/credential-storage/src/backends/mod.rs +++ b/common/credential-storage/src/backends/mod.rs @@ -2,5 +2,5 @@ // SPDX-License-Identifier: Apache-2.0 pub mod memory; -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "persistent-storage"))] pub mod sqlite; diff --git a/common/credential-storage/src/backends/sqlite.rs b/common/credential-storage/src/backends/sqlite.rs index 4d4face955..43f019afa3 100644 --- a/common/credential-storage/src/backends/sqlite.rs +++ b/common/credential-storage/src/backends/sqlite.rs @@ -1,116 +1,282 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::models::StoredIssuedCredential; +use crate::models::{ + BasicTicketbookInformation, RawExpirationDateSignatures, StoredIssuedTicketbook, + StoredPendingTicketbook, +}; +use nym_ecash_time::Date; +use sqlx::{Executor, Sqlite, Transaction}; #[derive(Clone)] -pub struct CoconutCredentialManager { +pub struct SqliteEcashTicketbookManager { connection_pool: sqlx::SqlitePool, } -impl CoconutCredentialManager { - /// Creates new instance of the `CoconutCredentialManager` with the provided sqlite connection pool. +impl SqliteEcashTicketbookManager { + /// Creates new instance of the `EcashTicketbookManager` with the provided sqlite connection pool. /// /// # Arguments /// /// * `connection_pool`: database connection pool to use. pub fn new(connection_pool: sqlx::SqlitePool) -> Self { - CoconutCredentialManager { connection_pool } + SqliteEcashTicketbookManager { connection_pool } } - pub async fn insert_issued_credential( + pub(crate) async fn cleanup_expired(&self, deadline: Date) -> Result<(), sqlx::Error> { + sqlx::query!( + "DELETE FROM ecash_ticketbook WHERE expiration_date <= ?", + deadline + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn begin_storage_tx(&self) -> Result, sqlx::Error> { + self.connection_pool.begin().await + } + + pub(crate) async fn insert_pending_ticketbook( &self, - credential_type: String, - serialization_revision: u8, - credential_data: &[u8], + serialisation_revision: u8, + deposit_id: u32, + data: &[u8], + expiration_date: Date, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO pending_issuance + (deposit_id, serialization_revision, pending_ticketbook_data, expiration_date) + VALUES (?, ?, ?, ?) + "#, + deposit_id, + serialisation_revision, + data, + expiration_date, + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) async fn insert_new_ticketbook( + &self, + serialisation_revision: u8, + data: &[u8], + expiration_date: Date, + typ: &str, epoch_id: u32, + total_tickets: u32, + used_tickets: u32, ) -> Result<(), sqlx::Error> { sqlx::query!( r#" - INSERT INTO coconut_credentials(serialization_revision, credential_type, credential_data, epoch_id, expired) - VALUES (?, ?, ?, ?, false) + INSERT INTO ecash_ticketbook + (serialization_revision, ticketbook_data, expiration_date, ticketbook_type, epoch_id, total_tickets, used_tickets) + VALUES (?, ?, ?, ?, ?, ?, ?) "#, - serialization_revision, credential_type, credential_data, epoch_id + serialisation_revision, + data, + expiration_date, + typ, + epoch_id, + total_tickets, + used_tickets, ).execute(&self.connection_pool).await?; + Ok(()) } - pub async fn get_next_unspect_freepass( + pub(crate) async fn get_ticketbooks_info( &self, - gateway_id: &str, - ) -> Result, sqlx::Error> { - // get a credential of freepass type that doesn't appear in `credential_usage` for the provided gateway_id + ) -> Result, sqlx::Error> { sqlx::query_as( r#" - SELECT * - FROM coconut_credentials - WHERE coconut_credentials.credential_type == "FreeBandwidthPass" AND coconut_credentials.expired = false - AND NOT EXISTS (SELECT 1 - FROM credential_usage - WHERE credential_usage.credential_id = coconut_credentials.id - AND credential_usage.gateway_id_bs58 == ?) - ORDER BY coconut_credentials.id - LIMIT 1 - "#, + SELECT id, expiration_date, ticketbook_type, epoch_id, total_tickets, used_tickets + FROM ecash_ticketbook + "#, ) - .bind(gateway_id) - .fetch_optional(&self.connection_pool) + .fetch_all(&self.connection_pool) .await } - pub async fn get_next_unspect_bandwidth_voucher( + pub(crate) async fn decrease_used_ticketbook_tickets( &self, - ) -> Result, sqlx::Error> { - // get a credential of bandwidth voucher type that doesn't appear in `credential_usage` for any gateway_id - sqlx::query_as( + ticketbook_id: i64, + reverted_spent: u32, + expected_current_total_spent: u32, + ) -> Result { + // the 'AND' clause will ensure this will only be executed if nobody else interacted with the row + let affected = sqlx::query!( r#" - SELECT * - FROM coconut_credentials - WHERE coconut_credentials.credential_type == "BandwidthVoucher" - AND NOT EXISTS (SELECT 1 - FROM credential_usage - WHERE credential_usage.credential_id = coconut_credentials.id) - ORDER BY coconut_credentials.id - LIMIT 1 + UPDATE ecash_ticketbook + SET used_tickets = used_tickets - ? + WHERE id = ? + AND used_tickets = ? "#, + reverted_spent, + ticketbook_id, + expected_current_total_spent ) - .fetch_optional(&self.connection_pool) - .await + .execute(&self.connection_pool) + .await? + .rows_affected(); + Ok(affected > 0) } - /// Consumes in the database the specified credential. - /// - /// # Arguments - /// - /// * `id`: Database id. - /// * `gateway_id`: id of the gateway that received the credential - pub async fn consume_coconut_credential( + pub(crate) async fn get_pending_ticketbooks( &self, - id: i64, - gateway_id: &str, + ) -> Result, sqlx::Error> { + sqlx::query_as("SELECT * FROM pending_issuance") + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn remove_pending_ticketbook( + &self, + pending_id: i64, ) -> Result<(), sqlx::Error> { sqlx::query!( - "INSERT INTO credential_usage (credential_id, gateway_id_bs58) VALUES (?, ?)", - id, - gateway_id + "DELETE FROM pending_issuance WHERE deposit_id = ?", + pending_id ) .execute(&self.connection_pool) .await?; Ok(()) } - /// Marks the specified credential as expired - /// - /// # Arguments - /// - /// * `id`: Id of the credential to mark as expired. - pub async fn mark_expired(&self, id: i64) -> Result<(), sqlx::Error> { + pub(crate) async fn get_master_verification_key( + &self, + epoch_id: i64, + ) -> Result>, sqlx::Error> { sqlx::query!( - "UPDATE coconut_credentials SET expired = TRUE WHERE id = ?", - id + "SELECT serialised_key FROM master_verification_key WHERE epoch_id = ?", + epoch_id + ) + .fetch_optional(&self.connection_pool) + .await + .map(|maybe_record| maybe_record.map(|r| r.serialised_key)) + } + + pub(crate) async fn insert_master_verification_key( + &self, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO master_verification_key(epoch_id, serialised_key) VALUES (?, ?)", + epoch_id, + data ) .execute(&self.connection_pool) .await?; Ok(()) } + + pub(crate) async fn get_coin_index_signatures( + &self, + epoch_id: i64, + ) -> Result>, sqlx::Error> { + sqlx::query!( + "SELECT serialised_signatures FROM coin_indices_signatures WHERE epoch_id = ?", + epoch_id + ) + .fetch_optional(&self.connection_pool) + .await + .map(|maybe_record| maybe_record.map(|r| r.serialised_signatures)) + } + + pub(crate) async fn insert_coin_index_signatures( + &self, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO coin_indices_signatures(epoch_id, serialised_signatures) VALUES (?, ?)", + epoch_id, + data + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + RawExpirationDateSignatures, + r#" + SELECT epoch_id as "epoch_id: u32", serialised_signatures + FROM expiration_date_signatures + WHERE expiration_date = ? + "#, + expiration_date + ) + .fetch_optional(&self.connection_pool) + .await + } + + pub(crate) async fn insert_expiration_date_signatures( + &self, + epoch_id: i64, + expiration_date: Date, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO expiration_date_signatures(expiration_date, epoch_id, serialised_signatures) VALUES (?, ?, ?)", + expiration_date, + epoch_id, + data + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } +} + +pub(crate) async fn get_next_unspent_ticketbook<'a, E>( + executor: E, + deadline: Date, + tickets: u32, +) -> Result, sqlx::Error> +where + E: Executor<'a, Database = Sqlite>, +{ + sqlx::query_as( + r#" + SELECT * + FROM ecash_ticketbook + WHERE used_tickets + ? <= total_tickets + AND expiration_date >= ? + ORDER BY expiration_date ASC + LIMIT 1 + "#, + ) + .bind(tickets) + .bind(deadline) + .fetch_optional(executor) + .await +} + +pub(crate) async fn increase_used_ticketbook_tickets<'a, E>( + executor: E, + ticketbook_id: i64, + extra_spent: u32, +) -> Result<(), sqlx::Error> +where + E: Executor<'a, Database = Sqlite>, +{ + sqlx::query!( + "UPDATE ecash_ticketbook SET used_tickets = used_tickets + ? WHERE id = ?", + extra_spent, + ticketbook_id + ) + .execute(executor) + .await?; + Ok(()) } diff --git a/common/credential-storage/src/ephemeral_storage.rs b/common/credential-storage/src/ephemeral_storage.rs index ead11edcf8..e1a18942c2 100644 --- a/common/credential-storage/src/ephemeral_storage.rs +++ b/common/credential-storage/src/ephemeral_storage.rs @@ -1,26 +1,30 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::fmt::{self, Debug, Formatter}; - -use crate::backends::memory::CoconutCredentialManager; +use crate::backends::memory::MemoryEcachTicketbookManager; use crate::error::StorageError; -use crate::models::{StorableIssuedCredential, StoredIssuedCredential}; +use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook}; use crate::storage::Storage; use async_trait::async_trait; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use nym_compact_ecash::VerificationKeyAuth; +use nym_credentials::{IssuanceTicketBook, IssuedTicketBook}; +use nym_ecash_time::Date; +use std::fmt::{self, Debug, Formatter}; pub type EphemeralCredentialStorage = EphemeralStorage; // note that clone here is fine as upon cloning the same underlying pool will be used #[derive(Clone)] pub struct EphemeralStorage { - coconut_credential_manager: CoconutCredentialManager, + storage_manager: MemoryEcachTicketbookManager, } impl Default for EphemeralStorage { fn default() -> Self { EphemeralStorage { - coconut_credential_manager: CoconutCredentialManager::new(), + storage_manager: MemoryEcachTicketbookManager::new(), } } } @@ -35,55 +39,135 @@ impl Debug for EphemeralStorage { impl Storage for EphemeralStorage { type StorageError = StorageError; - async fn insert_issued_credential<'a>( + async fn cleanup_expired(&self) -> Result<(), Self::StorageError> { + self.storage_manager.cleanup_expired().await; + Ok(()) + } + + async fn insert_pending_ticketbook( &self, - bandwidth_credential: StorableIssuedCredential<'a>, - ) -> Result<(), StorageError> { - self.coconut_credential_manager - .insert_issued_credential( - bandwidth_credential.credential_type, - bandwidth_credential.serialization_revision, - bandwidth_credential.credential_data, - bandwidth_credential.epoch_id, - ) + ticketbook: &IssuanceTicketBook, + ) -> Result<(), Self::StorageError> { + self.storage_manager + .insert_pending_ticketbook(ticketbook) .await; Ok(()) } - async fn get_next_unspent_credential( + async fn insert_issued_ticketbook( &self, - gateway_id: &str, - ) -> Result, Self::StorageError> { - // first try to get a free pass if available, otherwise fallback to bandwidth voucher - let maybe_freepass = self - .coconut_credential_manager - .get_next_unspect_freepass(gateway_id) - .await; - if maybe_freepass.is_some() { - return Ok(maybe_freepass); - } + ticketbook: &IssuedTicketBook, + ) -> Result<(), StorageError> { + self.storage_manager.insert_new_ticketbook(ticketbook).await; + Ok(()) + } + async fn get_ticketbooks_info( + &self, + ) -> Result, Self::StorageError> { + Ok(self.storage_manager.get_ticketbooks_info().await) + } + + async fn get_pending_ticketbooks( + &self, + ) -> Result, Self::StorageError> { + Ok(self.storage_manager.get_pending_ticketbooks().await) + } + + async fn remove_pending_ticketbook(&self, pending_id: i64) -> Result<(), Self::StorageError> { + self.storage_manager + .remove_pending_ticketbook(pending_id) + .await; + Ok(()) + } + + /// Tries to retrieve one of the stored ticketbook, + /// that has not yet expired and has required number of unspent tickets. + /// it immediately updated the on-disk number of used tickets so that another task + /// could obtain their own tickets at the same time + async fn get_next_unspent_usable_ticketbook( + &self, + tickets: u32, + ) -> Result, Self::StorageError> { Ok(self - .coconut_credential_manager - .get_next_unspect_bandwidth_voucher() + .storage_manager + .get_next_unspent_ticketbook_and_update(tickets) .await) } - async fn consume_coconut_credential( + async fn attempt_revert_ticketbook_withdrawal( &self, - id: i64, - gateway_id: &str, - ) -> Result<(), StorageError> { - self.coconut_credential_manager - .consume_coconut_credential(id, gateway_id) - .await; + ticketbook_id: i64, + previous_total_spent: u32, + withdrawn: u32, + ) -> Result { + Ok(self + .storage_manager + .revert_ticketbook_withdrawal(ticketbook_id, previous_total_spent, withdrawn) + .await) + } + async fn get_master_verification_key( + &self, + epoch_id: u64, + ) -> Result, Self::StorageError> { + Ok(self + .storage_manager + .get_master_verification_key(epoch_id) + .await) + } + + async fn insert_master_verification_key( + &self, + epoch_id: u64, + key: &VerificationKeyAuth, + ) -> Result<(), Self::StorageError> { + self.storage_manager + .insert_master_verification_key(epoch_id, key) + .await; Ok(()) } - async fn mark_expired(&self, id: i64) -> Result<(), Self::StorageError> { - self.coconut_credential_manager.mark_expired(id).await; + async fn get_coin_index_signatures( + &self, + epoch_id: u64, + ) -> Result>, Self::StorageError> { + Ok(self + .storage_manager + .get_coin_index_signatures(epoch_id) + .await) + } + async fn insert_coin_index_signatures( + &self, + epoch_id: u64, + data: &[AnnotatedCoinIndexSignature], + ) -> Result<(), Self::StorageError> { + self.storage_manager + .insert_coin_index_signatures(epoch_id, data) + .await; + Ok(()) + } + + async fn get_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result>, Self::StorageError> { + Ok(self + .storage_manager + .get_expiration_date_signatures(expiration_date) + .await) + } + + async fn insert_expiration_date_signatures( + &self, + epoch_id: u64, + expiration_date: Date, + data: &[AnnotatedExpirationDateSignature], + ) -> Result<(), Self::StorageError> { + self.storage_manager + .insert_expiration_date_signatures(epoch_id, expiration_date, data) + .await; Ok(()) } } diff --git a/common/credential-storage/src/error.rs b/common/credential-storage/src/error.rs index 42b94c2fc4..665e4db638 100644 --- a/common/credential-storage/src/error.rs +++ b/common/credential-storage/src/error.rs @@ -9,6 +9,9 @@ pub enum StorageError { #[error("Database experienced an internal error - {0}")] InternalDatabaseError(#[from] sqlx::Error), + #[error("experienced internal storage error due to database inconsistency: {reason}")] + DatabaseInconsistency { reason: String }, + #[cfg(not(target_arch = "wasm32"))] #[error("Failed to perform database migration - {0}")] MigrationError(#[from] sqlx::migrate::MigrateError), @@ -19,6 +22,17 @@ pub enum StorageError { #[error("No unused credential in database. You need to buy at least one")] NoCredential, + #[error("No signatures for epoch {epoch_id} in the database")] + NoSignatures { epoch_id: i64 }, + #[error("Database unique constraint violation. Is the credential already imported?")] ConstraintUnique, } + +impl StorageError { + pub fn database_inconsistency>(reason: S) -> StorageError { + StorageError::DatabaseInconsistency { + reason: reason.into(), + } + } +} diff --git a/common/credential-storage/src/lib.rs b/common/credential-storage/src/lib.rs index c27e904192..e320926175 100644 --- a/common/credential-storage/src/lib.rs +++ b/common/credential-storage/src/lib.rs @@ -5,21 +5,20 @@ use crate::ephemeral_storage::EphemeralStorage; -#[cfg(not(target_arch = "wasm32"))] -use crate::persistent_storage::PersistentStorage; -#[cfg(not(target_arch = "wasm32"))] -use std::path::Path; - mod backends; pub mod ephemeral_storage; pub mod error; pub mod models; -#[cfg(not(target_arch = "wasm32"))] + +#[cfg(all(not(target_arch = "wasm32"), feature = "persistent-storage"))] pub mod persistent_storage; + pub mod storage; -#[cfg(not(target_arch = "wasm32"))] -pub async fn initialise_persistent_storage>(path: P) -> PersistentStorage { +#[cfg(all(not(target_arch = "wasm32"), feature = "persistent-storage"))] +pub async fn initialise_persistent_storage>( + path: P, +) -> crate::persistent_storage::PersistentStorage { match persistent_storage::PersistentStorage::init(path).await { Err(err) => panic!("failed to initialise credential storage - {err}"), Ok(storage) => storage, diff --git a/common/credential-storage/src/models.rs b/common/credential-storage/src/models.rs index 49c004ece0..2a3faa2d70 100644 --- a/common/credential-storage/src/models.rs +++ b/common/credential-storage/src/models.rs @@ -1,44 +1,65 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use nym_credentials::{IssuanceTicketBook, IssuedTicketBook}; +use nym_ecash_time::Date; use zeroize::{Zeroize, ZeroizeOnDrop}; -// #[derive(Clone)] -// pub struct CoconutCredential { -// #[allow(dead_code)] -// pub id: i64, -// pub voucher_value: String, -// pub voucher_info: String, -// pub serial_number: String, -// pub binding_number: String, -// pub signature: String, -// pub epoch_id: String, -// pub consumed: bool, -// } +pub struct RetrievedTicketbook { + pub ticketbook_id: i64, + pub ticketbook: IssuedTicketBook, +} + +pub struct RetrievedPendingTicketbook { + pub pending_id: i64, + pub pending_ticketbook: IssuanceTicketBook, +} + +#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] +pub struct BasicTicketbookInformation { + pub id: i64, + pub expiration_date: Date, + pub ticketbook_type: String, + pub epoch_id: u32, + pub total_tickets: u32, + pub used_tickets: u32, +} #[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] #[derive(Zeroize, ZeroizeOnDrop, Clone)] -pub struct StoredIssuedCredential { +pub struct StoredIssuedTicketbook { pub id: i64, pub serialization_revision: u8, - pub credential_data: Vec, - pub credential_type: String, + + pub ticketbook_type: String, + + pub ticketbook_data: Vec, + + #[zeroize(skip)] + pub expiration_date: Date, pub epoch_id: u32, - pub expired: bool, -} -pub struct StorableIssuedCredential<'a> { - pub serialization_revision: u8, - pub credential_data: &'a [u8], - pub credential_type: String, - - pub epoch_id: u32, + pub total_tickets: u32, + pub used_tickets: u32, } #[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] -pub struct CredentialUsage { - pub credential_id: i64, - pub gateway_id_bs58: String, +#[derive(Zeroize, ZeroizeOnDrop, Clone)] +pub struct StoredPendingTicketbook { + pub deposit_id: i64, + + pub serialization_revision: u8, + + pub pending_ticketbook_data: Vec, + + #[zeroize(skip)] + pub expiration_date: Date, +} + +#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] +pub struct RawExpirationDateSignatures { + pub epoch_id: u32, + pub serialised_signatures: Vec, } diff --git a/common/credential-storage/src/persistent_storage.rs b/common/credential-storage/src/persistent_storage.rs deleted file mode 100644 index cd2bd0170f..0000000000 --- a/common/credential-storage/src/persistent_storage.rs +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::backends::sqlite::CoconutCredentialManager; -use crate::error::StorageError; -use crate::storage::Storage; - -use crate::models::{StorableIssuedCredential, StoredIssuedCredential}; -use async_trait::async_trait; -use log::{debug, error}; -use sqlx::ConnectOptions; -use std::path::Path; - -// note that clone here is fine as upon cloning the same underlying pool will be used -#[derive(Clone)] -pub struct PersistentStorage { - coconut_credential_manager: CoconutCredentialManager, -} - -impl PersistentStorage { - /// Initialises `PersistentStorage` using the provided path. - /// - /// # Arguments - /// - /// * `database_path`: path to the database. - pub async fn init>(database_path: P) -> Result { - debug!( - "Attempting to connect to database {:?}", - database_path.as_ref().as_os_str() - ); - - let mut opts = sqlx::sqlite::SqliteConnectOptions::new() - .filename(database_path) - .create_if_missing(true); - - opts.disable_statement_logging(); - - let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { - Ok(db) => db, - Err(err) => { - error!("Failed to connect to SQLx database: {err}"); - return Err(err.into()); - } - }; - - if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await { - error!("Failed to perform migration on the SQLx database: {err}"); - return Err(err.into()); - } - - Ok(PersistentStorage { - coconut_credential_manager: CoconutCredentialManager::new(connection_pool.clone()), - }) - } -} - -#[async_trait] -impl Storage for PersistentStorage { - type StorageError = StorageError; - - async fn insert_issued_credential<'a>( - &self, - bandwidth_credential: StorableIssuedCredential<'a>, - ) -> Result<(), Self::StorageError> { - self.coconut_credential_manager - .insert_issued_credential( - bandwidth_credential.credential_type, - bandwidth_credential.serialization_revision, - bandwidth_credential.credential_data, - bandwidth_credential.epoch_id, - ) - .await - .map_err(|err| { - // There is one error we want to handle specifically. - // Check if database_error is `SqliteError` with code 2067 which - // means UNIQUE constraint violation - if let Some(db_error) = err.as_database_error() { - if db_error.code().map_or(false, |code| code == "2067") { - StorageError::ConstraintUnique - } else { - err.into() - } - } else { - err.into() - } - }) - } - - async fn get_next_unspent_credential( - &self, - gateway_id: &str, - ) -> Result, Self::StorageError> { - // first try to get a free pass if available, otherwise fallback to bandwidth voucher - let maybe_freepass = self - .coconut_credential_manager - .get_next_unspect_freepass(gateway_id) - .await?; - if maybe_freepass.is_some() { - return Ok(maybe_freepass); - } - - Ok(self - .coconut_credential_manager - .get_next_unspect_bandwidth_voucher() - .await?) - } - - async fn consume_coconut_credential( - &self, - id: i64, - gateway_id: &str, - ) -> Result<(), StorageError> { - self.coconut_credential_manager - .consume_coconut_credential(id, gateway_id) - .await?; - - Ok(()) - } - - async fn mark_expired(&self, id: i64) -> Result<(), Self::StorageError> { - self.coconut_credential_manager.mark_expired(id).await?; - - Ok(()) - } -} diff --git a/common/credential-storage/src/persistent_storage/helpers.rs b/common/credential-storage/src/persistent_storage/helpers.rs new file mode 100644 index 0000000000..614800d609 --- /dev/null +++ b/common/credential-storage/src/persistent_storage/helpers.rs @@ -0,0 +1,54 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::StorageError; +use bincode::Options; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize)] +struct StorageBorrowedSerdeWrapper<'a, T>(&'a T); + +#[derive(Serialize, Deserialize)] +struct StorageSerdeWrapper(T); + +pub(crate) fn serialise_coin_index_signatures(sigs: &[AnnotatedCoinIndexSignature]) -> Vec { + storage_serialiser() + .serialize(&StorageBorrowedSerdeWrapper(&sigs)) + .unwrap() +} + +pub(crate) fn deserialise_coin_index_signatures( + raw: &[u8], +) -> Result, StorageError> { + let de: StorageSerdeWrapper<_> = storage_serialiser().deserialize(raw).map_err(|_| { + StorageError::database_inconsistency("malformed stored coin index signatures") + })?; + Ok(de.0) +} + +pub(crate) fn serialise_expiration_date_signatures( + sigs: &[AnnotatedExpirationDateSignature], +) -> Vec { + storage_serialiser() + .serialize(&StorageBorrowedSerdeWrapper(&sigs)) + .unwrap() +} + +pub(crate) fn deserialise_expiration_date_signatures( + raw: &[u8], +) -> Result, StorageError> { + let de: StorageSerdeWrapper<_> = storage_serialiser().deserialize(raw).map_err(|_| { + StorageError::database_inconsistency("malformed expiration date signatures") + })?; + Ok(de.0) +} + +// storage serialiser used for non-critical data, such as global expiration signatures or master verification keys, +// i.e. data that could always be queried for again if malformed +fn storage_serialiser() -> impl bincode::Options { + bincode::DefaultOptions::new() + .with_big_endian() + .with_varint_encoding() +} diff --git a/common/credential-storage/src/persistent_storage/mod.rs b/common/credential-storage/src/persistent_storage/mod.rs new file mode 100644 index 0000000000..3940d82c3c --- /dev/null +++ b/common/credential-storage/src/persistent_storage/mod.rs @@ -0,0 +1,308 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::backends::sqlite::{ + get_next_unspent_ticketbook, increase_used_ticketbook_tickets, SqliteEcashTicketbookManager, +}; +use crate::error::StorageError; +use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook}; +use crate::persistent_storage::helpers::{ + deserialise_coin_index_signatures, deserialise_expiration_date_signatures, + serialise_coin_index_signatures, serialise_expiration_date_signatures, +}; +use crate::storage::Storage; +use async_trait::async_trait; +use log::{debug, error}; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use nym_compact_ecash::VerificationKeyAuth; +use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise; +use nym_credentials::{IssuanceTicketBook, IssuedTicketBook}; +use nym_ecash_time::{ecash_today, Date, EcashTime}; +use sqlx::ConnectOptions; +use std::path::Path; +use zeroize::Zeroizing; + +mod helpers; + +// note that clone here is fine as upon cloning the same underlying pool will be used +#[derive(Clone)] +pub struct PersistentStorage { + storage_manager: SqliteEcashTicketbookManager, +} + +impl PersistentStorage { + /// Initialises `PersistentStorage` using the provided path. + /// + /// # Arguments + /// + /// * `database_path`: path to the database. + pub async fn init>(database_path: P) -> Result { + debug!( + "Attempting to connect to database {:?}", + database_path.as_ref().as_os_str() + ); + + let mut opts = sqlx::sqlite::SqliteConnectOptions::new() + .filename(database_path) + .create_if_missing(true); + + opts.disable_statement_logging(); + + let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { + Ok(db) => db, + Err(err) => { + error!("Failed to connect to SQLx database: {err}"); + return Err(err.into()); + } + }; + + if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await { + error!("Failed to perform migration on the SQLx database: {err}"); + return Err(err.into()); + } + + Ok(PersistentStorage { + storage_manager: SqliteEcashTicketbookManager::new(connection_pool.clone()), + }) + } +} + +#[async_trait] +impl Storage for PersistentStorage { + type StorageError = StorageError; + + /// remove all expired ticketbooks and expiration date signatures + async fn cleanup_expired(&self) -> Result<(), Self::StorageError> { + let ecash_yesterday = ecash_today().date().previous_day().unwrap(); + self.storage_manager + .cleanup_expired(ecash_yesterday) + .await?; + Ok(()) + } + + async fn insert_pending_ticketbook( + &self, + ticketbook: &IssuanceTicketBook, + ) -> Result<(), Self::StorageError> { + let ser = ticketbook.pack(); + let data = Zeroizing::new(ser.data); + let serialisation_revision = ser.revision; + + self.storage_manager + .insert_pending_ticketbook( + serialisation_revision, + ticketbook.deposit_id(), + &data, + ticketbook.expiration_date(), + ) + .await?; + + Ok(()) + } + + async fn insert_issued_ticketbook( + &self, + ticketbook: &IssuedTicketBook, + ) -> Result<(), Self::StorageError> { + let ser = ticketbook.pack(); + let data = Zeroizing::new(ser.data); + let serialisation_revision = ser.revision; + + self.storage_manager + .insert_new_ticketbook( + serialisation_revision, + &data, + ticketbook.expiration_date(), + &ticketbook.ticketbook_type().to_string(), + ticketbook.epoch_id() as u32, + ticketbook.params_total_tickets() as u32, + ticketbook.spent_tickets() as u32, + ) + .await?; + + Ok(()) + } + + async fn get_ticketbooks_info( + &self, + ) -> Result, Self::StorageError> { + Ok(self.storage_manager.get_ticketbooks_info().await?) + } + + async fn get_pending_ticketbooks( + &self, + ) -> Result, Self::StorageError> { + let pending = self + .storage_manager + .get_pending_ticketbooks() + .await? + .into_iter() + .map(|p| { + IssuanceTicketBook::try_unpack(&p.pending_ticketbook_data, p.serialization_revision) + .map_err(|err| { + StorageError::database_inconsistency(format!( + "failed to deserialise stored pending ticketbook: {err}" + )) + }) + .map(|pending_ticketbook| RetrievedPendingTicketbook { + pending_id: p.deposit_id, + pending_ticketbook, + }) + }) + .collect::>()?; + Ok(pending) + } + + async fn remove_pending_ticketbook(&self, pending_id: i64) -> Result<(), Self::StorageError> { + self.storage_manager + .remove_pending_ticketbook(pending_id) + .await?; + Ok(()) + } + + /// Tries to retrieve one of the stored ticketbook, + /// that has not yet expired and has required number of unspent tickets. + /// it immediately updated the on-disk number of used tickets so that another task + /// could obtain their own tickets at the same time + async fn get_next_unspent_usable_ticketbook( + &self, + tickets: u32, + ) -> Result, Self::StorageError> { + let deadline = ecash_today().ecash_date(); + let mut tx = self.storage_manager.begin_storage_tx().await?; + + // we don't want ticketbooks with expiration in the past + let Some(raw) = get_next_unspent_ticketbook(&mut tx, deadline, tickets).await? else { + // make sure to finish our tx + tx.commit().await?; + return Ok(None); + }; + + let mut deserialised = + IssuedTicketBook::try_unpack(&raw.ticketbook_data, raw.serialization_revision) + .map_err(|err| { + StorageError::database_inconsistency(format!( + "failed to deserialise stored ticketbook: {err}" + )) + })?; + + increase_used_ticketbook_tickets(&mut tx, raw.id, tickets).await?; + tx.commit().await?; + + // set the number of spent tickets on the crypto object + // TODO: I don't like how that's required and can be easily missed, + // perhaps we shouldn't be storing the `IssuedTicketBook` data in the db, + // but all of its fields instead? + deserialised.update_spent_tickets(raw.used_tickets as u64); + Ok(Some(RetrievedTicketbook { + ticketbook_id: raw.id, + ticketbook: deserialised, + })) + } + + async fn attempt_revert_ticketbook_withdrawal( + &self, + ticketbook_id: i64, + withdrawn: u32, + expected_current_total_spent: u32, + ) -> Result { + Ok(self + .storage_manager + .decrease_used_ticketbook_tickets( + ticketbook_id, + withdrawn, + expected_current_total_spent, + ) + .await?) + } + + async fn get_master_verification_key( + &self, + epoch_id: u64, + ) -> Result, Self::StorageError> { + let Some(raw) = self + .storage_manager + .get_master_verification_key(epoch_id as i64) + .await? + else { + return Ok(None); + }; + + let master_vk = VerificationKeyAuth::from_bytes(&raw).map_err(|_| { + StorageError::database_inconsistency("malformed stored master verification key") + })?; + + Ok(Some(master_vk)) + } + + async fn insert_master_verification_key( + &self, + epoch_id: u64, + key: &VerificationKeyAuth, + ) -> Result<(), Self::StorageError> { + Ok(self + .storage_manager + .insert_master_verification_key(epoch_id as i64, &key.to_bytes()) + .await?) + } + + async fn get_coin_index_signatures( + &self, + epoch_id: u64, + ) -> Result>, Self::StorageError> { + let Some(raw) = self + .storage_manager + .get_coin_index_signatures(epoch_id as i64) + .await? + else { + return Ok(None); + }; + + Ok(Some(deserialise_coin_index_signatures(&raw)?)) + } + + async fn insert_coin_index_signatures( + &self, + epoch_id: u64, + sigs: &[AnnotatedCoinIndexSignature], + ) -> Result<(), Self::StorageError> { + self.storage_manager + .insert_coin_index_signatures(epoch_id as i64, &serialise_coin_index_signatures(sigs)) + .await?; + Ok(()) + } + + async fn get_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result>, Self::StorageError> { + let Some(raw) = self + .storage_manager + .get_expiration_date_signatures(expiration_date) + .await? + else { + return Ok(None); + }; + + Ok(Some(deserialise_expiration_date_signatures( + &raw.serialised_signatures, + )?)) + } + + async fn insert_expiration_date_signatures( + &self, + epoch_id: u64, + expiration_date: Date, + sigs: &[AnnotatedExpirationDateSignature], + ) -> Result<(), Self::StorageError> { + self.storage_manager + .insert_expiration_date_signatures( + epoch_id as i64, + expiration_date, + &serialise_expiration_date_signatures(sigs), + ) + .await?; + Ok(()) + } +} diff --git a/common/credential-storage/src/storage.rs b/common/credential-storage/src/storage.rs index 7880a3722e..8b3ebc74ec 100644 --- a/common/credential-storage/src/storage.rs +++ b/common/credential-storage/src/storage.rs @@ -1,42 +1,93 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::models::{StorableIssuedCredential, StoredIssuedCredential}; +use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook}; use async_trait::async_trait; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use nym_compact_ecash::VerificationKeyAuth; +use nym_credentials::{IssuanceTicketBook, IssuedTicketBook}; +use nym_ecash_time::Date; use std::error::Error; +// for future reference, if you want to make a query for "how much bandwidth do we have left" +// do something along the lines of +// `SELECT total_tickets, used_tickets FROM ecash_ticketbook WHERE expiration_date >= ?`, today_date +// then for each calculate the diff total_tickets - used_tickets and multiply the result by the size of the ticket #[async_trait] pub trait Storage: Send + Sync { type StorageError: Error; - async fn insert_issued_credential<'a>( + /// remove all expired ticketbooks and expiration date signatures + async fn cleanup_expired(&self) -> Result<(), Self::StorageError>; + + async fn insert_pending_ticketbook( &self, - bandwidth_credential: StorableIssuedCredential<'a>, + ticketbook: &IssuanceTicketBook, ) -> Result<(), Self::StorageError>; - /// Tries to retrieve one of the stored, unused credentials, - /// that is also not marked as expired - async fn get_next_unspent_credential( + async fn insert_issued_ticketbook( &self, - gateway_id: &str, - ) -> Result, Self::StorageError>; - - /// Marks as consumed in the database the specified credential. - /// - /// # Arguments - /// - /// * `id`: Id of the credential to be consumed. - /// * `gateway_id`: id of the gateway that received the credential. - async fn consume_coconut_credential( - &self, - id: i64, - gateway_id: &str, + ticketbook: &IssuedTicketBook, ) -> Result<(), Self::StorageError>; - /// Marks the specified credential as expired - /// - /// # Arguments - /// - /// * `id`: Id of the credential to mark as expired. - async fn mark_expired(&self, id: i64) -> Result<(), Self::StorageError>; + async fn get_ticketbooks_info( + &self, + ) -> Result, Self::StorageError>; + + async fn get_pending_ticketbooks( + &self, + ) -> Result, Self::StorageError>; + + async fn remove_pending_ticketbook(&self, pending_id: i64) -> Result<(), Self::StorageError>; + + /// Tries to retrieve one of the stored ticketbook, + /// that has not yet expired and has required number of unspent tickets. + /// it immediately updated the on-disk number of used tickets so that another task + /// could obtain their own tickets at the same time + async fn get_next_unspent_usable_ticketbook( + &self, + tickets: u32, + ) -> Result, Self::StorageError>; + + async fn attempt_revert_ticketbook_withdrawal( + &self, + ticketbook_id: i64, + withdrawn: u32, + expected_current_total_spent: u32, + ) -> Result; + + async fn get_master_verification_key( + &self, + epoch_id: u64, + ) -> Result, Self::StorageError>; + + async fn insert_master_verification_key( + &self, + epoch_id: u64, + key: &VerificationKeyAuth, + ) -> Result<(), Self::StorageError>; + + async fn get_coin_index_signatures( + &self, + epoch_id: u64, + ) -> Result>, Self::StorageError>; + + async fn insert_coin_index_signatures( + &self, + epoch_id: u64, + data: &[AnnotatedCoinIndexSignature], + ) -> Result<(), Self::StorageError>; + + async fn get_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result>, Self::StorageError>; + + async fn insert_expiration_date_signatures( + &self, + epoch_id: u64, + expiration_date: Date, + data: &[AnnotatedExpirationDateSignature], + ) -> Result<(), Self::StorageError>; } diff --git a/common/credential-utils/Cargo.toml b/common/credential-utils/Cargo.toml index 232c9cdb1f..81ba19a85b 100644 --- a/common/credential-utils/Cargo.toml +++ b/common/credential-utils/Cargo.toml @@ -10,11 +10,13 @@ license.workspace = true log = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } +time.workspace = true nym-bandwidth-controller = { path = "../../common/bandwidth-controller" } -nym-coconut = { path = "../nymcoconut" } nym-credentials = { path = "../../common/credentials" } -nym-credential-storage = { path = "../../common/credential-storage" } +nym-credentials-interface = { path = "../../common/credentials-interface" } +nym-credential-storage = { path = "../../common/credential-storage", features = ["persistent-storage"] } nym-validator-client = { path = "../../common/client-libs/validator-client" } nym-config = { path = "../../common/config" } nym-client-core = { path = "../../common/client-core" } +nym-ecash-time = { path = "../../common/ecash-time" } \ No newline at end of file diff --git a/common/credential-utils/src/errors.rs b/common/credential-utils/src/errors.rs index 6407929a99..456f542180 100644 --- a/common/credential-utils/src/errors.rs +++ b/common/credential-utils/src/errors.rs @@ -3,6 +3,7 @@ use nym_credential_storage::error::StorageError; use nym_credentials::error::Error as CredentialError; +use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nyxd::error::NyxdError; use std::num::ParseIntError; use thiserror::Error; @@ -17,15 +18,30 @@ pub enum Error { #[error(transparent)] BandwidthControllerError(#[from] nym_bandwidth_controller::error::BandwidthControllerError), + #[error(transparent)] + EcashApiError(#[from] EcashApiError), + #[error(transparent)] Nyxd(#[from] NyxdError), #[error(transparent)] Credential(#[from] CredentialError), - #[error("Could not use shared storage: {0}")] - SharedStorageError(#[from] StorageError), + #[error("could not use shared storage: {0}")] + SharedStorageError(Box), #[error("failed to parse credential value: {0}")] MalformedCredentialValue(#[from] ParseIntError), } + +impl Error { + pub fn storage_error(source: impl std::error::Error + Send + Sync + 'static) -> Self { + Error::SharedStorageError(Box::new(source)) + } +} + +impl From for Error { + fn from(value: StorageError) -> Self { + Self::storage_error(value) + } +} diff --git a/common/credential-utils/src/lib.rs b/common/credential-utils/src/lib.rs index 6e5fbc3158..60bd6d2bcf 100644 --- a/common/credential-utils/src/lib.rs +++ b/common/credential-utils/src/lib.rs @@ -1,5 +1,4 @@ pub mod errors; -pub mod recovery_storage; pub mod utils; pub use errors::{Error, Result}; diff --git a/common/credential-utils/src/recovery_storage.rs b/common/credential-utils/src/recovery_storage.rs deleted file mode 100644 index 0344a3ad86..0000000000 --- a/common/credential-utils/src/recovery_storage.rs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::errors::Result; -use log::error; -use nym_credentials::coconut::bandwidth::IssuanceBandwidthCredential; -use std::fs::{create_dir_all, read_dir, File}; -use std::io::{Read, Write}; -use std::path::PathBuf; - -pub const DUMPED_VOUCHER_EXTENSION: &str = "credentialrecovery"; - -pub struct RecoveryStorage { - recovery_dir: PathBuf, -} - -impl RecoveryStorage { - pub fn new(recovery_dir: PathBuf) -> Result { - create_dir_all(&recovery_dir)?; - Ok(Self { recovery_dir }) - } - - pub fn unconsumed_vouchers(&self) -> Result> { - let entries = read_dir(&self.recovery_dir)?; - - let mut paths = vec![]; - for entry in entries.flatten() { - let path = entry.path(); - if let Some(extension) = path.extension() { - if extension == DUMPED_VOUCHER_EXTENSION { - paths.push(path) - } - } - } - - let mut vouchers = vec![]; - for path in paths { - if let Ok(mut file) = File::open(&path) { - let mut buff = Vec::new(); - if file.read_to_end(&mut buff).is_ok() { - match IssuanceBandwidthCredential::try_from_recovered_bytes(&buff) { - Ok(voucher) => vouchers.push(voucher), - Err(err) => { - error!("failed to parse the voucher at {}: {err}", path.display()) - } - } - } - } - } - - Ok(vouchers) - } - - pub fn voucher_filename(voucher: &IssuanceBandwidthCredential) -> String { - let prefix = voucher.typ().to_string(); - let suffix = voucher.blinded_serial_number_bs58(); - format!("{prefix}-{suffix}.{DUMPED_VOUCHER_EXTENSION}") - } - - pub fn insert_voucher(&self, voucher: &IssuanceBandwidthCredential) -> Result { - let file_name = Self::voucher_filename(voucher); - let file_path = self.recovery_dir.join(file_name); - let mut file = File::create(&file_path)?; - let buff = voucher.to_recovery_bytes(); - file.write_all(&buff)?; - - Ok(file_path) - } - - pub fn remove_voucher(&self, file_name: String) -> Result<()> { - let file_path = self.recovery_dir.join(file_name); - Ok(std::fs::remove_file(file_path)?) - } -} diff --git a/common/credential-utils/src/utils.rs b/common/credential-utils/src/utils.rs index 33a0b20541..be7133b4c2 100644 --- a/common/credential-utils/src/utils.rs +++ b/common/credential-utils/src/utils.rs @@ -1,74 +1,79 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + use crate::errors::{Error, Result}; -use crate::recovery_storage::RecoveryStorage; use log::*; -use nym_bandwidth_controller::acquire::state::State; +use nym_bandwidth_controller::acquire::{get_ticket_book, query_and_persist_required_global_data}; use nym_client_core::config::disk_persistence::CommonClientPaths; use nym_config::DEFAULT_DATA_DIR; use nym_credential_storage::persistent_storage::PersistentStorage; -use nym_credentials::coconut::bandwidth::CredentialType; +use nym_credential_storage::storage::Storage; +use nym_credentials_interface::TicketType; +use nym_ecash_time::ecash_default_expiration_date; +use nym_validator_client::coconut::all_ecash_api_clients; use nym_validator_client::nyxd::contract_traits::{ - dkg_query_client::EpochState, CoconutBandwidthSigningClient, DkgQueryClient, + dkg_query_client::EpochState, DkgQueryClient, EcashQueryClient, EcashSigningClient, }; -use nym_validator_client::nyxd::Coin; use std::path::PathBuf; -use std::time::{Duration, SystemTime}; +use std::time::Duration; +use time::OffsetDateTime; -pub async fn issue_credential( +pub async fn issue_credential( client: &C, - amount: Coin, - persistent_storage: &PersistentStorage, - recovery_storage_path: PathBuf, + storage: &S, + client_id: &[u8], + typ: TicketType, ) -> Result<()> where - C: DkgQueryClient + CoconutBandwidthSigningClient + Send + Sync, + C: DkgQueryClient + EcashSigningClient + EcashQueryClient + Send + Sync, + S: Storage, + ::StorageError: Send + Sync + 'static, { - let recovery_storage = setup_recovery_storage(recovery_storage_path).await; - - block_until_coconut_is_available(client).await?; + block_until_ecash_is_available(client).await?; info!("Starting to deposit funds, don't kill the process"); - if let Ok(recovered_amount) = - recover_credentials(client, &recovery_storage, persistent_storage).await - { - if recovered_amount != 0 { - info!( - "Recovered credentials in the amount of {}", - recovered_amount - ); + if let Ok(recovered_ticketbooks) = recover_deposits(client, storage).await { + if recovered_ticketbooks != 0 { + info!("managed to recover {recovered_ticketbooks} ticket books. no need to make fresh deposit"); return Ok(()); } }; - let state = nym_bandwidth_controller::acquire::deposit(client, amount.clone()).await?; + let epoch_id = client.get_current_epoch().await?.epoch_id; + let apis = all_ecash_api_clients(client, epoch_id).await?; + let ticketbook_expiration = ecash_default_expiration_date(); - if nym_bandwidth_controller::acquire::get_bandwidth_voucher(&state, client, persistent_storage) + // make sure we have all required coin indices and expiration date signatures alongside the master verification key + // before attempting the deposit + query_and_persist_required_global_data(storage, epoch_id, ticketbook_expiration, apis.clone()) + .await?; + + let issuance_data = nym_bandwidth_controller::acquire::make_deposit( + client, + client_id, + Some(ticketbook_expiration), + typ, + ) + .await?; + info!("Deposit done"); + + if get_ticket_book(&issuance_data, client, storage, Some(apis)) .await .is_err() { - warn!("Failed to obtain credential. Dumping recovery data.",); - match recovery_storage.insert_voucher(&state.voucher) { - Ok(file_path) => { - warn!("Dumped recovery data to {}. Try using recovery mode to convert it to a credential", file_path.to_str().unwrap()); - } - Err(e) => { - error!("Could not dump recovery data to file system due to {:?}, the deposit will be lost!", e) - } - } + error!("failed to obtain credential. saving recovery data..."); - return Err(Error::Credential( - nym_credentials::error::Error::BandwidthCredentialError, - )); + storage.insert_pending_ticketbook(&issuance_data).await.inspect_err(|err| { + let deposit = issuance_data.deposit_id(); + error!("could not save the recovery data for deposit {deposit}: {err}. the data will unfortunately get lost") + }).map_err(Error::storage_error)? } - info!("Succeeded adding a credential with amount {amount}"); + info!("Succeeded adding a ticketbook of type '{typ}'"); Ok(()) } -pub async fn setup_recovery_storage(recovery_dir: PathBuf) -> RecoveryStorage { - RecoveryStorage::new(recovery_dir).expect("") -} - pub async fn setup_persistent_storage(client_home_directory: PathBuf) -> PersistentStorage { let data_dir = client_home_directory.join(DEFAULT_DATA_DIR); let paths = CommonClientPaths::new_base(data_dir); @@ -77,16 +82,13 @@ pub async fn setup_persistent_storage(client_home_directory: PathBuf) -> Persist nym_credential_storage::initialise_persistent_storage(db_path).await } -pub async fn block_until_coconut_is_available(client: &C) -> Result<()> +pub async fn block_until_ecash_is_available(client: &C) -> Result<()> where C: DkgQueryClient + Send + Sync, { loop { let epoch = client.get_current_epoch().await?; - let current_timestamp_secs = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("the system clock is set to 01/01/1970 (or earlier)") - .as_secs(); + let current_timestamp_secs = OffsetDateTime::now_utc().unix_timestamp() as u64; if epoch.state.is_final() { break; @@ -101,7 +103,7 @@ where } else { // this should never be the case since the only case where final timestamp is unknown is when it's waiting for initialisation, // but let's guard ourselves against future changes - info!("it is unknown when coconut will be come available. Going to check again later"); + info!("it is unknown when ecash will be come available. Going to check again later"); tokio::time::sleep(Duration::from_secs(60 * 5)).await; } } @@ -109,42 +111,47 @@ where Ok(()) } -pub async fn recover_credentials( - client: &C, - recovery_storage: &RecoveryStorage, - shared_storage: &PersistentStorage, -) -> Result +pub async fn recover_deposits(client: &C, storage: &S) -> Result where C: DkgQueryClient + Send + Sync, + S: Storage, + ::StorageError: Send + Sync + 'static, { - let mut recovered_amount: u128 = 0; - for voucher in recovery_storage.unconsumed_vouchers()? { - let voucher_value = match voucher.typ() { - CredentialType::Voucher => voucher.get_bandwidth_attribute(), - CredentialType::FreePass => { - error!("unimplemented recovery of free pass credentials"); - continue; - } - }; - recovered_amount += voucher_value.parse::()?; + info!("checking for any incomplete previous issuance attempts..."); - let voucher_name = RecoveryStorage::voucher_filename(&voucher); - let state = State::new(voucher); + let incomplete = storage + .get_pending_ticketbooks() + .await + .map_err(Error::storage_error)?; + info!( + "we recovered {} incomplete ticketbook issuances", + incomplete.len() + ); - if let Err(e) = - nym_bandwidth_controller::acquire::get_bandwidth_voucher(&state, client, shared_storage) - .await - { - error!("Could not recover deposit {voucher_name} due to {e}, try again later",) - } else { - info!( - "Converted deposit {voucher_name} to a credential, removing recovery data for it", - ); - if let Err(err) = recovery_storage.remove_voucher(voucher_name) { - warn!("Could not remove recovery data: {err}"); + let mut recovered_books = 0; + for issuance in incomplete { + let deposit = issuance.pending_ticketbook.deposit_id(); + if issuance.pending_ticketbook.expired() { + warn!("ticketbook data associated with deposit {deposit} has expired. if you haven't contacted more than 1/3 of signers. it could still be recoverable (but out of scope of this library)"); + continue; + } + + if issuance.pending_ticketbook.check_expiration_date() { + warn!("deposit {deposit} was made with a different expiration date, it's validity will be shorter than the max one"); + } + + match get_ticket_book(&issuance.pending_ticketbook, client, storage, None).await { + Err(err) => error!("could not recover deposit {deposit} due to: {err}"), + Ok(_) => { + info!("managed to recover deposit {deposit}! the ticketbook has been added to the storage"); + storage + .remove_pending_ticketbook(issuance.pending_id) + .await + .map_err(Error::storage_error)?; + recovered_books += 1; } } } - Ok(recovered_amount) + Ok(recovered_books) } diff --git a/common/credentials-interface/Cargo.toml b/common/credentials-interface/Cargo.toml index 5393ea37fd..2142acf14a 100644 --- a/common/credentials-interface/Cargo.toml +++ b/common/credentials-interface/Cargo.toml @@ -14,5 +14,10 @@ license.workspace = true bls12_381 = { workspace = true, default-features = false } serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } +strum = { workspace = true, features = ["derive"] } +time = { workspace = true, features = ["serde"] } +rand = { workspace = true } -nym-coconut = { path = "../nymcoconut" } +nym-compact-ecash = { path = "../nym_offline_compact_ecash" } +nym-ecash-time = { path = "../ecash-time" } +nym-network-defaults = { path = "../network-defaults" } diff --git a/common/credentials-interface/src/lib.rs b/common/credentials-interface/src/lib.rs index 1337e633d5..85938c3c61 100644 --- a/common/credentials-interface/src/lib.rs +++ b/common/credentials-interface/src/lib.rs @@ -1,136 +1,330 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use bls12_381::Scalar; +use nym_network_defaults::TicketTypeRepr; +use rand::Rng; use serde::{Deserialize, Serialize}; -use std::fmt::{Display, Formatter}; -use std::str::FromStr; use thiserror::Error; +use time::{Date, OffsetDateTime}; -pub use nym_coconut::{ - aggregate_signature_shares, aggregate_signature_shares_and_verify, aggregate_verification_keys, - blind_sign, hash_to_scalar, keygen, prepare_blind_sign, prove_bandwidth_credential, - verify_credential, Attribute, Base58, BlindSignRequest, BlindedSerialNumber, BlindedSignature, - Bytable, CoconutError, KeyPair, Parameters, PrivateAttribute, PublicAttribute, SecretKey, - Signature, SignatureShare, VerificationKey, VerifyCredentialRequest, +pub use nym_compact_ecash::{ + aggregate_verification_keys, aggregate_wallets, constants, ecash_parameters, + error::CompactEcashError, + generate_keypair_user, generate_keypair_user_from_seed, issue_verify, + scheme::coin_indices_signatures::aggregate_indices_signatures, + scheme::coin_indices_signatures::{ + AnnotatedCoinIndexSignature, CoinIndexSignature, CoinIndexSignatureShare, + PartialCoinIndexSignature, + }, + scheme::expiration_date_signatures::aggregate_expiration_signatures, + scheme::expiration_date_signatures::{ + AnnotatedExpirationDateSignature, ExpirationDateSignature, ExpirationDateSignatureShare, + PartialExpirationDateSignature, + }, + scheme::keygen::KeyPairUser, + scheme::withdrawal::RequestInfo, + scheme::Payment, + scheme::{Wallet, WalletSignatures}, + withdrawal_request, Base58, BlindedSignature, Bytable, EncodedDate, EncodedTicketType, + PartialWallet, PayInfo, PublicKeyUser, SecretKeyUser, VerificationKeyAuth, WithdrawalRequest, }; - -pub const VOUCHER_INFO_TYPE: &str = "BandwidthVoucher"; -pub const FREE_PASS_INFO_TYPE: &str = "FreeBandwidthPass"; - -// pub trait NymCredential { -// fn prove_credential(&self) -> Result<(), ()>; -// } - -#[derive(Debug, Error)] -#[error("{0} is not a valid credential type")] -pub struct UnknownCredentialType(String); - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] -pub enum CredentialType { - Voucher, - FreePass, -} - -impl FromStr for CredentialType { - type Err = UnknownCredentialType; - - fn from_str(s: &str) -> Result { - if s == VOUCHER_INFO_TYPE { - Ok(CredentialType::Voucher) - } else if s == FREE_PASS_INFO_TYPE { - Ok(CredentialType::FreePass) - } else { - Err(UnknownCredentialType(s.to_string())) - } - } -} - -impl CredentialType { - pub fn validate(&self, type_plain: &str) -> bool { - match self { - CredentialType::Voucher => type_plain == VOUCHER_INFO_TYPE, - CredentialType::FreePass => type_plain == FREE_PASS_INFO_TYPE, - } - } - - pub fn is_free_pass(&self) -> bool { - matches!(self, CredentialType::FreePass) - } - - pub fn is_voucher(&self) -> bool { - matches!(self, CredentialType::Voucher) - } -} - -impl Display for CredentialType { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - CredentialType::Voucher => VOUCHER_INFO_TYPE.fmt(f), - CredentialType::FreePass => FREE_PASS_INFO_TYPE.fmt(f), - } - } -} +use nym_ecash_time::{ecash_today, EcashTime}; #[derive(Debug, Clone)] pub struct CredentialSigningData { - pub pedersen_commitments_openings: Vec, + pub withdrawal_request: WithdrawalRequest, - pub blind_sign_request: BlindSignRequest, + pub request_info: RequestInfo, - pub public_attributes_plain: Vec, + pub ecash_pub_key: PublicKeyUser, - pub typ: CredentialType, + pub expiration_date: Date, + + pub ticketbook_type: TicketType, } -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] pub struct CredentialSpendingData { - pub embedded_private_attributes: usize, + pub payment: Payment, - pub verify_credential_request: VerifyCredentialRequest, + pub pay_info: PayInfo, - pub public_attributes_plain: Vec, - - pub typ: CredentialType, + pub spend_date: Date, + // pub value: u64, /// The (DKG) epoch id under which the credential has been issued so that the verifier could use correct verification key for validation. pub epoch_id: u64, } impl CredentialSpendingData { - pub fn verify(&self, params: &Parameters, verification_key: &VerificationKey) -> bool { - let hashed_public_attributes = self - .public_attributes_plain - .iter() - .map(hash_to_scalar) - .collect::>(); - - // get references to the attributes - let public_attributes = hashed_public_attributes.iter().collect::>(); - - verify_credential( - params, + pub fn verify(&self, verification_key: &VerificationKeyAuth) -> Result<(), CompactEcashError> { + self.payment.spend_verify( verification_key, - &self.verify_credential_request, - &public_attributes, + &self.pay_info, + self.spend_date.ecash_unix_timestamp(), ) } - pub fn validate_type_attribute(&self) -> bool { - // the first attribute is variant specific bandwidth encoding, the second one should be the type - let Some(type_plain) = self.public_attributes_plain.get(1) else { - return false; + pub fn encoded_serial_number(&self) -> Vec { + self.payment.encoded_serial_number() + } + + pub fn serial_number_b58(&self) -> String { + self.payment.serial_number_bs58() + } + + pub fn to_bytes(&self) -> Vec { + // simple length prefixed serialization + // TODO: change it to a standard format instead + let mut bytes = Vec::new(); + let payment_bytes = self.payment.to_bytes(); + + bytes.extend_from_slice(&(payment_bytes.len() as u32).to_be_bytes()); + bytes.extend_from_slice(&payment_bytes); + bytes.extend_from_slice(&self.pay_info.pay_info_bytes); //this is 72 bytes long + bytes.extend_from_slice(&self.spend_date.to_julian_day().to_be_bytes()); + bytes.extend_from_slice(&self.epoch_id.to_be_bytes()); + + bytes + } + + pub fn try_from_bytes(raw: &[u8]) -> Result { + // minimum length: 72 (pay_info) + 8 (epoch_id) + 4 (spend date) + 4 (payment length prefix) + if raw.len() < 72 + 8 + 4 + 4 { + return Err(CompactEcashError::DeserializationFailure { + object: "EcashCredential".into(), + }); + } + let mut index = 0; + //SAFETY : casting a slice of length 4 into an array of size 4 + let payment_len = u32::from_be_bytes(raw[index..index + 4].try_into().unwrap()) as usize; + index += 4; + + if raw[index..].len() != payment_len + 84 { + return Err(CompactEcashError::DeserializationFailure { + object: "EcashCredential".into(), + }); + } + let payment = Payment::try_from(&raw[index..index + payment_len])?; + index += payment_len; + + let pay_info = PayInfo { + //SAFETY : casting a slice of length 72 into an array of size 72 + pay_info_bytes: raw[index..index + 72].try_into().unwrap(), }; + index += 72; - self.typ.validate(type_plain) - } + //SAFETY : casting a slice of length 4 into an array of size 4 + let spend_date_julian = i32::from_be_bytes(raw[index..index + 4].try_into().unwrap()); + let spend_date = Date::from_julian_day(spend_date_julian).map_err(|_| { + CompactEcashError::DeserializationFailure { + object: "CredentialSpendingData".into(), + } + })?; + index += 4; - pub fn get_bandwidth_attribute(&self) -> Option<&String> { - // the first attribute is variant specific bandwidth encoding, the second one should be the type - self.public_attributes_plain.first() - } + if raw[index..].len() != 8 { + return Err(CompactEcashError::DeserializationFailure { + object: "EcashCredential".into(), + }); + } - pub fn blinded_serial_number(&self) -> BlindedSerialNumber { - self.verify_credential_request.blinded_serial_number() + //SAFETY : casting a slice of length 8 into an array of size 8 + let epoch_id = u64::from_be_bytes(raw[index..].try_into().unwrap()); + + Ok(CredentialSpendingData { + payment, + pay_info, + spend_date, + epoch_id, + }) + } +} + +impl Bytable for CredentialSpendingData { + fn to_byte_vec(&self) -> Vec { + self.to_bytes() + } + + fn try_from_byte_slice(slice: &[u8]) -> Result { + Self::try_from_bytes(slice) + } +} + +impl Base58 for CredentialSpendingData {} + +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub struct NymPayInfo { + randomness: [u8; 32], + timestamp: i64, + provider_public_key: [u8; 32], +} + +impl NymPayInfo { + /// Generates a new `NymPayInfo` instance with random bytes, a timestamp, and a provider public key. + /// + /// # Arguments + /// + /// * `provider_pk` - The public key of the payment provider. + /// + /// # Returns + /// + /// A new `NymPayInfo` instance. + /// + pub fn generate(provider_pk: [u8; 32]) -> Self { + let mut randomness = [0u8; 32]; + rand::thread_rng().fill(&mut randomness[..32]); + + let timestamp = OffsetDateTime::now_utc().unix_timestamp(); + + NymPayInfo { + randomness, + timestamp, + provider_public_key: provider_pk, + } + } + + pub fn timestamp(&self) -> i64 { + self.timestamp + } + + pub fn pk(&self) -> [u8; 32] { + self.provider_public_key + } +} + +impl From for PayInfo { + fn from(value: NymPayInfo) -> Self { + let mut pay_info_bytes = [0u8; 72]; + + pay_info_bytes[..32].copy_from_slice(&value.randomness); + pay_info_bytes[32..40].copy_from_slice(&value.timestamp.to_be_bytes()); + pay_info_bytes[40..].copy_from_slice(&value.provider_public_key); + + PayInfo { pay_info_bytes } + } +} + +impl From for NymPayInfo { + fn from(value: PayInfo) -> Self { + //SAFETY : slice to array of same length + let randomness = value.pay_info_bytes[..32].try_into().unwrap(); + let timestamp = i64::from_be_bytes(value.pay_info_bytes[32..40].try_into().unwrap()); + let provider_public_key = value.pay_info_bytes[40..].try_into().unwrap(); + + NymPayInfo { + randomness, + timestamp, + provider_public_key, + } + } +} + +#[derive( + Default, + Copy, + Clone, + Debug, + PartialEq, + Serialize, + Deserialize, + strum::Display, + strum::EnumString, +)] +#[serde(rename_all = "kebab-case")] +#[strum(serialize_all = "kebab-case")] +pub enum TicketType { + #[default] + V1MixnetEntry, + V1MixnetExit, + V1WireguardEntry, + V1WireguardExit, +} + +#[derive(Debug, Copy, Clone, Error)] +#[error("provided unknown ticketbook type")] +pub struct UnknownTicketType; + +impl TicketType { + pub fn to_repr(&self) -> TicketTypeRepr { + (*self).into() + } + + pub fn encode(&self) -> EncodedTicketType { + self.to_repr() as EncodedTicketType + } + + pub fn try_from_encoded(val: EncodedTicketType) -> Result { + match val { + n if n == TicketTypeRepr::V1MixnetEntry as u8 => { + Ok(TicketTypeRepr::V1MixnetEntry.into()) + } + n if n == TicketTypeRepr::V1MixnetExit as u8 => Ok(TicketTypeRepr::V1MixnetExit.into()), + n if n == TicketTypeRepr::V1WireguardEntry as u8 => { + Ok(TicketTypeRepr::V1WireguardEntry.into()) + } + n if n == TicketTypeRepr::V1WireguardExit as u8 => { + Ok(TicketTypeRepr::V1WireguardExit.into()) + } + _ => Err(UnknownTicketType), + } + } +} + +impl From for TicketTypeRepr { + fn from(value: TicketType) -> Self { + match value { + TicketType::V1MixnetEntry => TicketTypeRepr::V1MixnetEntry, + TicketType::V1MixnetExit => TicketTypeRepr::V1MixnetExit, + TicketType::V1WireguardEntry => TicketTypeRepr::V1WireguardEntry, + TicketType::V1WireguardExit => TicketTypeRepr::V1WireguardExit, + } + } +} + +impl From for TicketType { + fn from(value: TicketTypeRepr) -> Self { + match value { + TicketTypeRepr::V1MixnetEntry => TicketType::V1MixnetEntry, + TicketTypeRepr::V1MixnetExit => TicketType::V1MixnetExit, + TicketTypeRepr::V1WireguardEntry => TicketType::V1WireguardEntry, + TicketTypeRepr::V1WireguardExit => TicketType::V1WireguardExit, + } + } +} + +#[derive(Clone)] +pub struct ClientTicket { + pub spending_data: CredentialSpendingData, + pub ticket_id: i64, +} + +impl ClientTicket { + pub fn new(spending_data: CredentialSpendingData, ticket_id: i64) -> Self { + ClientTicket { + spending_data, + ticket_id, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct AvailableBandwidth { + pub bytes: i64, + pub expiration: OffsetDateTime, +} + +impl AvailableBandwidth { + pub fn expired(&self) -> bool { + self.expiration < ecash_today() + } +} + +impl Default for AvailableBandwidth { + fn default() -> Self { + Self { + bytes: 0, + expiration: OffsetDateTime::UNIX_EPOCH, + } } } diff --git a/common/credentials/Cargo.toml b/common/credentials/Cargo.toml index a51604a142..48159258fc 100644 --- a/common/credentials/Cargo.toml +++ b/common/credentials/Cargo.toml @@ -16,11 +16,15 @@ time = { workspace = true, features = ["serde"] } serde = { workspace = true, features = ["derive"] } zeroize = { workspace = true } +nym-ecash-time = { path = "../ecash-time", features = ["expiration"] } + # I guess temporarily until we get serde support in coconut up and running nym-credentials-interface = { path = "../credentials-interface" } -nym-crypto = { path = "../crypto", features = ["rand", "asymmetric", "serde"] } +nym-crypto = { path = "../crypto" } nym-api-requests = { path = "../../nym-api/nym-api-requests" } nym-validator-client = { path = "../client-libs/validator-client", default-features = false } +nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" } +nym-network-defaults = { path = "../network-defaults" } [dev-dependencies] rand = "0.8.5" diff --git a/common/credentials/src/coconut/bandwidth/freepass.rs b/common/credentials/src/coconut/bandwidth/freepass.rs deleted file mode 100644 index 9329af2a6e..0000000000 --- a/common/credentials/src/coconut/bandwidth/freepass.rs +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::coconut::utils::scalar_serde_helper; -use crate::error::Error; -use nym_api_requests::coconut::FreePassRequest; -use nym_credentials_interface::{ - hash_to_scalar, Attribute, BlindedSignature, CredentialSigningData, PublicAttribute, -}; -use nym_validator_client::signing::AccountData; -use serde::{Deserialize, Serialize}; -use time::{Duration, OffsetDateTime, Time}; -use zeroize::{Zeroize, ZeroizeOnDrop}; - -pub const DEFAULT_FREE_PASS_VALIDITY: Duration = Duration::WEEK; // 1 week -pub const MAX_FREE_PASS_VALIDITY: Duration = Duration::weeks(12); // 12 weeks - -#[derive(Debug, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] -pub struct FreePassIssuedData { - /// the plain validity value of this credential expressed as unix timestamp - #[zeroize(skip)] - expiry_date: OffsetDateTime, -} - -impl<'a> From<&'a FreePassIssuanceData> for FreePassIssuedData { - fn from(value: &'a FreePassIssuanceData) -> Self { - FreePassIssuedData { - expiry_date: value.expiry_date, - } - } -} - -impl FreePassIssuedData { - pub fn expired(&self) -> bool { - self.expiry_date <= OffsetDateTime::now_utc() - } - - pub fn expiry_date(&self) -> OffsetDateTime { - self.expiry_date - } - - pub fn expiry_date_plain(&self) -> String { - self.expiry_date.unix_timestamp().to_string() - } -} - -#[derive(Zeroize, Serialize, Deserialize)] -pub struct FreePassIssuanceData { - /// the plain validity value of this credential expressed as unix timestamp - #[zeroize(skip)] - expiry_date: OffsetDateTime, - - // the expiry date, as unix timestamp, hashed into a scalar - #[serde(with = "scalar_serde_helper")] - expiry_date_prehashed: PublicAttribute, -} - -impl FreePassIssuanceData { - pub fn new(expiry_date: Option) -> Self { - // ideally we should have implemented a proper error handling here, sure. - // but given it's meant to only be used by nym, imo it's fine to just panic here in case of invalid arguments - let expiry_date = if let Some(provided) = expiry_date { - if provided - OffsetDateTime::now_utc() > MAX_FREE_PASS_VALIDITY { - panic!("the provided expiry date is bigger than the maximum value of {MAX_FREE_PASS_VALIDITY}"); - } - - provided - } else { - Self::default_expiry_date() - }; - - let expiry_date_prehashed = hash_to_scalar(expiry_date.unix_timestamp().to_string()); - - FreePassIssuanceData { - expiry_date, - expiry_date_prehashed, - } - } - - pub fn default_expiry_date() -> OffsetDateTime { - // set it to the furthest midnight in the future such as it's no more than a week away, - // i.e. if it's currently for example 9:43 on 2nd March 2024, it will set it to 0:00 on 9th March 2024 - (OffsetDateTime::now_utc() + DEFAULT_FREE_PASS_VALIDITY).replace_time(Time::MIDNIGHT) - } - - pub fn expiry_date_attribute(&self) -> &Attribute { - &self.expiry_date_prehashed - } - - pub fn expiry_date_plain(&self) -> String { - self.expiry_date.unix_timestamp().to_string() - } - - pub async fn obtain_free_pass_nonce( - &self, - client: &nym_validator_client::client::NymApiClient, - ) -> Result<[u8; 16], Error> { - let server_response = client.free_pass_nonce().await?; - Ok(server_response.current_nonce) - } - - pub fn create_free_pass_request( - &self, - signing_request: &CredentialSigningData, - account_data: &AccountData, - issuer_nonce: [u8; 16], - ) -> Result { - let nonce_signature = account_data - .private_key() - .sign(&issuer_nonce) - .map_err(|_| Error::Secp256k1SignFailure)?; - - Ok(FreePassRequest { - cosmos_pubkey: account_data.public_key(), - inner_sign_request: signing_request.blind_sign_request.clone(), - used_nonce: issuer_nonce, - nonce_signature, - public_attributes_plain: signing_request.public_attributes_plain.clone(), - }) - } - - pub async fn obtain_blinded_credential( - &self, - client: &nym_validator_client::client::NymApiClient, - request: &FreePassRequest, - ) -> Result { - let server_response = client.issue_free_pass_credential(request).await?; - Ok(server_response.blinded_signature) - } - - pub async fn request_blinded_credential( - &self, - signing_request: &CredentialSigningData, - account_data: &AccountData, - client: &nym_validator_client::client::NymApiClient, - ) -> Result { - let signing_nonce = self.obtain_free_pass_nonce(client).await?; - let request = - self.create_free_pass_request(signing_request, account_data, signing_nonce)?; - self.obtain_blinded_credential(client, &request).await - } -} diff --git a/common/credentials/src/coconut/bandwidth/issuance.rs b/common/credentials/src/coconut/bandwidth/issuance.rs deleted file mode 100644 index a845b6ac7b..0000000000 --- a/common/credentials/src/coconut/bandwidth/issuance.rs +++ /dev/null @@ -1,355 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::coconut::bandwidth::freepass::FreePassIssuanceData; -use crate::coconut::bandwidth::issued::IssuedBandwidthCredential; -use crate::coconut::bandwidth::voucher::BandwidthVoucherIssuanceData; -use crate::coconut::bandwidth::{ - bandwidth_credential_params, CredentialSigningData, CredentialType, -}; -use crate::coconut::utils::scalar_serde_helper; -use crate::error::Error; -use nym_credentials_interface::{ - aggregate_signature_shares, aggregate_signature_shares_and_verify, hash_to_scalar, - prepare_blind_sign, Attribute, BlindedSerialNumber, BlindedSignature, Parameters, - PrivateAttribute, PublicAttribute, Signature, SignatureShare, VerificationKey, -}; -use nym_crypto::asymmetric::{encryption, identity}; -use nym_validator_client::nym_api::EpochId; -use nym_validator_client::signing::AccountData; -use serde::{Deserialize, Serialize}; -use time::OffsetDateTime; -use zeroize::{Zeroize, ZeroizeOnDrop}; - -pub use nym_validator_client::nyxd::{Coin, Hash}; - -#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] -pub enum BandwidthCredentialIssuanceDataVariant { - Voucher(BandwidthVoucherIssuanceData), - FreePass(FreePassIssuanceData), -} - -impl From for BandwidthCredentialIssuanceDataVariant { - fn from(value: FreePassIssuanceData) -> Self { - BandwidthCredentialIssuanceDataVariant::FreePass(value) - } -} - -impl From for BandwidthCredentialIssuanceDataVariant { - fn from(value: BandwidthVoucherIssuanceData) -> Self { - BandwidthCredentialIssuanceDataVariant::Voucher(value) - } -} - -impl BandwidthCredentialIssuanceDataVariant { - pub fn info(&self) -> CredentialType { - match self { - BandwidthCredentialIssuanceDataVariant::Voucher(..) => CredentialType::Voucher, - BandwidthCredentialIssuanceDataVariant::FreePass(..) => CredentialType::FreePass, - } - } - - // currently this works under the assumption of there being a single unique public attribute for given variant - pub fn public_value(&self) -> &Attribute { - match self { - BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => voucher.value_attribute(), - BandwidthCredentialIssuanceDataVariant::FreePass(freepass) => { - freepass.expiry_date_attribute() - } - } - } - - // currently this works under the assumption of there being a single unique public attribute for given variant - pub fn public_value_plain(&self) -> String { - match self { - BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => voucher.value_plain(), - BandwidthCredentialIssuanceDataVariant::FreePass(freepass) => { - freepass.expiry_date_plain() - } - } - } - - pub fn voucher_data(&self) -> Option<&BandwidthVoucherIssuanceData> { - match self { - BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => Some(voucher), - _ => None, - } - } -} - -// all types of bandwidth credentials contain serial number and binding number -#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] -pub struct IssuanceBandwidthCredential { - // private attributes - /// a random secret value generated by the client used for double-spending detection - #[serde(with = "scalar_serde_helper")] - serial_number: PrivateAttribute, - - /// a random secret value generated by the client used to bind multiple credentials together - #[serde(with = "scalar_serde_helper")] - binding_number: PrivateAttribute, - - /// data specific to given bandwidth credential, for example a value for bandwidth voucher and expiry date for the free pass - variant_data: BandwidthCredentialIssuanceDataVariant, - - /// type of the bandwdith credential hashed onto a scalar - #[serde(with = "scalar_serde_helper")] - type_prehashed: PublicAttribute, -} - -impl IssuanceBandwidthCredential { - pub const PUBLIC_ATTRIBUTES: u32 = 2; - pub const PRIVATE_ATTRIBUTES: u32 = 2; - pub const ENCODED_ATTRIBUTES: u32 = Self::PUBLIC_ATTRIBUTES + Self::PRIVATE_ATTRIBUTES; - - pub fn default_parameters() -> Parameters { - // safety: the unwrap is fine here as Self::ENCODED_ATTRIBUTES is non-zero - Parameters::new(Self::ENCODED_ATTRIBUTES).unwrap() - } - - pub fn new>(variant_data: B) -> Self { - let variant_data = variant_data.into(); - let type_prehashed = hash_to_scalar(variant_data.info().to_string()); - - let params = bandwidth_credential_params(); - let serial_number = params.random_scalar(); - let binding_number = params.random_scalar(); - - IssuanceBandwidthCredential { - serial_number, - binding_number, - variant_data, - type_prehashed, - } - } - - pub fn new_voucher( - value: impl Into, - deposit_tx_hash: Hash, - signing_key: identity::PrivateKey, - unused_ed25519: encryption::PrivateKey, - ) -> Self { - Self::new(BandwidthVoucherIssuanceData::new( - value, - deposit_tx_hash, - signing_key, - unused_ed25519, - )) - } - - pub fn new_freepass(expiry_date: Option) -> Self { - Self::new(FreePassIssuanceData::new(expiry_date)) - } - - pub fn blind_serial_number(&self) -> BlindedSerialNumber { - (bandwidth_credential_params().gen2() * self.serial_number).into() - } - - pub fn blinded_serial_number_bs58(&self) -> String { - use nym_credentials_interface::Base58; - - self.blind_serial_number().to_bs58() - } - - pub fn typ(&self) -> CredentialType { - self.variant_data.info() - } - - pub fn get_private_attributes(&self) -> Vec<&PrivateAttribute> { - vec![&self.serial_number, &self.binding_number] - } - - pub fn get_public_attributes(&self) -> Vec<&PublicAttribute> { - vec![self.variant_data.public_value(), &self.type_prehashed] - } - - pub fn get_plain_public_attributes(&self) -> Vec { - vec![ - self.variant_data.public_value_plain(), - self.typ().to_string(), - ] - } - - pub fn get_variant_data(&self) -> &BandwidthCredentialIssuanceDataVariant { - &self.variant_data - } - - pub fn get_bandwidth_attribute(&self) -> String { - self.variant_data.public_value_plain() - } - - pub fn prepare_for_signing(&self) -> CredentialSigningData { - let params = bandwidth_credential_params(); - - // safety: the creation of the request can only fail if one provided invalid parameters - // and we created then specific to this type of the credential so the unwrap is fine - let (pedersen_commitments_openings, blind_sign_request) = prepare_blind_sign( - params, - &[&self.serial_number, &self.binding_number], - &self.get_public_attributes(), - ) - .unwrap(); - - CredentialSigningData { - pedersen_commitments_openings, - blind_sign_request, - public_attributes_plain: self.get_plain_public_attributes(), - typ: self.typ(), - } - } - - pub fn unblind_signature( - &self, - validator_vk: &VerificationKey, - signing_data: &CredentialSigningData, - blinded_signature: BlindedSignature, - ) -> Result { - let public_attributes = self.get_public_attributes(); - let private_attributes = self.get_private_attributes(); - - let params = bandwidth_credential_params(); - let unblinded_signature = blinded_signature.unblind_and_verify( - params, - validator_vk, - &private_attributes, - &public_attributes, - &signing_data.blind_sign_request.get_commitment_hash(), - &signing_data.pedersen_commitments_openings, - )?; - - Ok(unblinded_signature) - } - - pub async fn obtain_partial_freepass_credential( - &self, - client: &nym_validator_client::client::NymApiClient, - account_data: &AccountData, - validator_vk: &VerificationKey, - signing_data: impl Into>, - ) -> Result { - // if we provided signing data, do use them, otherwise generate fresh data - let signing_data = signing_data - .into() - .unwrap_or_else(|| self.prepare_for_signing()); - - let blinded_signature = match &self.variant_data { - BandwidthCredentialIssuanceDataVariant::FreePass(freepass) => { - freepass - .request_blinded_credential(&signing_data, account_data, client) - .await? - } - _ => return Err(Error::NotAFreePass), - }; - self.unblind_signature(validator_vk, &signing_data, blinded_signature) - } - - // ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers - pub async fn obtain_partial_bandwidth_voucher_credential( - &self, - client: &nym_validator_client::client::NymApiClient, - validator_vk: &VerificationKey, - signing_data: impl Into>, - ) -> Result { - // if we provided signing data, do use them, otherwise generate fresh data - let signing_data = signing_data - .into() - .unwrap_or_else(|| self.prepare_for_signing()); - - let blinded_signature = match &self.variant_data { - BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => { - // TODO: the request can be re-used between different apis - let request = voucher.create_blind_sign_request_body(&signing_data); - voucher.obtain_blinded_credential(client, &request).await? - } - _ => return Err(Error::NotABandwdithVoucher), - }; - self.unblind_signature(validator_vk, &signing_data, blinded_signature) - } - - pub fn unchecked_aggregate_signature_shares( - &self, - shares: &[SignatureShare], - ) -> Result { - aggregate_signature_shares(shares).map_err(Error::SignatureAggregationError) - } - - pub fn aggregate_signature_shares( - &self, - verification_key: &VerificationKey, - shares: &[SignatureShare], - ) -> Result { - let public_attributes = self.get_public_attributes(); - let private_attributes = self.get_private_attributes(); - - let params = bandwidth_credential_params(); - - let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len()); - attributes.extend_from_slice(&private_attributes); - attributes.extend_from_slice(&public_attributes); - - aggregate_signature_shares_and_verify(params, verification_key, &attributes, shares) - .map_err(Error::SignatureAggregationError) - } - - // also drops self after the conversion - pub fn into_issued_credential( - self, - aggregate_signature: Signature, - epoch_id: EpochId, - ) -> IssuedBandwidthCredential { - self.to_issued_credential(aggregate_signature, epoch_id) - } - - pub fn to_issued_credential( - &self, - aggregate_signature: Signature, - epoch_id: EpochId, - ) -> IssuedBandwidthCredential { - IssuedBandwidthCredential::new( - self.serial_number, - self.binding_number, - aggregate_signature, - (&self.variant_data).into(), - self.type_prehashed, - epoch_id, - ) - } - - // TODO: is that actually needed? - pub fn to_recovery_bytes(&self) -> Vec { - use bincode::Options; - // safety: our data format is stable and thus the serialization should not fail - make_recovery_bincode_serializer().serialize(self).unwrap() - } - - // TODO: is that actually needed? - // idea: make it consistent with the issued credential and its vX serde - pub fn try_from_recovered_bytes(bytes: &[u8]) -> Result { - use bincode::Options; - make_recovery_bincode_serializer() - .deserialize(bytes) - .map_err(|source| Error::RecoveryCredentialDeserializationFailure { source }) - } -} - -fn make_recovery_bincode_serializer() -> impl bincode::Options { - use bincode::Options; - bincode::DefaultOptions::new() - .with_big_endian() - .with_varint_encoding() -} - -#[cfg(test)] -mod tests { - use super::*; - - fn assert_zeroize_on_drop() {} - - fn assert_zeroize() {} - - #[test] - fn credential_is_zeroized() { - assert_zeroize::(); - assert_zeroize_on_drop::(); - } -} diff --git a/common/credentials/src/coconut/bandwidth/issued.rs b/common/credentials/src/coconut/bandwidth/issued.rs deleted file mode 100644 index 00e0ab341b..0000000000 --- a/common/credentials/src/coconut/bandwidth/issued.rs +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::coconut::bandwidth::bandwidth_credential_params; -use crate::coconut::bandwidth::freepass::FreePassIssuedData; -use crate::coconut::bandwidth::issuance::{ - BandwidthCredentialIssuanceDataVariant, IssuanceBandwidthCredential, -}; -use crate::coconut::bandwidth::voucher::BandwidthVoucherIssuedData; -use crate::coconut::bandwidth::{CredentialSpendingData, CredentialType}; -use crate::coconut::utils::scalar_serde_helper; -use crate::error::Error; -use nym_credentials_interface::prove_bandwidth_credential; -use nym_credentials_interface::{ - Parameters, PrivateAttribute, PublicAttribute, Signature, VerificationKey, -}; -use nym_validator_client::nym_api::EpochId; -use serde::{Deserialize, Serialize}; -use zeroize::{Zeroize, ZeroizeOnDrop}; - -pub const CURRENT_SERIALIZATION_REVISION: u8 = 1; - -#[derive(Debug, Zeroize, Serialize, Deserialize)] -pub enum BandwidthCredentialIssuedDataVariant { - Voucher(BandwidthVoucherIssuedData), - FreePass(FreePassIssuedData), -} - -impl<'a> From<&'a BandwidthCredentialIssuanceDataVariant> for BandwidthCredentialIssuedDataVariant { - fn from(value: &'a BandwidthCredentialIssuanceDataVariant) -> Self { - match value { - BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => { - BandwidthCredentialIssuedDataVariant::Voucher(voucher.into()) - } - BandwidthCredentialIssuanceDataVariant::FreePass(freepass) => { - BandwidthCredentialIssuedDataVariant::FreePass(freepass.into()) - } - } - } -} - -impl From for BandwidthCredentialIssuedDataVariant { - fn from(value: FreePassIssuedData) -> Self { - BandwidthCredentialIssuedDataVariant::FreePass(value) - } -} - -impl From for BandwidthCredentialIssuedDataVariant { - fn from(value: BandwidthVoucherIssuedData) -> Self { - BandwidthCredentialIssuedDataVariant::Voucher(value) - } -} - -impl BandwidthCredentialIssuedDataVariant { - pub fn info(&self) -> CredentialType { - match self { - BandwidthCredentialIssuedDataVariant::Voucher(..) => CredentialType::Voucher, - BandwidthCredentialIssuedDataVariant::FreePass(..) => CredentialType::FreePass, - } - } - - // currently this works under the assumption of there being a single unique public attribute for given variant - pub fn public_value_plain(&self) -> String { - match self { - BandwidthCredentialIssuedDataVariant::Voucher(voucher) => voucher.value_plain(), - BandwidthCredentialIssuedDataVariant::FreePass(freepass) => { - freepass.expiry_date_plain() - } - } - } -} - -// the only important thing to zeroize here are the private attributes, the rest can be made fully public for what we're concerned -#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] -pub struct IssuedBandwidthCredential { - // private attributes - /// a random secret value generated by the client used for double-spending detection - #[serde(with = "scalar_serde_helper")] - serial_number: PrivateAttribute, - - /// a random secret value generated by the client used to bind multiple credentials together - #[serde(with = "scalar_serde_helper")] - binding_number: PrivateAttribute, - - /// the underlying aggregated signature on the attributes - #[zeroize(skip)] - signature: Signature, - - /// data specific to given bandwidth credential, for example a value for bandwidth voucher and expiry date for the free pass - variant_data: BandwidthCredentialIssuedDataVariant, - - /// type of the bandwdith credential hashed onto a scalar - #[serde(with = "scalar_serde_helper")] - type_prehashed: PublicAttribute, - - /// Specifies the (DKG) epoch id when this credential has been issued - epoch_id: EpochId, -} - -impl IssuedBandwidthCredential { - pub fn new( - serial_number: PrivateAttribute, - binding_number: PrivateAttribute, - signature: Signature, - variant_data: BandwidthCredentialIssuedDataVariant, - type_prehashed: PublicAttribute, - epoch_id: EpochId, - ) -> Self { - IssuedBandwidthCredential { - serial_number, - binding_number, - signature, - variant_data, - type_prehashed, - epoch_id, - } - } - - pub fn try_unpack(bytes: &[u8], revision: impl Into>) -> Result { - let revision = revision.into().unwrap_or(CURRENT_SERIALIZATION_REVISION); - - match revision { - 1 => Self::unpack_v1(bytes), - _ => Err(Error::UnknownSerializationRevision { revision }), - } - } - - pub fn epoch_id(&self) -> EpochId { - self.epoch_id - } - - pub fn variant_data(&self) -> &BandwidthCredentialIssuedDataVariant { - &self.variant_data - } - - pub fn current_serialization_revision(&self) -> u8 { - CURRENT_SERIALIZATION_REVISION - } - - /// Pack (serialize) this credential data into a stream of bytes using v1 serializer. - pub fn pack_v1(&self) -> Vec { - use bincode::Options; - // safety: our data format is stable and thus the serialization should not fail - make_storable_bincode_serializer().serialize(self).unwrap() - } - - /// Unpack (deserialize) the credential data from the given bytes using v1 serializer. - pub fn unpack_v1(bytes: &[u8]) -> Result { - use bincode::Options; - make_storable_bincode_serializer() - .deserialize(bytes) - .map_err(|source| Error::SerializationFailure { - source, - revision: 1, - }) - } - - pub fn default_parameters() -> Parameters { - IssuanceBandwidthCredential::default_parameters() - } - - pub fn typ(&self) -> CredentialType { - self.variant_data.info() - } - - pub fn get_plain_public_attributes(&self) -> Vec { - vec![ - self.variant_data.public_value_plain(), - self.typ().to_string(), - ] - } - - pub fn prepare_for_spending( - &self, - verification_key: &VerificationKey, - ) -> Result { - let params = bandwidth_credential_params(); - - let verify_credential_request = prove_bandwidth_credential( - params, - verification_key, - &self.signature, - &self.serial_number, - &self.binding_number, - )?; - - Ok(CredentialSpendingData { - embedded_private_attributes: IssuanceBandwidthCredential::PRIVATE_ATTRIBUTES as usize, - verify_credential_request, - public_attributes_plain: self.get_plain_public_attributes(), - typ: self.typ(), - epoch_id: self.epoch_id, - }) - } -} - -fn make_storable_bincode_serializer() -> impl bincode::Options { - use bincode::Options; - bincode::DefaultOptions::new() - .with_big_endian() - .with_varint_encoding() -} - -#[cfg(test)] -mod tests { - use super::*; - - fn assert_zeroize_on_drop() {} - - fn assert_zeroize() {} - - #[test] - fn credential_is_zeroized() { - assert_zeroize::(); - assert_zeroize_on_drop::(); - } -} diff --git a/common/credentials/src/coconut/bandwidth/mod.rs b/common/credentials/src/coconut/bandwidth/mod.rs deleted file mode 100644 index de49c9d4b4..0000000000 --- a/common/credentials/src/coconut/bandwidth/mod.rs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2021-2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use std::sync::OnceLock; - -pub use issuance::IssuanceBandwidthCredential; -pub use issued::IssuedBandwidthCredential; -pub use nym_credentials_interface::{ - CredentialSigningData, CredentialSpendingData, CredentialType, Parameters, - UnknownCredentialType, -}; - -pub mod freepass; -pub mod issuance; -pub mod issued; -pub mod voucher; - -// works under the assumption of having 4 attributes in the underlying credential(s) -pub fn bandwidth_credential_params() -> &'static Parameters { - static BANDWIDTH_CREDENTIAL_PARAMS: OnceLock = OnceLock::new(); - BANDWIDTH_CREDENTIAL_PARAMS.get_or_init(IssuanceBandwidthCredential::default_parameters) -} diff --git a/common/credentials/src/coconut/bandwidth/voucher.rs b/common/credentials/src/coconut/bandwidth/voucher.rs deleted file mode 100644 index 58c93edf7b..0000000000 --- a/common/credentials/src/coconut/bandwidth/voucher.rs +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::coconut::bandwidth::CredentialSigningData; -use crate::coconut::utils::scalar_serde_helper; -use crate::error::Error; -use nym_api_requests::coconut::BlindSignRequestBody; -use nym_credentials_interface::{ - hash_to_scalar, Attribute, BlindSignRequest, BlindedSignature, CredentialType, PublicAttribute, -}; -use nym_crypto::asymmetric::{encryption, identity}; -use nym_validator_client::nyxd::{Coin, Hash}; -use serde::{Deserialize, Serialize}; -use zeroize::{Zeroize, ZeroizeOnDrop}; - -#[derive(Debug, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] -pub struct BandwidthVoucherIssuedData { - /// the plain value (e.g., bandwidth) encoded in this voucher - // note: for legacy reasons we're only using the value of the coin and ignoring the denom - #[zeroize(skip)] - value: Coin, -} - -impl<'a> From<&'a BandwidthVoucherIssuanceData> for BandwidthVoucherIssuedData { - fn from(value: &'a BandwidthVoucherIssuanceData) -> Self { - BandwidthVoucherIssuedData { - value: value.value.clone(), - } - } -} - -impl BandwidthVoucherIssuedData { - pub fn new(value: Coin) -> Self { - BandwidthVoucherIssuedData { value } - } - - pub fn value(&self) -> &Coin { - &self.value - } - - pub fn value_plain(&self) -> String { - self.value.amount.to_string() - } -} - -#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] -pub struct BandwidthVoucherIssuanceData { - /// the plain value (e.g., bandwidth) encoded in this voucher - // note: for legacy reasons we're only using the value of the coin and ignoring the denom - #[zeroize(skip)] - value: Coin, - - // note: as mentioned above, we're only hashing the value of the coin! - #[serde(with = "scalar_serde_helper")] - value_prehashed: PublicAttribute, - - /// the hash of the deposit transaction - #[zeroize(skip)] - deposit_tx_hash: Hash, - - /// base58 encoded private key ensuring the depositer requested these attributes - signing_key: identity::PrivateKey, - - /// base58 encoded private key ensuring only this client receives the signature share - unused_ed25519: encryption::PrivateKey, -} - -impl BandwidthVoucherIssuanceData { - pub fn new( - value: impl Into, - deposit_tx_hash: Hash, - signing_key: identity::PrivateKey, - unused_ed25519: encryption::PrivateKey, - ) -> Self { - let value = value.into(); - let value_prehashed = hash_to_scalar(value.amount.to_string()); - - BandwidthVoucherIssuanceData { - value, - value_prehashed, - deposit_tx_hash, - signing_key, - unused_ed25519, - } - } - - pub fn request_plaintext(request: &BlindSignRequest, tx_hash: Hash) -> Vec { - let mut message = request.to_bytes(); - message.extend_from_slice(tx_hash.as_bytes()); - message - } - - fn request_signature(&self, signing_request: &CredentialSigningData) -> identity::Signature { - let message = - Self::request_plaintext(&signing_request.blind_sign_request, self.deposit_tx_hash); - self.signing_key.sign(message) - } - - pub fn create_blind_sign_request_body( - &self, - signing_request: &CredentialSigningData, - ) -> BlindSignRequestBody { - let request_signature = self.request_signature(signing_request); - - BlindSignRequestBody::new( - signing_request.blind_sign_request.clone(), - self.deposit_tx_hash, - request_signature, - signing_request.public_attributes_plain.clone(), - ) - } - - pub async fn obtain_blinded_credential( - &self, - client: &nym_validator_client::client::NymApiClient, - request_body: &BlindSignRequestBody, - ) -> Result { - let server_response = client.blind_sign(request_body).await?; - Ok(server_response.blinded_signature) - } - - pub fn value_plain(&self) -> String { - self.value.amount.to_string() - } - - pub fn value_attribute(&self) -> &Attribute { - &self.value_prehashed - } - - pub fn typ() -> CredentialType { - CredentialType::Voucher - } - - pub fn tx_hash(&self) -> Hash { - self.deposit_tx_hash - } - - pub fn identity_key(&self) -> &identity::PrivateKey { - &self.signing_key - } - - pub fn encryption_key(&self) -> &encryption::PrivateKey { - &self.unused_ed25519 - } -} diff --git a/common/credentials/src/coconut/utils.rs b/common/credentials/src/coconut/utils.rs deleted file mode 100644 index 3192804530..0000000000 --- a/common/credentials/src/coconut/utils.rs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2021-2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::coconut::bandwidth::IssuanceBandwidthCredential; -use crate::error::Error; -use log::{debug, warn}; -use nym_credentials_interface::{ - aggregate_verification_keys, Signature, SignatureShare, VerificationKey, -}; -use nym_validator_client::client::CoconutApiClient; - -pub fn obtain_aggregate_verification_key( - api_clients: &[CoconutApiClient], -) -> Result { - if api_clients.is_empty() { - return Err(Error::NoValidatorsAvailable); - } - - 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))?) -} - -pub async fn obtain_aggregate_signature( - voucher: &IssuanceBandwidthCredential, - coconut_api_clients: &[CoconutApiClient], - threshold: u64, -) -> Result { - if coconut_api_clients.is_empty() { - return Err(Error::NoValidatorsAvailable); - } - let mut shares = Vec::with_capacity(coconut_api_clients.len()); - let verification_key = obtain_aggregate_verification_key(coconut_api_clients)?; - - let request = voucher.prepare_for_signing(); - - for coconut_api_client in coconut_api_clients.iter() { - debug!( - "attempting to obtain partial credential from {}", - coconut_api_client.api_client.api_url() - ); - - match voucher - .obtain_partial_bandwidth_voucher_credential( - &coconut_api_client.api_client, - &coconut_api_client.verification_key, - Some(request.clone()), - ) - .await - { - Ok(signature) => { - let share = SignatureShare::new(signature, coconut_api_client.node_id); - shares.push(share) - } - Err(err) => { - warn!( - "failed to obtain partial credential from {}: {err}", - coconut_api_client.api_client.api_url() - ); - } - }; - } - if shares.len() < threshold as usize { - return Err(Error::NotEnoughShares); - } - - voucher.aggregate_signature_shares(&verification_key, &shares) -} - -pub(crate) mod scalar_serde_helper { - use bls12_381::Scalar; - use serde::{Deserialize, Deserializer, Serialize, Serializer}; - use zeroize::Zeroizing; - - pub fn serialize(scalar: &Scalar, serializer: S) -> Result { - scalar.to_bytes().serialize(serializer) - } - - pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result { - let b = <[u8; 32]>::deserialize(deserializer)?; - - // make sure the bytes get zeroed - let bytes = Zeroizing::new(b); - - let maybe_scalar: Option = Scalar::from_bytes(&bytes).into(); - maybe_scalar.ok_or(serde::de::Error::custom( - "did not construct a valid bls12-381 scalar out of the provided bytes", - )) - } -} diff --git a/common/credentials/src/ecash/bandwidth/issuance.rs b/common/credentials/src/ecash/bandwidth/issuance.rs new file mode 100644 index 0000000000..72b1dd0a25 --- /dev/null +++ b/common/credentials/src/ecash/bandwidth/issuance.rs @@ -0,0 +1,254 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash::bandwidth::issued::IssuedTicketBook; +use crate::ecash::bandwidth::serialiser::VersionedSerialise; +use crate::ecash::bandwidth::CredentialSigningData; +use crate::ecash::utils::cred_exp_date; +use crate::error::Error; +use nym_api_requests::ecash::BlindSignRequestBody; +use nym_credentials_interface::{ + aggregate_wallets, generate_keypair_user_from_seed, issue_verify, withdrawal_request, + BlindedSignature, KeyPairUser, PartialWallet, TicketType, VerificationKeyAuth, + WalletSignatures, WithdrawalRequest, +}; +use nym_crypto::asymmetric::identity; +use nym_ecash_contract_common::deposit::DepositId; +use nym_ecash_time::{ecash_default_expiration_date, ecash_today, EcashTime}; +use nym_validator_client::nym_api::EpochId; +use serde::{Deserialize, Serialize}; +use time::Date; + +pub use nym_validator_client::nyxd::{Coin, Hash}; + +#[derive(Serialize, Deserialize)] +pub struct IssuanceTicketBook { + /// the id of the associated deposit + deposit_id: DepositId, + + /// base58 encoded private key ensuring the depositer requested these attributes + signing_key: identity::PrivateKey, + + /// ecash keypair related to the credential + ecash_keypair: KeyPairUser, + + /// the type of the ticketbook to be issued + ticketbook_type: TicketType, + + /// expiration_date of that credential + expiration_date: Date, +} + +impl IssuanceTicketBook { + pub fn new>( + deposit_id: DepositId, + identifier: M, + signing_key: identity::PrivateKey, + ticketbook_type: TicketType, + ) -> Self { + //this expiration date will get fed to the ecash library, force midnight to be set + Self::new_with_expiration( + deposit_id, + identifier, + signing_key, + ticketbook_type, + ecash_default_expiration_date(), + ) + } + + pub fn new_with_expiration>( + deposit_id: DepositId, + identifier: M, + signing_key: identity::PrivateKey, + ticketbook_type: TicketType, + expiration_date: Date, + ) -> Self { + let ecash_keypair = generate_keypair_user_from_seed(identifier); + IssuanceTicketBook { + deposit_id, + signing_key, + ecash_keypair, + ticketbook_type, + expiration_date, + } + } + + pub fn ecash_pubkey_bs58(&self) -> String { + use nym_credentials_interface::Base58; + + self.ecash_keypair.public_key().to_bs58() + } + + pub fn expiration_date(&self) -> Date { + self.expiration_date + } + + pub fn ticketbook_type(&self) -> TicketType { + self.ticketbook_type + } + + pub fn request_plaintext(request: &WithdrawalRequest, deposit_id: DepositId) -> Vec { + let mut message = request.to_bytes(); + message.extend_from_slice(&deposit_id.to_be_bytes()); + message + } + + fn request_signature(&self, signing_request: &CredentialSigningData) -> identity::Signature { + let message = Self::request_plaintext(&signing_request.withdrawal_request, self.deposit_id); + self.signing_key.sign(message) + } + + pub fn create_blind_sign_request_body( + &self, + signing_request: &CredentialSigningData, + ) -> BlindSignRequestBody { + let request_signature = self.request_signature(signing_request); + + BlindSignRequestBody::new( + signing_request.withdrawal_request.clone(), + self.deposit_id, + request_signature, + signing_request.ecash_pub_key.clone(), + signing_request.expiration_date, + signing_request.ticketbook_type, + ) + } + + pub async fn obtain_blinded_credential( + &self, + client: &nym_validator_client::client::NymApiClient, + request_body: &BlindSignRequestBody, + ) -> Result { + let server_response = client.blind_sign(request_body).await?; + Ok(server_response.blinded_signature) + } + + pub fn deposit_id(&self) -> DepositId { + self.deposit_id + } + + pub fn identity_key(&self) -> &identity::PrivateKey { + &self.signing_key + } + + pub fn check_expiration_date(&self) -> bool { + self.expiration_date != cred_exp_date().ecash_date() + } + + pub fn expired(&self) -> bool { + self.expiration_date < ecash_today().date() + } + + pub fn prepare_for_signing(&self) -> CredentialSigningData { + // safety: the creation of the request can only fail if one provided invalid parameters + // and we created then specific to this type of the credential so the unwrap is fine + let (withdrawal_request, request_info) = withdrawal_request( + self.ecash_keypair.secret_key(), + self.expiration_date.ecash_unix_timestamp(), + self.ticketbook_type.encode(), + ) + .unwrap(); + + CredentialSigningData { + withdrawal_request, + request_info, + ecash_pub_key: self.ecash_keypair.public_key(), + expiration_date: self.expiration_date, + ticketbook_type: self.ticketbook_type, + } + } + + pub fn unblind_signature( + &self, + validator_vk: &VerificationKeyAuth, + signing_data: &CredentialSigningData, + blinded_signature: BlindedSignature, + signer_index: u64, + ) -> Result { + let unblinded_signature = issue_verify( + validator_vk, + self.ecash_keypair.secret_key(), + &blinded_signature, + &signing_data.request_info, + signer_index, + )?; + + Ok(unblinded_signature) + } + + // ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers + pub async fn obtain_partial_bandwidth_voucher_credential( + &self, + client: &nym_validator_client::client::NymApiClient, + signer_index: u64, + validator_vk: &VerificationKeyAuth, + signing_data: CredentialSigningData, + ) -> Result { + // We need signing data, because they will be used at the aggregation step + + let request = self.create_blind_sign_request_body(&signing_data); + let blinded_signature = self.obtain_blinded_credential(client, &request).await?; + self.unblind_signature(validator_vk, &signing_data, blinded_signature, signer_index) + } + + // pub fn unchecked_aggregate_signature_shares( + // &self, + // shares: &[SignatureShare], + // ) -> Result { + // aggregate_signature_shares(shares).map_err(Error::SignatureAggregationError) + // } + + pub fn aggregate_signature_shares( + &self, + verification_key: &VerificationKeyAuth, + shares: &[PartialWallet], + signing_data: CredentialSigningData, + ) -> Result { + aggregate_wallets( + verification_key, + self.ecash_keypair.secret_key(), + shares, + &signing_data.request_info, + ) + .map_err(Error::SignatureAggregationError) + .map(|w| w.into_wallet_signatures()) + } + + // also drops self after the conversion + pub fn into_issued_ticketbook( + self, + wallet: WalletSignatures, + epoch_id: EpochId, + ) -> IssuedTicketBook { + self.to_issued_ticketbook(wallet, epoch_id) + } + + pub fn to_issued_ticketbook( + &self, + wallet: WalletSignatures, + epoch_id: EpochId, + ) -> IssuedTicketBook { + IssuedTicketBook::new( + wallet, + epoch_id, + self.ecash_keypair.secret_key().clone(), + self.ticketbook_type, + self.expiration_date, + ) + } +} + +impl VersionedSerialise for IssuanceTicketBook { + const CURRENT_SERIALISATION_REVISION: u8 = 1; + + fn try_unpack(b: &[u8], revision: impl Into>) -> Result { + let revision = revision + .into() + .unwrap_or(::CURRENT_SERIALISATION_REVISION); + + match revision { + 1 => Self::try_unpack_current(b), + _ => Err(Error::UnknownSerializationRevision { revision }), + } + } +} diff --git a/common/credentials/src/ecash/bandwidth/issued.rs b/common/credentials/src/ecash/bandwidth/issued.rs new file mode 100644 index 0000000000..da04d38620 --- /dev/null +++ b/common/credentials/src/ecash/bandwidth/issued.rs @@ -0,0 +1,190 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash::bandwidth::serialiser::VersionedSerialise; +use crate::ecash::bandwidth::CredentialSpendingData; +use crate::ecash::utils::ecash_today; +use crate::error::Error; +use nym_credentials_interface::{ + CoinIndexSignature, ExpirationDateSignature, NymPayInfo, PayInfo, SecretKeyUser, TicketType, + VerificationKeyAuth, Wallet, WalletSignatures, +}; +use nym_ecash_time::EcashTime; +use nym_validator_client::nym_api::EpochId; +use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; +use time::Date; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +pub const CURRENT_SERIALIZATION_REVISION: u8 = 1; + +// the only important thing to zeroize here are the private attributes, the rest can be made fully public for what we're concerned +#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] +pub struct IssuedTicketBook { + /// the underlying wallet signatures + signatures_wallet: WalletSignatures, + + /// the counter indicating how many tickets have been spent so far + spent_tickets: u64, + + /// Specifies the (DKG) epoch id when this credential has been issued + epoch_id: EpochId, + + /// secret ecash key used to generate this wallet + ecash_secret_key: SecretKeyUser, + + /// expiration_date for easier discarding + #[zeroize(skip)] + expiration_date: Date, + + /// the type of the ticketbook to got issued + #[zeroize(skip)] + ticketbook_type: TicketType, +} + +impl IssuedTicketBook { + pub fn new( + wallet: WalletSignatures, + epoch_id: EpochId, + ecash_secret_key: SecretKeyUser, + ticketbook_type: TicketType, + expiration_date: Date, + ) -> Self { + IssuedTicketBook { + signatures_wallet: wallet, + spent_tickets: 0, + epoch_id, + ecash_secret_key, + expiration_date, + ticketbook_type, + } + } + + pub fn from_parts( + signatures_wallet: WalletSignatures, + epoch_id: EpochId, + ecash_secret_key: SecretKeyUser, + ticketbook_type: TicketType, + expiration_date: Date, + spent_tickets: u64, + ) -> Self { + IssuedTicketBook { + signatures_wallet, + spent_tickets, + epoch_id, + ecash_secret_key, + expiration_date, + ticketbook_type, + } + } + + pub fn update_spent_tickets(&mut self, spent_tickets: u64) { + self.spent_tickets = spent_tickets + } + + pub fn epoch_id(&self) -> EpochId { + self.epoch_id + } + + pub fn ticketbook_type(&self) -> TicketType { + self.ticketbook_type + } + + pub fn current_serialization_revision(&self) -> u8 { + CURRENT_SERIALIZATION_REVISION + } + + pub fn expiration_date(&self) -> Date { + self.expiration_date + } + + pub fn expired(&self) -> bool { + self.expiration_date < ecash_today().date() + } + + pub fn params_total_tickets(&self) -> u64 { + nym_credentials_interface::ecash_parameters().get_total_coins() + } + + pub fn spent_tickets(&self) -> u64 { + self.spent_tickets + } + + pub fn wallet(&self) -> &WalletSignatures { + &self.signatures_wallet + } + + pub fn generate_pay_info(&self, provider_pk: [u8; 32]) -> NymPayInfo { + NymPayInfo::generate(provider_pk) + } + + pub fn prepare_for_spending( + &mut self, + verification_key: &VerificationKeyAuth, + pay_info: PayInfo, + coin_indices_signatures: &[BI], + expiration_date_signatures: &[BE], + tickets_to_spend: u64, + ) -> Result + where + BI: Borrow, + BE: Borrow, + { + let params = nym_credentials_interface::ecash_parameters(); + let spend_date = ecash_today(); + + // make sure we still have enough tickets to spend + Wallet::ensure_allowance(params, self.spent_tickets, tickets_to_spend)?; + + let payment = self.signatures_wallet.spend( + params, + verification_key, + &self.ecash_secret_key, + &pay_info, + self.spent_tickets, + tickets_to_spend, + expiration_date_signatures, + coin_indices_signatures, + spend_date.ecash_unix_timestamp(), + )?; + + self.spent_tickets += tickets_to_spend; + + Ok(CredentialSpendingData { + payment, + pay_info, + spend_date: spend_date.ecash_date(), + epoch_id: self.epoch_id, + }) + } +} + +impl VersionedSerialise for IssuedTicketBook { + const CURRENT_SERIALISATION_REVISION: u8 = 1; + + fn try_unpack(b: &[u8], revision: impl Into>) -> Result { + let revision = revision + .into() + .unwrap_or(::CURRENT_SERIALISATION_REVISION); + + match revision { + 1 => Self::try_unpack_current(b), + _ => Err(Error::UnknownSerializationRevision { revision }), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_zeroize_on_drop() {} + + fn assert_zeroize() {} + + #[test] + fn credential_is_zeroized() { + assert_zeroize::(); + assert_zeroize_on_drop::(); + } +} diff --git a/common/credentials/src/ecash/bandwidth/mod.rs b/common/credentials/src/ecash/bandwidth/mod.rs new file mode 100644 index 0000000000..ce75507df6 --- /dev/null +++ b/common/credentials/src/ecash/bandwidth/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2021-2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub use issuance::IssuanceTicketBook; +pub use issued::IssuedTicketBook; +pub use nym_credentials_interface::{CredentialSigningData, CredentialSpendingData}; + +pub mod issuance; +pub mod issued; +pub mod serialiser; diff --git a/common/credentials/src/ecash/bandwidth/serialiser.rs b/common/credentials/src/ecash/bandwidth/serialiser.rs new file mode 100644 index 0000000000..acb7629002 --- /dev/null +++ b/common/credentials/src/ecash/bandwidth/serialiser.rs @@ -0,0 +1,65 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash::bandwidth::issued::CURRENT_SERIALIZATION_REVISION; +use crate::Error; +use bincode::Options; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::marker::PhantomData; + +pub struct VersionSerialised { + pub data: Vec, + pub revision: u8, + + // still wondering if there's any point in having the phantom in here + _phantom: PhantomData, +} + +pub trait VersionedSerialise { + const CURRENT_SERIALISATION_REVISION: u8; + + fn current_serialization_revision(&self) -> u8 { + CURRENT_SERIALIZATION_REVISION + } + + // implicitly always uses current revision + fn pack(&self) -> VersionSerialised + where + Self: Serialize, + { + let data = make_current_storable_bincode_serializer() + .serialize(self) + .expect("serialisation failure"); + + VersionSerialised { + data, + revision: Self::CURRENT_SERIALISATION_REVISION, + _phantom: Default::default(), + } + } + + fn try_unpack_current(b: &[u8]) -> Result + where + Self: DeserializeOwned, + { + make_current_storable_bincode_serializer() + .deserialize(b) + .map_err(|source| Error::SerializationFailure { + source, + revision: Self::CURRENT_SERIALISATION_REVISION, + }) + } + + // this is up to whoever implements the trait to provide function implementation, + // as they might have to have different implementations per revision + fn try_unpack(b: &[u8], revision: impl Into>) -> Result + where + Self: DeserializeOwned; +} + +fn make_current_storable_bincode_serializer() -> impl bincode::Options { + bincode::DefaultOptions::new() + .with_big_endian() + .with_varint_encoding() +} diff --git a/common/credentials/src/coconut/mod.rs b/common/credentials/src/ecash/mod.rs similarity index 100% rename from common/credentials/src/coconut/mod.rs rename to common/credentials/src/ecash/mod.rs diff --git a/common/credentials/src/ecash/utils.rs b/common/credentials/src/ecash/utils.rs new file mode 100644 index 0000000000..970288b075 --- /dev/null +++ b/common/credentials/src/ecash/utils.rs @@ -0,0 +1,194 @@ +// Copyright 2021-2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash::bandwidth::IssuanceTicketBook; +use crate::error::Error; +use log::{debug, warn}; +use nym_credentials_interface::{ + aggregate_expiration_signatures, aggregate_indices_signatures, Base58, CoinIndexSignature, + CoinIndexSignatureShare, ExpirationDateSignature, ExpirationDateSignatureShare, + VerificationKeyAuth, WalletSignatures, +}; +use nym_validator_client::client::EcashApiClient; + +// so we wouldn't break all the existing imports +pub use nym_ecash_time::{cred_exp_date, ecash_date_offset, ecash_today, EcashTime}; + +pub fn aggregate_verification_keys( + api_clients: &[EcashApiClient], +) -> Result { + if api_clients.is_empty() { + return Err(Error::NoValidatorsAvailable); + } + + 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(nym_credentials_interface::aggregate_verification_keys( + &shares, + Some(&indices), + )?) +} + +pub async fn obtain_expiration_date_signatures( + ecash_api_clients: &[EcashApiClient], + verification_key: &VerificationKeyAuth, + threshold: u64, +) -> Result, Error> { + if ecash_api_clients.is_empty() { + return Err(Error::NoValidatorsAvailable); + } + + let mut signatures_shares: Vec<_> = Vec::with_capacity(ecash_api_clients.len()); + + let expiration_date = cred_exp_date().ecash_unix_timestamp(); + for ecash_api_client in ecash_api_clients.iter() { + match ecash_api_client + .api_client + .partial_expiration_date_signatures(None) + .await + { + Ok(signature) => { + let index = ecash_api_client.node_id; + let key_share = ecash_api_client.verification_key.clone(); + signatures_shares.push(ExpirationDateSignatureShare { + index, + key: key_share, + signatures: signature.signatures, + }); + } + Err(err) => { + warn!( + "failed to obtain expiration date signature from {}: {err}", + ecash_api_client.api_client.api_url() + ); + } + } + } + + if signatures_shares.len() < threshold as usize { + return Err(Error::NotEnoughShares); + } + + //this already takes care of partial signatures validation + aggregate_expiration_signatures(verification_key, expiration_date, &signatures_shares) + .map_err(Error::CompactEcashError) +} + +pub async fn obtain_coin_indices_signatures( + ecash_api_clients: &[EcashApiClient], + verification_key: &VerificationKeyAuth, + threshold: u64, +) -> Result, Error> { + if ecash_api_clients.is_empty() { + return Err(Error::NoValidatorsAvailable); + } + + let mut signatures_shares: Vec<_> = Vec::with_capacity(ecash_api_clients.len()); + + for ecash_api_client in ecash_api_clients.iter() { + match ecash_api_client + .api_client + .partial_coin_indices_signatures(None) + .await + { + Ok(signature) => { + let index = ecash_api_client.node_id; + let key_share = ecash_api_client.verification_key.clone(); + signatures_shares.push(CoinIndexSignatureShare { + index, + key: key_share, + signatures: signature.signatures, + }); + } + Err(err) => { + warn!( + "failed to obtain expiration date signature from {}: {err}", + ecash_api_client.api_client.api_url() + ); + } + } + } + + if signatures_shares.len() < threshold as usize { + return Err(Error::NotEnoughShares); + } + + //this takes care of validating partial signatures + aggregate_indices_signatures( + nym_credentials_interface::ecash_parameters(), + verification_key, + &signatures_shares, + ) + .map_err(Error::CompactEcashError) +} + +pub async fn obtain_aggregate_wallet( + voucher: &IssuanceTicketBook, + ecash_api_clients: &[EcashApiClient], + threshold: u64, +) -> Result { + if ecash_api_clients.len() < threshold as usize { + return Err(Error::NoValidatorsAvailable); + } + let verification_key = aggregate_verification_keys(ecash_api_clients)?; + + let request = voucher.prepare_for_signing(); + + let mut wallets = Vec::with_capacity(ecash_api_clients.len()); + + // TODO: optimise and query just threshold + for ecash_api_client in ecash_api_clients.iter() { + debug!( + "attempting to obtain partial credential from {}", + ecash_api_client.api_client.api_url() + ); + + match voucher + .obtain_partial_bandwidth_voucher_credential( + &ecash_api_client.api_client, + ecash_api_client.node_id, + &ecash_api_client.verification_key, + request.clone(), + ) + .await + { + Ok(wallet) => wallets.push(wallet), + Err(err) => { + warn!("failed to obtain partial credential from API {ecash_api_client}: {err}",); + } + }; + } + if wallets.len() < threshold as usize { + return Err(Error::NotEnoughShares); + } + + voucher.aggregate_signature_shares(&verification_key, &wallets, request) +} + +pub fn signatures_to_string(sigs: &[B]) -> String +where + B: Base58, +{ + sigs.iter() + .map(|sig| sig.to_bs58()) + .collect::>() + .join(",") +} + +pub fn signatures_from_string(bs58_sigs: String) -> Result, Error> +where + B: Base58, +{ + bs58_sigs + .split(',') + .map(B::try_from_bs58) + .collect::, _>>() + .map_err(Error::CompactEcashError) +} diff --git a/common/credentials/src/error.rs b/common/credentials/src/error.rs index 6487d23870..c1f99608ee 100644 --- a/common/credentials/src/error.rs +++ b/common/credentials/src/error.rs @@ -1,11 +1,10 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_credentials_interface::CoconutError; +use crate::ecash::bandwidth::issued::CURRENT_SERIALIZATION_REVISION; +use nym_credentials_interface::CompactEcashError; use nym_crypto::asymmetric::encryption::KeyRecoveryError; use nym_validator_client::ValidatorClientError; - -use crate::coconut::bandwidth::issued::CURRENT_SERIALIZATION_REVISION; use thiserror::Error; #[derive(Debug, Error)] @@ -26,14 +25,11 @@ pub enum Error { #[error("unknown credential serializatio revision {revision}. the current (and max supported) version is {CURRENT_SERIALIZATION_REVISION}")] UnknownSerializationRevision { revision: u8 }, - #[error("The detailed description is yet to be determined")] - BandwidthCredentialError, - #[error("Could not contact any validator")] NoValidatorsAvailable, - #[error("Ran into a coconut error - {0}")] - CoconutError(#[from] CoconutError), + #[error("Ran into a Compact ecash error - {0}")] + CompactEcashError(#[from] CompactEcashError), #[error("Ran into a validator client error - {0}")] ValidatorClientError(#[from] ValidatorClientError), @@ -51,7 +47,7 @@ pub enum Error { NotEnoughShares, #[error("Could not aggregate signature shares - {0}. Try again using the recovery command")] - SignatureAggregationError(CoconutError), + SignatureAggregationError(CompactEcashError), #[error("Could not deserialize bandwidth voucher - {0}")] BandwidthVoucherDeserializationError(String), @@ -59,9 +55,6 @@ pub enum Error { #[error("the provided issuance data wasn't prepared for a bandwidth voucher")] NotABandwdithVoucher, - #[error("the provided issuance data wasn't prepared for a free pass")] - NotAFreePass, - #[error("failed to create a secp256k1 signature")] Secp256k1SignFailure, } diff --git a/common/credentials/src/lib.rs b/common/credentials/src/lib.rs index c06eeb0a05..7f2c3ef2d4 100644 --- a/common/credentials/src/lib.rs +++ b/common/credentials/src/lib.rs @@ -1,12 +1,11 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -pub mod coconut; +pub mod ecash; pub mod error; -pub use coconut::bandwidth::{ - CredentialSigningData, CredentialSpendingData, IssuanceBandwidthCredential, - IssuedBandwidthCredential, +pub use ecash::bandwidth::{ + CredentialSigningData, CredentialSpendingData, IssuanceTicketBook, IssuedTicketBook, }; -pub use coconut::utils::{obtain_aggregate_signature, obtain_aggregate_verification_key}; +pub use ecash::utils::{aggregate_verification_keys, obtain_aggregate_wallet}; pub use error::Error; diff --git a/common/crypto/src/asymmetric/encryption/mod.rs b/common/crypto/src/asymmetric/encryption/mod.rs index cadb5437e4..75ea0b2206 100644 --- a/common/crypto/src/asymmetric/encryption/mod.rs +++ b/common/crypto/src/asymmetric/encryption/mod.rs @@ -81,6 +81,15 @@ impl KeyPair { } } +impl From for KeyPair { + fn from(private_key: PrivateKey) -> Self { + KeyPair { + public_key: (&private_key).into(), + private_key, + } + } +} + impl PemStorableKeyPair for KeyPair { type PrivatePemKey = PrivateKey; type PublicPemKey = PublicKey; diff --git a/common/crypto/src/asymmetric/identity/mod.rs b/common/crypto/src/asymmetric/identity/mod.rs index f65290f907..41afaf67a4 100644 --- a/common/crypto/src/asymmetric/identity/mod.rs +++ b/common/crypto/src/asymmetric/identity/mod.rs @@ -91,6 +91,15 @@ impl KeyPair { } } +impl From for KeyPair { + fn from(private_key: PrivateKey) -> Self { + KeyPair { + public_key: (&private_key).into(), + private_key, + } + } +} + impl PemStorableKeyPair for KeyPair { type PrivatePemKey = PrivateKey; type PublicPemKey = PublicKey; diff --git a/common/crypto/src/shared_key.rs b/common/crypto/src/shared_key.rs index bcc707489b..f90e99feb2 100644 --- a/common/crypto/src/shared_key.rs +++ b/common/crypto/src/shared_key.rs @@ -3,12 +3,13 @@ use crate::asymmetric::encryption; use crate::hkdf; -#[cfg(feature = "rand")] -use cipher::crypto_common::rand_core::{CryptoRng, RngCore}; use cipher::{Key, KeyIvInit, StreamCipher}; use digest::crypto_common::BlockSizeUser; use digest::Digest; +#[cfg(feature = "rand")] +use rand::{CryptoRng, RngCore}; + /// Generate an ephemeral encryption keypair and perform diffie-hellman to establish /// shared key with the remote. #[cfg(feature = "rand")] diff --git a/common/ecash-double-spending/Cargo.toml b/common/ecash-double-spending/Cargo.toml new file mode 100644 index 0000000000..feb237f5d6 --- /dev/null +++ b/common/ecash-double-spending/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "nym-ecash-double-spending" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +bit-vec = { workspace = true } +bloomfilter = { workspace = true } +nym-network-defaults = { path = "../network-defaults" } diff --git a/common/ecash-double-spending/src/lib.rs b/common/ecash-double-spending/src/lib.rs new file mode 100644 index 0000000000..78f24dba38 --- /dev/null +++ b/common/ecash-double-spending/src/lib.rs @@ -0,0 +1,136 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bit_vec::BitVec; +use bloomfilter::Bloom; +use nym_network_defaults::{BloomfilterParameters, ECASH_DS_BLOOMFILTER_PARAMS}; + +pub struct DoubleSpendingFilter { + params: BloomfilterParameters, + inner: Bloom>, +} + +impl Default for DoubleSpendingFilter { + fn default() -> Self { + DoubleSpendingFilter::new_empty_ecash() + } +} + +pub fn bloom_from_params(params: &BloomfilterParameters, bitvec: BitVec) -> Bloom> { + assert_eq!(params.bitmap_size, bitvec.len() as u64); + + Bloom::from_bit_vec( + bitvec, + params.bitmap_size, + params.num_hashes, + params.sip_keys, + ) +} + +impl DoubleSpendingFilter { + pub fn new_empty(params: BloomfilterParameters) -> Self { + let bitvec = BitVec::from_elem(params.bitmap_size as usize, false); + DoubleSpendingFilter { + inner: bloom_from_params(¶ms, bitvec), + params, + } + } + + pub fn params(&self) -> BloomfilterParameters { + self.params + } + + pub fn rebuild(&self) -> DoubleSpendingFilterBuilder { + DoubleSpendingFilterBuilder::new(self.params) + } + + pub fn reset(&mut self) { + self.inner.clear() + } + + pub fn new_empty_ecash() -> Self { + DoubleSpendingFilter::new_empty(ECASH_DS_BLOOMFILTER_PARAMS) + } + + pub fn builder(params: BloomfilterParameters) -> DoubleSpendingFilterBuilder { + DoubleSpendingFilterBuilder::new(params) + } + + pub fn from_bytes(params: BloomfilterParameters, bitmap: &[u8]) -> Self { + DoubleSpendingFilter { + inner: bloom_from_params(¶ms, BitVec::from_bytes(bitmap)), + params, + } + } + + pub fn replace_bitvec(&mut self, new: BitVec) { + self.inner = bloom_from_params(&self.params, new) + } + + pub fn dump_bitmap(&self) -> Vec { + self.inner.bitmap() + } + + pub fn set(&mut self, b: &Vec) { + self.inner.set(b); + } + + pub fn check(&self, b: &Vec) -> bool { + self.inner.check(b) + } +} + +pub struct DoubleSpendingFilterBuilder { + params: BloomfilterParameters, + bit_vec_builder: Option, +} + +impl DoubleSpendingFilterBuilder { + pub fn new(params: BloomfilterParameters) -> Self { + DoubleSpendingFilterBuilder { + params, + bit_vec_builder: None, + } + } + + pub fn add_bytes(&mut self, b: &[u8]) -> bool { + match &mut self.bit_vec_builder { + None => { + self.bit_vec_builder = Some(BitVecBuilder::new(b)); + true + } + Some(builder) => builder.add_bytes(b), + } + } + + pub fn build(self) -> DoubleSpendingFilter { + match self.bit_vec_builder { + None => DoubleSpendingFilter::new_empty(self.params), + Some(builder) => DoubleSpendingFilter { + inner: bloom_from_params(&self.params, builder.finish()), + params: self.params, + }, + } + } +} + +pub struct BitVecBuilder(BitVec); + +impl BitVecBuilder { + pub fn new(initial_bitmap: &[u8]) -> Self { + BitVecBuilder(BitVec::from_bytes(initial_bitmap)) + } + + pub fn add_bytes(&mut self, b: &[u8]) -> bool { + let add = BitVec::from_bytes(b); + if self.0.len() != add.len() { + return false; + } + self.0.or(&add); + true + } + + pub fn finish(self) -> BitVec { + self.0 + } +} diff --git a/common/ecash-time/Cargo.toml b/common/ecash-time/Cargo.toml new file mode 100644 index 0000000000..4eaee90438 --- /dev/null +++ b/common/ecash-time/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "nym-ecash-time" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +time.workspace = true + +nym-compact-ecash = { path = "../nym_offline_compact_ecash", optional = true } + +[features] +expiration = ["nym-compact-ecash"] \ No newline at end of file diff --git a/common/ecash-time/src/lib.rs b/common/ecash-time/src/lib.rs new file mode 100644 index 0000000000..332b51e2e6 --- /dev/null +++ b/common/ecash-time/src/lib.rs @@ -0,0 +1,74 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use time::{Duration, PrimitiveDateTime, Time}; + +pub use time::{Date, OffsetDateTime}; + +pub trait EcashTime { + fn ecash_unix_timestamp(&self) -> u32 { + let ts = self.ecash_datetime().unix_timestamp(); + + // just panic on pre-1970 timestamps... + assert!(ts > 0); + + // and on anything in 22nd century... + assert!(ts <= u32::MAX as i64); + + ts as u32 + } + + fn ecash_date(&self) -> Date { + self.ecash_datetime().date() + } + + fn ecash_datetime(&self) -> OffsetDateTime; +} + +impl EcashTime for OffsetDateTime { + fn ecash_datetime(&self) -> OffsetDateTime { + self.replace_time(Time::MIDNIGHT) + } +} + +impl EcashTime for PrimitiveDateTime { + fn ecash_datetime(&self) -> OffsetDateTime { + self.assume_utc().ecash_datetime() + } +} + +impl EcashTime for Date { + fn ecash_datetime(&self) -> OffsetDateTime { + OffsetDateTime::new_utc(*self, Time::MIDNIGHT) + } +} + +pub fn ecash_today() -> OffsetDateTime { + OffsetDateTime::now_utc().ecash_datetime() +} + +pub fn ecash_today_date() -> Date { + ecash_today().ecash_date() +} + +// no point in supporting more than i8 variance +pub fn ecash_date_offset(offset: i8) -> OffsetDateTime { + let today = ecash_today(); + + let day = today + Duration::days(offset as i64); + + // make sure to correct the time in case of DST + day.replace_time(Time::MIDNIGHT) +} + +#[cfg(feature = "expiration")] +pub fn cred_exp_date() -> OffsetDateTime { + //count today as well + ecash_date_offset(nym_compact_ecash::constants::CRED_VALIDITY_PERIOD_DAYS as i8 - 1) + // ecash_today() + Duration::days(constants::CRED_VALIDITY_PERIOD_DAYS as i64 - 1) +} + +#[cfg(feature = "expiration")] +pub fn ecash_default_expiration_date() -> Date { + cred_exp_date().ecash_date() +} diff --git a/gateway/gateway-requests/Cargo.toml b/common/gateway-requests/Cargo.toml similarity index 68% rename from gateway/gateway-requests/Cargo.toml rename to common/gateway-requests/Cargo.toml index 9802a6c590..72a2bdfe9d 100644 --- a/gateway/gateway-requests/Cargo.toml +++ b/common/gateway-requests/Cargo.toml @@ -14,19 +14,20 @@ license.workspace = true bs58 = { workspace = true } futures = { workspace = true } generic-array = { workspace = true, features = ["serde"] } -log = { workspace = true } rand = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } thiserror = { workspace = true } +tracing = { workspace = true, features = ["log"] } zeroize = { workspace = true } -nym-crypto = { path = "../../common/crypto" } -nym-pemstore = { path = "../../common/pemstore" } -nym-sphinx = { path = "../../common/nymsphinx" } +nym-crypto = { path = "../crypto" } +nym-pemstore = { path = "../pemstore" } +nym-sphinx = { path = "../nymsphinx" } +nym-task = { path = "../task" } -nym-credentials = { path = "../../common/credentials" } -nym-credentials-interface = { path = "../../common/credentials-interface" } +nym-credentials = { path = "../credentials" } +nym-credentials-interface = { path = "../credentials-interface" } [target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio] workspace = true @@ -40,4 +41,5 @@ features = ["tokio"] workspace = true default-features = false - +[dev-dependencies] +nym-compact-ecash = { path = "../nym_offline_compact_ecash" } # we need specific imports in tests diff --git a/gateway/gateway-requests/src/authentication/encrypted_address.rs b/common/gateway-requests/src/authentication/encrypted_address.rs similarity index 100% rename from gateway/gateway-requests/src/authentication/encrypted_address.rs rename to common/gateway-requests/src/authentication/encrypted_address.rs diff --git a/gateway/gateway-requests/src/authentication/mod.rs b/common/gateway-requests/src/authentication/mod.rs similarity index 100% rename from gateway/gateway-requests/src/authentication/mod.rs rename to common/gateway-requests/src/authentication/mod.rs diff --git a/gateway/gateway-requests/src/iv.rs b/common/gateway-requests/src/iv.rs similarity index 100% rename from gateway/gateway-requests/src/iv.rs rename to common/gateway-requests/src/iv.rs diff --git a/gateway/gateway-requests/src/lib.rs b/common/gateway-requests/src/lib.rs similarity index 100% rename from gateway/gateway-requests/src/lib.rs rename to common/gateway-requests/src/lib.rs diff --git a/common/gateway-requests/src/models.rs b/common/gateway-requests/src/models.rs new file mode 100644 index 0000000000..32b2a9f812 --- /dev/null +++ b/common/gateway-requests/src/models.rs @@ -0,0 +1,141 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials::ecash::bandwidth::CredentialSpendingData; +use nym_credentials_interface::{Base58, Bytable, CompactEcashError}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct CredentialSpendingRequest { + /// The cryptographic material required for spending the underlying credential. + pub data: CredentialSpendingData, +} + +impl CredentialSpendingRequest { + pub fn new(data: CredentialSpendingData) -> Self { + CredentialSpendingRequest { data } + } + + // pub fn matches_serial_number( + // &self, + // serial_number_bs58: &str, + // ) -> Result { + // self.data.payment.has_serial_number(serial_number_bs58) + // } + + pub fn to_bytes(&self) -> Vec { + self.data.to_bytes() + } + + pub fn try_from_bytes(raw: &[u8]) -> Result { + Ok(CredentialSpendingRequest { + data: CredentialSpendingData::try_from_bytes(raw)?, + }) + } + + pub fn encoded_serial_number(&self) -> Vec { + self.data.encoded_serial_number() + } + + pub fn serial_number_bs58(&self) -> String { + self.data.serial_number_b58() + } +} + +impl Bytable for CredentialSpendingRequest { + fn to_byte_vec(&self) -> Vec { + self.to_bytes() + } + + fn try_from_byte_slice(slice: &[u8]) -> Result { + Self::try_from_bytes(slice) + } +} + +impl Base58 for CredentialSpendingRequest {} + +#[cfg(test)] +mod tests { + use super::*; + use nym_compact_ecash::{ + issue, + tests::helpers::{generate_coin_indices_signatures, generate_expiration_date_signatures}, + ttp_keygen, PayInfo, + }; + use nym_credentials::ecash::utils::EcashTime; + use nym_credentials::IssuanceTicketBook; + use nym_credentials_interface::TicketType; + use nym_crypto::asymmetric::ed25519; + use rand::rngs::OsRng; + + #[test] + fn credential_roundtrip() { + // make valid request + let keypair = ttp_keygen(1, 1).unwrap().remove(0); + + let mut rng = OsRng; + let signing_key = ed25519::PrivateKey::new(&mut rng); + + let issuance = IssuanceTicketBook::new(42, [], signing_key, TicketType::V1MixnetEntry); + let expiration_date = issuance.expiration_date(); + let sig_req = issuance.prepare_for_signing(); + let exp_date_sigs = generate_expiration_date_signatures( + sig_req.expiration_date.ecash_unix_timestamp(), + &[keypair.secret_key()], + &vec![keypair.verification_key()], + &keypair.verification_key(), + &[keypair.index.unwrap()], + ) + .unwrap(); + let blind_sig = issue( + keypair.secret_key(), + sig_req.ecash_pub_key.clone(), + &sig_req.withdrawal_request, + expiration_date.ecash_unix_timestamp(), + issuance.ticketbook_type().encode(), + ) + .unwrap(); + + let partial_wallet = issuance + .unblind_signature( + &keypair.verification_key(), + &sig_req, + blind_sig, + keypair.index.unwrap(), + ) + .unwrap(); + + let wallet = issuance + .aggregate_signature_shares(&keypair.verification_key(), &vec![partial_wallet], sig_req) + .unwrap(); + + let mut issued = issuance.into_issued_ticketbook(wallet, 1); + let coin_indices_signatures = generate_coin_indices_signatures( + nym_credentials_interface::ecash_parameters(), + &[keypair.secret_key()], + &vec![keypair.verification_key()], + &keypair.verification_key(), + &[keypair.index.unwrap()], + ) + .unwrap(); + let pay_info = PayInfo { + pay_info_bytes: [6u8; 72], + }; + let spending = issued + .prepare_for_spending( + &keypair.verification_key(), + pay_info, + &coin_indices_signatures, + &exp_date_sigs, + 1, + ) + .unwrap(); + + let with_epoch = CredentialSpendingRequest { data: spending }; + + let bytes = with_epoch.to_bytes(); + let recovered = CredentialSpendingRequest::try_from_bytes(&bytes).unwrap(); + + assert_eq!(with_epoch, recovered); + } +} diff --git a/gateway/gateway-requests/src/registration/handshake/client.rs b/common/gateway-requests/src/registration/handshake/client.rs similarity index 97% rename from gateway/gateway-requests/src/registration/handshake/client.rs rename to common/gateway-requests/src/registration/handshake/client.rs index edb30ebf20..902a520bca 100644 --- a/gateway/gateway-requests/src/registration/handshake/client.rs +++ b/common/gateway-requests/src/registration/handshake/client.rs @@ -25,6 +25,7 @@ impl<'a> ClientHandshake<'a> { identity: &'a nym_crypto::asymmetric::identity::KeyPair, gateway_pubkey: identity::PublicKey, expects_credential_usage: bool, + #[cfg(not(target_arch = "wasm32"))] shutdown: nym_task::TaskClient, ) -> Self where S: Stream + Sink + Unpin + Send + 'a, @@ -35,6 +36,8 @@ impl<'a> ClientHandshake<'a> { identity, Some(gateway_pubkey), expects_credential_usage, + #[cfg(not(target_arch = "wasm32"))] + shutdown, ); ClientHandshake { diff --git a/gateway/gateway-requests/src/registration/handshake/error.rs b/common/gateway-requests/src/registration/handshake/error.rs similarity index 94% rename from gateway/gateway-requests/src/registration/handshake/error.rs rename to common/gateway-requests/src/registration/handshake/error.rs index 8fee78fd38..6e82ef040c 100644 --- a/gateway/gateway-requests/src/registration/handshake/error.rs +++ b/common/gateway-requests/src/registration/handshake/error.rs @@ -25,6 +25,8 @@ pub enum HandshakeError { MalformedRequest, #[error("sent request was malformed")] HandshakeFailure, + #[error("received shutdown")] + ReceivedShutdown, #[error("timed out waiting for a handshake message")] Timeout, diff --git a/gateway/gateway-requests/src/registration/handshake/gateway.rs b/common/gateway-requests/src/registration/handshake/gateway.rs similarity index 98% rename from gateway/gateway-requests/src/registration/handshake/gateway.rs rename to common/gateway-requests/src/registration/handshake/gateway.rs index bb0003d0a3..a42851a320 100644 --- a/gateway/gateway-requests/src/registration/handshake/gateway.rs +++ b/common/gateway-requests/src/registration/handshake/gateway.rs @@ -8,6 +8,7 @@ use futures::future::BoxFuture; use futures::task::{Context, Poll}; use futures::{Future, Sink, Stream}; use nym_crypto::asymmetric::encryption; +use nym_task::TaskClient; use rand::{CryptoRng, RngCore}; use std::pin::Pin; use tungstenite::Message as WsMessage; @@ -22,11 +23,12 @@ impl<'a> GatewayHandshake<'a> { ws_stream: &'a mut S, identity: &'a nym_crypto::asymmetric::identity::KeyPair, received_init_payload: Vec, + shutdown: TaskClient, ) -> Self where S: Stream + Sink + Unpin + Send + 'a, { - let mut state = State::new(rng, ws_stream, identity, None, true); + let mut state = State::new(rng, ws_stream, identity, None, true, shutdown); GatewayHandshake { handshake_future: Box::pin(async move { // If any step along the way failed (that are non-network related), diff --git a/gateway/gateway-requests/src/registration/handshake/mod.rs b/common/gateway-requests/src/registration/handshake/mod.rs similarity index 89% rename from gateway/gateway-requests/src/registration/handshake/mod.rs rename to common/gateway-requests/src/registration/handshake/mod.rs index 4e21fcfc0d..d3faec7903 100644 --- a/gateway/gateway-requests/src/registration/handshake/mod.rs +++ b/common/gateway-requests/src/registration/handshake/mod.rs @@ -8,6 +8,8 @@ use self::gateway::GatewayHandshake; pub use self::shared_key::{SharedKeySize, SharedKeys}; use futures::{Sink, Stream}; use nym_crypto::asymmetric::identity; +#[cfg(not(target_arch = "wasm32"))] +use nym_task::TaskClient; use rand::{CryptoRng, RngCore}; use tungstenite::{Error as WsError, Message as WsMessage}; @@ -31,6 +33,7 @@ pub async fn client_handshake<'a, S>( identity: &'a identity::KeyPair, gateway_pubkey: identity::PublicKey, expects_credential_usage: bool, + #[cfg(not(target_arch = "wasm32"))] shutdown: TaskClient, ) -> Result where S: Stream + Sink + Unpin + Send + 'a, @@ -41,6 +44,8 @@ where identity, gateway_pubkey, expects_credential_usage, + #[cfg(not(target_arch = "wasm32"))] + shutdown, ) .await } @@ -51,11 +56,12 @@ pub async fn gateway_handshake<'a, S>( ws_stream: &'a mut S, identity: &'a identity::KeyPair, received_init_payload: Vec, + shutdown: TaskClient, ) -> Result where S: Stream + Sink + Unpin + Send + 'a, { - GatewayHandshake::new(rng, ws_stream, identity, received_init_payload).await + GatewayHandshake::new(rng, ws_stream, identity, received_init_payload, shutdown).await } /* diff --git a/gateway/gateway-requests/src/registration/handshake/shared_key.rs b/common/gateway-requests/src/registration/handshake/shared_key.rs similarity index 100% rename from gateway/gateway-requests/src/registration/handshake/shared_key.rs rename to common/gateway-requests/src/registration/handshake/shared_key.rs diff --git a/gateway/gateway-requests/src/registration/handshake/state.rs b/common/gateway-requests/src/registration/handshake/state.rs similarity index 77% rename from gateway/gateway-requests/src/registration/handshake/state.rs rename to common/gateway-requests/src/registration/handshake/state.rs index 6212a919b5..70cd48620c 100644 --- a/gateway/gateway-requests/src/registration/handshake/state.rs +++ b/common/gateway-requests/src/registration/handshake/state.rs @@ -6,7 +6,6 @@ use crate::registration::handshake::shared_key::{SharedKeySize, SharedKeys}; use crate::registration::handshake::WsItem; use crate::types; use futures::{Sink, SinkExt, Stream, StreamExt}; -use log::*; use nym_crypto::{ asymmetric::{encryption, identity}, generic_array::typenum::Unsigned, @@ -14,7 +13,10 @@ use nym_crypto::{ symmetric::stream_cipher, }; use nym_sphinx::params::{GatewayEncryptionAlgorithm, GatewaySharedKeyHkdfAlgorithm}; +#[cfg(not(target_arch = "wasm32"))] +use nym_task::TaskClient; use rand::{CryptoRng, RngCore}; +use tracing::log::*; use std::str::FromStr; use std::time::Duration; @@ -48,6 +50,10 @@ pub(crate) struct State<'a, S> { // this field is really out of place here, however, we need to propagate this information somehow // in order to establish correct protocol for backwards compatibility reasons expects_credential_usage: bool, + + // channel to receive shutdown signal + #[cfg(not(target_arch = "wasm32"))] + shutdown: TaskClient, } impl<'a, S> State<'a, S> { @@ -57,6 +63,7 @@ impl<'a, S> State<'a, S> { identity: &'a identity::KeyPair, remote_pubkey: Option, expects_credential_usage: bool, + #[cfg(not(target_arch = "wasm32"))] shutdown: TaskClient, ) -> Self { let ephemeral_keypair = encryption::KeyPair::new(rng); State { @@ -66,6 +73,8 @@ impl<'a, S> State<'a, S> { remote_pubkey, derived_shared_keys: None, expects_credential_usage, + #[cfg(not(target_arch = "wasm32"))] + shutdown, } } @@ -199,46 +208,76 @@ impl<'a, S> State<'a, S> { self.remote_pubkey = Some(remote_pubkey) } + fn on_wg_msg(msg: Option) -> Result>, HandshakeError> { + let Some(msg) = msg else { + return Err(HandshakeError::ClosedStream); + }; + + let Ok(msg) = msg else { + return Err(HandshakeError::NetworkError); + }; + match msg { + WsMessage::Text(ref ws_msg) => { + match types::RegistrationHandshake::from_str(ws_msg) { + Ok(reg_handshake_msg) => { + match reg_handshake_msg { + // 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(Some(data)) + } + types::RegistrationHandshake::HandshakeError { message } => { + Err(HandshakeError::RemoteError(message)) + } + } + } + Err(_) => { + error!("Received a non-handshake message during the registration handshake! It's getting dropped. The received content was: '{msg}'"); + Ok(None) + } + } + } + _ => { + error!("Received non-text message during registration handshake"); + Ok(None) + } + } + } + + #[cfg(not(target_arch = "wasm32"))] async fn _receive_handshake_message(&mut self) -> Result, HandshakeError> where S: Stream + Unpin, { loop { - let Some(msg) = self.ws_stream.next().await else { - return Err(HandshakeError::ClosedStream); - }; - - let Ok(msg) = msg else { - return Err(HandshakeError::NetworkError); - }; - - match msg { - WsMessage::Text(ref ws_msg) => { - match types::RegistrationHandshake::from_str(ws_msg) { - Ok(reg_handshake_msg) => { - return match reg_handshake_msg { - // 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. The received content was: '{msg}'"); - continue; - } - } + tokio::select! { + biased; + _ = self.shutdown.recv() => return Err(HandshakeError::ReceivedShutdown), + msg = self.ws_stream.next() => { + let Some(ret) = Self::on_wg_msg(msg)? else { + continue; + }; + return Ok(ret); } - _ => error!("Received non-text message during registration handshake"), } } } + #[cfg(target_arch = "wasm32")] + async fn _receive_handshake_message(&mut self) -> Result, HandshakeError> + where + S: Stream + Unpin, + { + loop { + let msg = self.ws_stream.next().await; + let Some(ret) = Self::on_wg_msg(msg)? else { + continue; + }; + return Ok(ret); + } + } + pub(crate) async fn receive_handshake_message(&mut self) -> Result, HandshakeError> where S: Stream + Unpin, diff --git a/gateway/gateway-requests/src/registration/mod.rs b/common/gateway-requests/src/registration/mod.rs similarity index 100% rename from gateway/gateway-requests/src/registration/mod.rs rename to common/gateway-requests/src/registration/mod.rs diff --git a/gateway/gateway-requests/src/types.rs b/common/gateway-requests/src/types.rs similarity index 89% rename from gateway/gateway-requests/src/types.rs rename to common/gateway-requests/src/types.rs index 6c564d4ae8..f9006300a1 100644 --- a/gateway/gateway-requests/src/types.rs +++ b/common/gateway-requests/src/types.rs @@ -2,13 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 use crate::authentication::encrypted_address::EncryptedAddressBytes; -use crate::iv::IV; -use crate::models::{CredentialSpendingRequest, OldV1Credential}; +use crate::iv::{IVConversionError, IV}; +use crate::models::CredentialSpendingRequest; use crate::registration::handshake::SharedKeys; use crate::{GatewayMacSize, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION}; -use log::error; -use nym_credentials::coconut::bandwidth::CredentialSpendingData; -use nym_credentials_interface::{CoconutError, UnknownCredentialType}; +use nym_credentials::ecash::bandwidth::CredentialSpendingData; +use nym_credentials_interface::CompactEcashError; use nym_crypto::generic_array::typenum::Unsigned; use nym_crypto::hmac::recompute_keyed_hmac_and_verify_tag; use nym_crypto::symmetric::stream_cipher; @@ -18,6 +17,7 @@ use nym_sphinx::params::packet_sizes::PacketSize; use nym_sphinx::params::{GatewayEncryptionAlgorithm, GatewayIntegrityHmacAlgorithm}; use nym_sphinx::DestinationAddressBytes; use serde::{Deserialize, Serialize}; +use tracing::log::error; use std::str::FromStr; use std::string::FromUtf8Error; @@ -84,6 +84,23 @@ impl TryInto for RegistrationHandshake { } } +// specific errors (that should not be nested!!) for clients to match on +#[derive(Debug, Copy, Clone, Error, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum SimpleGatewayRequestsError { + #[error("insufficient bandwidth available to process the request. required: {required}B, available: {available}B")] + OutOfBandwidth { required: i64, available: i64 }, + + #[error("the provided ticket has already been spent before at this gateway")] + TicketReplay, +} + +impl SimpleGatewayRequestsError { + pub fn is_ticket_replay(&self) -> bool { + matches!(self, SimpleGatewayRequestsError::TicketReplay) + } +} + #[derive(Debug, Error)] pub enum GatewayRequestsError { #[error("the request is too short")] @@ -92,6 +109,9 @@ pub enum GatewayRequestsError { #[error("provided MAC is invalid")] InvalidMac, + #[error("Provided bandwidth IV is malformed: {0}")] + MalformedIV(#[from] IVConversionError), + #[error("address field was incorrectly encoded: {source}")] IncorrectlyEncodedAddress { #[from] @@ -122,20 +142,21 @@ pub enum GatewayRequestsError { source: MixPacketFormattingError, }, + #[error("failed to deserialize provided credential: {0}")] + EcashCredentialDeserializationFailure(#[from] CompactEcashError), + #[error("failed to deserialize provided credential: EOF")] CredentialDeserializationFailureEOF, #[error("failed to deserialize provided credential: malformed string: {0}")] CredentialDeserializationFailureMalformedString(#[from] FromUtf8Error), - #[error("failed to deserialize provided credential: {0}")] - CredentialDeserializationFailureUnknownType(#[from] UnknownCredentialType), - - #[error("failed to deserialize provided credential: malformed verify request: {0}")] - CredentialDeserializationFailureMalformedTheta(CoconutError), - #[error("the provided [v1] credential has invalid number of parameters - {0}")] InvalidNumberOfEmbededParameters(u32), + + // variant to catch legacy errors + #[error("{0}")] + Other(String), } #[derive(Serialize, Deserialize, Debug)] @@ -164,6 +185,10 @@ pub enum ClientControlRequest { enc_credential: Vec, iv: Vec, }, + EcashCredential { + enc_credential: Vec, + iv: Vec, + }, ClaimFreeTestnetBandwidth, } @@ -200,37 +225,14 @@ impl ClientControlRequest { ClientControlRequest::BandwidthCredentialV2 { .. } => { "BandwidthCredentialV2".to_string() } + ClientControlRequest::EcashCredential { .. } => "EcashCredential".to_string(), ClientControlRequest::ClaimFreeTestnetBandwidth => { "ClaimFreeTestnetBandwidth".to_string() } } } - pub fn new_enc_coconut_bandwidth_credential_v1( - credential: &OldV1Credential, - shared_key: &SharedKeys, - iv: IV, - ) -> Self { - let serialized_credential = credential.as_bytes(); - let enc_credential = shared_key.encrypt_and_tag(&serialized_credential, Some(iv.inner())); - - ClientControlRequest::BandwidthCredential { - enc_credential, - iv: iv.to_bytes(), - } - } - - pub fn try_from_enc_coconut_bandwidth_credential_v1( - enc_credential: Vec, - shared_key: &SharedKeys, - iv: IV, - ) -> Result { - let credential_bytes = shared_key.decrypt_tagged(&enc_credential, Some(iv.inner()))?; - OldV1Credential::from_bytes(&credential_bytes) - .map_err(|_| GatewayRequestsError::MalformedEncryption) - } - - pub fn new_enc_coconut_bandwidth_credential_v2( + pub fn new_enc_ecash_credential( credential: CredentialSpendingData, shared_key: &SharedKeys, iv: IV, @@ -239,19 +241,20 @@ impl ClientControlRequest { let serialized_credential = cred.to_bytes(); let enc_credential = shared_key.encrypt_and_tag(&serialized_credential, Some(iv.inner())); - ClientControlRequest::BandwidthCredentialV2 { + ClientControlRequest::EcashCredential { enc_credential, iv: iv.to_bytes(), } } - pub fn try_from_enc_coconut_bandwidth_credential_v2( + pub fn try_from_enc_ecash_credential( enc_credential: Vec, shared_key: &SharedKeys, - iv: IV, + iv: Vec, ) -> Result { + let iv = IV::try_from_bytes(&iv)?; let credential_bytes = shared_key.decrypt_tagged(&enc_credential, Some(iv.inner()))?; - CredentialSpendingRequest::try_from_bytes(&credential_bytes) + CredentialSpendingRequest::try_from_bytes(credential_bytes.as_slice()) .map_err(|_| GatewayRequestsError::MalformedEncryption) } } @@ -301,9 +304,15 @@ pub enum ServerResponse { Send { remaining_bandwidth: i64, }, + // Generic error Error { message: String, }, + // Specific typed errors + // so that clients could match on this variant without doing naive string matching + TypedError { + error: SimpleGatewayRequestsError, + }, } impl ServerResponse { @@ -314,6 +323,7 @@ impl ServerResponse { ServerResponse::Bandwidth { .. } => "Bandwidth".to_string(), ServerResponse::Send { .. } => "Send".to_string(), ServerResponse::Error { .. } => "Error".to_string(), + ServerResponse::TypedError { .. } => "TypedError".to_string(), } } pub fn new_error>(msg: S) -> Self { diff --git a/common/gateway-storage/Cargo.toml b/common/gateway-storage/Cargo.toml new file mode 100644 index 0000000000..26b2be56a1 --- /dev/null +++ b/common/gateway-storage/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "nym-gateway-storage" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +async-trait = { workspace = true } +bincode = { workspace = true } +defguard_wireguard_rs = { workspace = true } +log = { workspace = true } +sqlx = { workspace = true, features = [ + "runtime-tokio-rustls", + "sqlite", + "macros", + "migrate", + "time", +] } +time = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } + +nym-credentials-interface = { path = "../credentials-interface" } +nym-gateway-requests = { path = "../gateway-requests" } +nym-sphinx = { path = "../nymsphinx" } + +[build-dependencies] +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +sqlx = { workspace = true, features = [ + "runtime-tokio-rustls", + "sqlite", + "macros", + "migrate", +] } diff --git a/gateway/build.rs b/common/gateway-storage/build.rs similarity index 100% rename from gateway/build.rs rename to common/gateway-storage/build.rs diff --git a/gateway/migrations/20210921120000_create_initial_tables.sql b/common/gateway-storage/migrations/20210921120000_create_initial_tables.sql similarity index 100% rename from gateway/migrations/20210921120000_create_initial_tables.sql rename to common/gateway-storage/migrations/20210921120000_create_initial_tables.sql diff --git a/gateway/migrations/20240126120000_store_serial_numbers.sql b/common/gateway-storage/migrations/20240126120000_store_serial_numbers.sql similarity index 100% rename from gateway/migrations/20240126120000_store_serial_numbers.sql rename to common/gateway-storage/migrations/20240126120000_store_serial_numbers.sql diff --git a/gateway/migrations/20240301120000_freepass_expiration.sql b/common/gateway-storage/migrations/20240301120000_freepass_expiration.sql similarity index 100% rename from gateway/migrations/20240301120000_freepass_expiration.sql rename to common/gateway-storage/migrations/20240301120000_freepass_expiration.sql diff --git a/common/gateway-storage/migrations/20240624120000_ecash_changes.sql b/common/gateway-storage/migrations/20240624120000_ecash_changes.sql new file mode 100644 index 0000000000..502de5c568 --- /dev/null +++ b/common/gateway-storage/migrations/20240624120000_ecash_changes.sql @@ -0,0 +1,93 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +ALTER TABLE available_bandwidth +RENAME COLUMN freepass_expiration TO expiration; + +DROP TABLE spent_credential; + +-- we need the id field to prevent data duplication +CREATE TABLE shared_keys_tmp ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + client_address_bs58 TEXT NOT NULL UNIQUE, + derived_aes128_ctr_blake3_hmac_keys_bs58 TEXT NOT NULL +); + +INSERT INTO shared_keys_tmp (client_address_bs58, derived_aes128_ctr_blake3_hmac_keys_bs58) +SELECT * FROM shared_keys; + +-- ideally this table would be called "clients" but I don't want to cause too many breaking changes +DROP TABLE shared_keys; +ALTER TABLE shared_keys_tmp RENAME TO shared_keys; + +CREATE TABLE available_bandwidth_tmp ( + client_id INTEGER NOT NULL PRIMARY KEY REFERENCES shared_keys(id), + available INTEGER NOT NULL, + expiration TIMESTAMP WITHOUT TIME ZONE +); + +INSERT INTO available_bandwidth_tmp (client_id, available, expiration) +SELECT t1.id as client_id, t2.available, t2.expiration + FROM shared_keys as t1 + JOIN available_bandwidth as t2 + ON t1.client_address_bs58 = t2.client_address_bs58; + +DROP TABLE available_bandwidth; +ALTER TABLE available_bandwidth_tmp RENAME TO available_bandwidth; + +CREATE TABLE ecash_signer ( + epoch_id INTEGER NOT NULL, + +-- unique id assigned by the DKG contract. it does not change between epochs + signer_id INTEGER NOT NULL +); + +CREATE TABLE received_ticket ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + client_id INTEGER NOT NULL REFERENCES shared_keys(id), + received_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + rejected BOOLEAN +); + +CREATE INDEX received_ticket_index ON received_ticket (received_at); + +-- received tickets that are in the process of verifying +CREATE TABLE ticket_data ( + ticket_id INTEGER NOT NULL PRIMARY KEY REFERENCES received_ticket(id), + + -- serial_number, alongside the entire row, will get purged after redemption is complete + serial_number BLOB NOT NULL UNIQUE, + + -- data will get purged after 80% of signers verifies it + data BLOB +); + + +-- result of a verification from a single signer (API) +CREATE TABLE ticket_verification ( + ticket_id INTEGER NOT NULL REFERENCES received_ticket(id), + signer_id INTEGER NOT NULL, + verified_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + accepted BOOLEAN NOT NULL, + + PRIMARY KEY (ticket_id, signer_id) +); + +CREATE INDEX ticket_verification_index ON ticket_verification (ticket_id); + +-- verified tickets that are yet to be redeemed +CREATE TABLE verified_tickets ( + ticket_id INTEGER NOT NULL PRIMARY KEY REFERENCES received_ticket(id), + proposal_id INTEGER REFERENCES redemption_proposals(proposal_id) +); + +CREATE INDEX verified_tickets_index ON verified_tickets (proposal_id); + +CREATE TABLE redemption_proposals ( + proposal_id INTEGER NOT NULL PRIMARY KEY, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + resolved_at TIMESTAMP WITHOUT TIME ZONE, -- either got executed or got rejected + rejected BOOLEAN +); \ No newline at end of file diff --git a/common/gateway-storage/migrations/20240724120000_wireguard_peers.sql b/common/gateway-storage/migrations/20240724120000_wireguard_peers.sql new file mode 100644 index 0000000000..083c9f8a79 --- /dev/null +++ b/common/gateway-storage/migrations/20240724120000_wireguard_peers.sql @@ -0,0 +1,18 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: Apache-2.0 + */ + +CREATE TABLE wireguard_peer +( + public_key TEXT NOT NULL PRIMARY KEY UNIQUE, + preshared_key TEXT, + protocol_version INTEGER, + endpoint TEXT, + last_handshake TIMESTAMP, + tx_bytes BIGINT NOT NULL, + rx_bytes BIGINT NOT NULL, + persistent_keepalive_interval INTEGER, + allowed_ips BLOB NOT NULL, + suspended BOOLEAN NOT NULL +); \ No newline at end of file diff --git a/common/gateway-storage/src/bandwidth.rs b/common/gateway-storage/src/bandwidth.rs new file mode 100644 index 0000000000..3554213ffd --- /dev/null +++ b/common/gateway-storage/src/bandwidth.rs @@ -0,0 +1,177 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::models::PersistedBandwidth; +use time::OffsetDateTime; + +#[derive(Debug, Clone, Copy, Default)] +pub struct AvailableBandwidth { + pub bytes: i64, + pub freepass_expiration: Option, +} + +impl AvailableBandwidth { + pub fn freepass_expired(&self) -> bool { + if let Some(expiration) = self.freepass_expiration { + if expiration < OffsetDateTime::now_utc() { + return true; + } + } + false + } +} + +#[derive(Clone)] +pub struct BandwidthManager { + connection_pool: sqlx::SqlitePool, +} + +impl BandwidthManager { + /// Creates new instance of the `BandwidthManager` with the provided sqlite connection pool. + /// + /// # Arguments + /// + /// * `connection_pool`: database connection pool to use. + pub fn new(connection_pool: sqlx::SqlitePool) -> Self { + BandwidthManager { connection_pool } + } + + /// Creates a new bandwidth entry for the particular client. + pub(crate) async fn insert_new_client(&self, client_id: i64) -> Result<(), sqlx::Error> { + // FIXME: hack; we need to change api slightly + sqlx::query!( + "INSERT INTO available_bandwidth(client_id, available, expiration) VALUES (?, 0, ?)", + client_id, + OffsetDateTime::UNIX_EPOCH, + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + /// Set the expiration date of the particular client to the provided date. + pub(crate) async fn set_expiration( + &self, + client_id: i64, + expiration: OffsetDateTime, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + UPDATE available_bandwidth + SET expiration = ? + WHERE client_id = ? + "#, + expiration, + client_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + /// Reset all the bandwidth associated with the freepass and reset its expiration date + pub(crate) async fn reset_bandwidth(&self, client_id: i64) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + UPDATE available_bandwidth + SET available = 0, expiration = ? + WHERE client_id = ? + "#, + OffsetDateTime::UNIX_EPOCH, + client_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + /// Tries to retrieve available bandwidth for the particular client. + pub(crate) async fn get_available_bandwidth( + &self, + client_id: i64, + ) -> Result, sqlx::Error> { + sqlx::query_as("SELECT * FROM available_bandwidth WHERE client_id = ?") + .bind(client_id) + .fetch_optional(&self.connection_pool) + .await + } + + pub(crate) async fn increase_bandwidth( + &self, + client_id: i64, + amount: i64, + ) -> Result { + let mut tx = self.connection_pool.begin().await?; + sqlx::query!( + r#" + UPDATE available_bandwidth + SET available = available + ? + WHERE client_id = ? + "#, + amount, + client_id + ) + .execute(&mut tx) + .await?; + + let remaining = sqlx::query!( + "SELECT available FROM available_bandwidth WHERE client_id = ?", + client_id + ) + .fetch_one(&mut tx) + .await? + .available; + + tx.commit().await?; + Ok(remaining) + } + + pub(crate) async fn revoke_ticket_bandwidth( + &self, + ticket_id: i64, + amount: i64, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + UPDATE available_bandwidth + SET available = available - ? + WHERE client_id = (SELECT client_id FROM received_ticket WHERE id = ?) + "#, + amount, + ticket_id, + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn decrease_bandwidth( + &self, + client_id: i64, + amount: i64, + ) -> Result { + let mut tx = self.connection_pool.begin().await?; + sqlx::query!( + r#" + UPDATE available_bandwidth + SET available = available - ? + WHERE client_id = ? + "#, + amount, + client_id + ) + .execute(&mut tx) + .await?; + + let remaining = sqlx::query!( + "SELECT available FROM available_bandwidth WHERE client_id = ?", + client_id + ) + .fetch_one(&mut tx) + .await? + .available; + + tx.commit().await?; + Ok(remaining) + } +} diff --git a/gateway/src/node/storage/error.rs b/common/gateway-storage/src/error.rs similarity index 56% rename from gateway/src/node/storage/error.rs rename to common/gateway-storage/src/error.rs index f908eac083..178d630c10 100644 --- a/gateway/src/node/storage/error.rs +++ b/common/gateway-storage/src/error.rs @@ -10,4 +10,13 @@ pub enum StorageError { #[error("Failed to perform database migration: {0}")] MigrationError(#[from] sqlx::migrate::MigrateError), + + #[error("Somehow stored data is incorrect: {0}")] + DataCorruption(String), + + #[error("the stored data associated with ticket {ticket_id} is malformed!")] + MalformedStoredTicketData { ticket_id: i64 }, + + #[error("Failed to convert from type of database: {0}")] + TypeConversion(String), } diff --git a/gateway/src/node/storage/inboxes.rs b/common/gateway-storage/src/inboxes.rs similarity index 98% rename from gateway/src/node/storage/inboxes.rs rename to common/gateway-storage/src/inboxes.rs index c94459fb88..6c1ac23c47 100644 --- a/gateway/src/node/storage/inboxes.rs +++ b/common/gateway-storage/src/inboxes.rs @@ -1,7 +1,7 @@ // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node::storage::models::StoredMessage; +use crate::models::StoredMessage; #[derive(Clone)] pub(crate) struct InboxManager { diff --git a/common/gateway-storage/src/lib.rs b/common/gateway-storage/src/lib.rs new file mode 100644 index 0000000000..d6274ae854 --- /dev/null +++ b/common/gateway-storage/src/lib.rs @@ -0,0 +1,649 @@ +// Copyright 2020 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use async_trait::async_trait; +use bandwidth::BandwidthManager; +use error::StorageError; +use inboxes::InboxManager; +use models::{ + PersistedBandwidth, PersistedSharedKeys, RedemptionProposal, StoredMessage, VerifiedTicket, + WireguardPeer, +}; +use nym_credentials_interface::ClientTicket; +use nym_gateway_requests::registration::handshake::SharedKeys; +use nym_sphinx::DestinationAddressBytes; +use shared_keys::SharedKeysManager; +use sqlx::ConnectOptions; +use std::path::Path; +use tickets::TicketStorageManager; +use time::OffsetDateTime; +use tracing::{debug, error}; + +pub mod bandwidth; +pub mod error; +mod inboxes; +pub(crate) mod models; +mod shared_keys; +mod tickets; +mod wireguard_peers; + +#[async_trait] +pub trait Storage: Send + Sync { + async fn get_client_id( + &self, + client_address: DestinationAddressBytes, + ) -> Result; + + /// Inserts provided derived shared keys into the database. + /// If keys previously existed for the provided client, they are overwritten with the new data. + /// + /// # Arguments + /// + /// * `client_address`: address of the client + /// * `shared_keys`: shared encryption (AES128CTR) and mac (hmac-blake3) derived shared keys to store. + async fn insert_shared_keys( + &self, + client_address: DestinationAddressBytes, + shared_keys: &SharedKeys, + ) -> Result; + + /// Tries to retrieve shared keys stored for the particular client. + /// + /// # Arguments + /// + /// * `client_address`: address of the client + async fn get_shared_keys( + &self, + client_address: DestinationAddressBytes, + ) -> Result, StorageError>; + + /// Removes from the database shared keys derived with the particular client. + /// + /// # Arguments + /// + /// * `client_address`: address of the client + // currently there is no code flow that causes removal (not overwriting) + // of the stored keys. However, retain the function for consistency and completion sake + #[allow(dead_code)] + async fn remove_shared_keys( + &self, + client_address: DestinationAddressBytes, + ) -> Result<(), StorageError>; + + /// Inserts new message to the storage for an offline client for future retrieval. + /// + /// # Arguments + /// + /// * `client_address`: address of the client + /// * `message`: raw message to store. + async fn store_message( + &self, + client_address: DestinationAddressBytes, + message: Vec, + ) -> Result<(), StorageError>; + + /// Retrieves messages stored for the particular client specified by the provided address. + /// + /// # Arguments + /// + /// * `client_address`: address of the client + /// * `start_after`: optional starting id of the messages to grab + /// + /// returns the retrieved messages alongside optional id of the last message retrieved if + /// there are more messages to retrieve. + async fn retrieve_messages( + &self, + client_address: DestinationAddressBytes, + start_after: Option, + ) -> Result<(Vec, Option), StorageError>; + + /// Removes messages with the specified ids + /// + /// # Arguments + /// + /// * `ids`: ids of the messages to remove + async fn remove_messages(&self, ids: Vec) -> Result<(), StorageError>; + + /// Creates a new bandwidth entry for the particular client. + async fn create_bandwidth_entry(&self, client_id: i64) -> Result<(), StorageError>; + + /// Set the freepass expiration date of the particular client to the provided date. + /// + /// # Arguments + /// + /// * `client_address`: address of the client + /// * `expiration`: the expiration date of the associated free pass. + async fn set_expiration( + &self, + client_id: i64, + expiration: OffsetDateTime, + ) -> Result<(), StorageError>; + + /// Reset all the bandwidth + /// + /// # Arguments + /// + /// * `client_address`: address of the client + async fn reset_bandwidth(&self, client_id: i64) -> Result<(), StorageError>; + + /// Tries to retrieve available bandwidth for the particular client. + async fn get_available_bandwidth( + &self, + client_id: i64, + ) -> Result, StorageError>; + + /// Increases specified client's bandwidth by the provided amount and returns the current value. + async fn increase_bandwidth(&self, client_id: i64, amount: i64) -> Result; + + async fn revoke_ticket_bandwidth( + &self, + ticket_id: i64, + amount: i64, + ) -> Result<(), StorageError>; + + #[allow(dead_code)] + /// Decreases specified client's bandwidth by the provided amount and returns the current value. + async fn decrease_bandwidth(&self, client_id: i64, amount: i64) -> Result; + + async fn insert_epoch_signers( + &self, + epoch_id: i64, + signer_ids: Vec, + ) -> Result<(), StorageError>; + + async fn insert_received_ticket( + &self, + client_id: i64, + received_at: OffsetDateTime, + serial_number: Vec, + data: Vec, + ) -> Result; + + // note: this only checks very recent tickets that haven't yet been redeemed + // (but it's better than nothing) + /// Check if the ticket with the provided serial number if already present in the storage. + /// + /// # Arguments + /// + /// * `serial_number`: the unique serial number embedded in the ticket + async fn contains_ticket(&self, serial_number: &[u8]) -> Result; + + async fn insert_ticket_verification( + &self, + ticket_id: i64, + signer_id: i64, + verified_at: OffsetDateTime, + accepted: bool, + ) -> Result<(), StorageError>; + + async fn update_rejected_ticket(&self, ticket_id: i64) -> Result<(), StorageError>; + + async fn update_verified_ticket(&self, ticket_id: i64) -> Result<(), StorageError>; + + async fn remove_verified_ticket_binary_data(&self, ticket_id: i64) -> Result<(), StorageError>; + + async fn get_all_verified_tickets_with_sn(&self) -> Result, StorageError>; + async fn get_all_proposed_tickets_with_sn( + &self, + proposal_id: u32, + ) -> Result, StorageError>; + + async fn insert_redemption_proposal( + &self, + tickets: &[VerifiedTicket], + proposal_id: u32, + created_at: OffsetDateTime, + ) -> Result<(), StorageError>; + + async fn clear_post_proposal_data( + &self, + proposal_id: u32, + resolved_at: OffsetDateTime, + rejected: bool, + ) -> Result<(), StorageError>; + + async fn latest_proposal(&self) -> Result, StorageError>; + + async fn get_all_unverified_tickets(&self) -> Result, StorageError>; + async fn get_all_unresolved_proposals(&self) -> Result, StorageError>; + async fn get_votes(&self, ticket_id: i64) -> Result, StorageError>; + + async fn get_signers(&self, epoch_id: i64) -> Result, StorageError>; + + /// Insert a wireguard peer in the storage. + /// + /// # Arguments + /// + /// * `peer`: wireguard peer data to be stored + /// * `suspended`: if peer exists, but it's currently suspended + async fn insert_wireguard_peer( + &self, + peer: &defguard_wireguard_rs::host::Peer, + suspended: bool, + ) -> Result<(), StorageError>; + + /// Tries to retrieve available bandwidth for the particular peer. + /// + /// # Arguments + /// + /// * `peer_public_key`: wireguard public key of the peer to be retrieved. + async fn get_wireguard_peer( + &self, + peer_public_key: &str, + ) -> Result, StorageError>; + + /// Retrieves all wireguard peers. + async fn get_all_wireguard_peers(&self) -> Result, StorageError>; + + /// Remove a wireguard peer from the storage. + /// + /// # Arguments + /// + /// * `peer_public_key`: wireguard public key of the peer to be removed. + async fn remove_wireguard_peer(&self, peer_public_key: &str) -> Result<(), StorageError>; +} + +// note that clone here is fine as upon cloning the same underlying pool will be used +#[derive(Clone)] +pub struct PersistentStorage { + shared_key_manager: SharedKeysManager, + inbox_manager: InboxManager, + bandwidth_manager: BandwidthManager, + ticket_manager: TicketStorageManager, + wireguard_peer_manager: wireguard_peers::WgPeerManager, +} + +impl PersistentStorage { + /// Initialises `PersistentStorage` using the provided path. + /// + /// # Arguments + /// + /// * `database_path`: path to the database. + /// * `message_retrieval_limit`: maximum number of stored client messages that can be retrieved at once. + pub async fn init + Send>( + database_path: P, + message_retrieval_limit: i64, + ) -> Result { + debug!( + "Attempting to connect to database {:?}", + database_path.as_ref().as_os_str() + ); + + // TODO: we can inject here more stuff based on our gateway global config + // struct. Maybe different pool size or timeout intervals? + let mut opts = sqlx::sqlite::SqliteConnectOptions::new() + .filename(database_path) + .create_if_missing(true); + + // TODO: do we want auto_vacuum ? + + opts.disable_statement_logging(); + + let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { + Ok(db) => db, + Err(err) => { + error!("Failed to connect to SQLx database: {err}"); + return Err(err.into()); + } + }; + + if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await { + error!("Failed to perform migration on the SQLx database: {err}"); + return Err(err.into()); + } + + // the cloning here are cheap as connection pool is stored behind an Arc + Ok(PersistentStorage { + wireguard_peer_manager: wireguard_peers::WgPeerManager::new(connection_pool.clone()), + shared_key_manager: SharedKeysManager::new(connection_pool.clone()), + inbox_manager: InboxManager::new(connection_pool.clone(), message_retrieval_limit), + bandwidth_manager: BandwidthManager::new(connection_pool.clone()), + ticket_manager: TicketStorageManager::new(connection_pool), + }) + } +} + +#[async_trait] +impl Storage for PersistentStorage { + async fn get_client_id( + &self, + client_address: DestinationAddressBytes, + ) -> Result { + Ok(self + .shared_key_manager + .client_id(&client_address.as_base58_string()) + .await?) + } + + async fn insert_shared_keys( + &self, + client_address: DestinationAddressBytes, + shared_keys: &SharedKeys, + ) -> Result { + let client_id = self + .shared_key_manager + .insert_shared_keys( + client_address.as_base58_string(), + shared_keys.to_base58_string(), + ) + .await?; + Ok(client_id) + } + + async fn get_shared_keys( + &self, + client_address: DestinationAddressBytes, + ) -> Result, StorageError> { + let keys = self + .shared_key_manager + .get_shared_keys(&client_address.as_base58_string()) + .await?; + Ok(keys) + } + + #[allow(dead_code)] + async fn remove_shared_keys( + &self, + client_address: DestinationAddressBytes, + ) -> Result<(), StorageError> { + self.shared_key_manager + .remove_shared_keys(&client_address.as_base58_string()) + .await?; + Ok(()) + } + + async fn store_message( + &self, + client_address: DestinationAddressBytes, + message: Vec, + ) -> Result<(), StorageError> { + self.inbox_manager + .insert_message(&client_address.as_base58_string(), message) + .await?; + Ok(()) + } + + async fn retrieve_messages( + &self, + client_address: DestinationAddressBytes, + start_after: Option, + ) -> Result<(Vec, Option), StorageError> { + let messages = self + .inbox_manager + .get_messages(&client_address.as_base58_string(), start_after) + .await?; + Ok(messages) + } + + async fn remove_messages(&self, ids: Vec) -> Result<(), StorageError> { + for id in ids { + self.inbox_manager.remove_message(id).await?; + } + Ok(()) + } + + async fn create_bandwidth_entry(&self, client_id: i64) -> Result<(), StorageError> { + self.bandwidth_manager.insert_new_client(client_id).await?; + Ok(()) + } + + async fn set_expiration( + &self, + client_id: i64, + expiration: OffsetDateTime, + ) -> Result<(), StorageError> { + self.bandwidth_manager + .set_expiration(client_id, expiration) + .await?; + Ok(()) + } + + async fn reset_bandwidth(&self, client_id: i64) -> Result<(), StorageError> { + self.bandwidth_manager.reset_bandwidth(client_id).await?; + Ok(()) + } + + async fn get_available_bandwidth( + &self, + client_id: i64, + ) -> Result, StorageError> { + Ok(self + .bandwidth_manager + .get_available_bandwidth(client_id) + .await?) + } + + async fn increase_bandwidth(&self, client_id: i64, amount: i64) -> Result { + Ok(self + .bandwidth_manager + .increase_bandwidth(client_id, amount) + .await?) + } + + async fn revoke_ticket_bandwidth( + &self, + ticket_id: i64, + amount: i64, + ) -> Result<(), StorageError> { + Ok(self + .bandwidth_manager + .revoke_ticket_bandwidth(ticket_id, amount) + .await?) + } + + async fn decrease_bandwidth(&self, client_id: i64, amount: i64) -> Result { + Ok(self + .bandwidth_manager + .decrease_bandwidth(client_id, amount) + .await?) + } + + async fn insert_epoch_signers( + &self, + epoch_id: i64, + signer_ids: Vec, + ) -> Result<(), StorageError> { + self.ticket_manager + .insert_ecash_signers(epoch_id, signer_ids) + .await?; + Ok(()) + } + + async fn insert_received_ticket( + &self, + client_id: i64, + received_at: OffsetDateTime, + serial_number: Vec, + data: Vec, + ) -> Result { + // technically if we crash between those 2 calls we'll have a bit of data inconsistency, + // but nothing too tragic. we just won't get paid for a single ticket + let ticket_id = self + .ticket_manager + .insert_new_ticket(client_id, received_at) + .await?; + self.ticket_manager + .insert_ticket_data(ticket_id, &serial_number, &data) + .await?; + + Ok(ticket_id) + } + + async fn contains_ticket(&self, serial_number: &[u8]) -> Result { + Ok(self.ticket_manager.has_ticket_data(serial_number).await?) + } + + async fn insert_ticket_verification( + &self, + ticket_id: i64, + signer_id: i64, + verified_at: OffsetDateTime, + accepted: bool, + ) -> Result<(), StorageError> { + self.ticket_manager + .insert_ticket_verification(ticket_id, signer_id, verified_at, accepted) + .await?; + Ok(()) + } + + async fn update_rejected_ticket(&self, ticket_id: i64) -> Result<(), StorageError> { + // set the ticket as rejected + self.ticket_manager.set_rejected_ticket(ticket_id).await?; + + // drop all ticket_data - we no longer need it + // TODO: or maybe we do as a proof of receiving bad data? + self.ticket_manager.remove_ticket_data(ticket_id).await?; + + Ok(()) + } + + async fn update_verified_ticket(&self, ticket_id: i64) -> Result<(), StorageError> { + // 1. insert into verified table + self.ticket_manager + .insert_verified_ticket(ticket_id) + .await?; + + // TODO: maybe we want to leave that be until ticket gets fully redeemed instead? + // 2. remove individual verifications + self.ticket_manager + .remove_ticket_verification(ticket_id) + .await?; + Ok(()) + } + + async fn remove_verified_ticket_binary_data(&self, ticket_id: i64) -> Result<(), StorageError> { + self.ticket_manager + .remove_binary_ticket_data(ticket_id) + .await?; + Ok(()) + } + + async fn get_all_verified_tickets_with_sn(&self) -> Result, StorageError> { + Ok(self + .ticket_manager + .get_all_verified_tickets_with_sn() + .await?) + } + + async fn get_all_proposed_tickets_with_sn( + &self, + proposal_id: u32, + ) -> Result, StorageError> { + Ok(self + .ticket_manager + .get_all_proposed_tickets_with_sn(proposal_id as i64) + .await?) + } + + async fn insert_redemption_proposal( + &self, + tickets: &[VerifiedTicket], + proposal_id: u32, + created_at: OffsetDateTime, + ) -> Result<(), StorageError> { + // if we crash between those, there might a bit of an issue. we should revisit it later + + // 1. insert the actual proposal + self.ticket_manager + .insert_redemption_proposal(proposal_id as i64, created_at) + .await?; + + // 2. update all the associated tickets + self.ticket_manager + .insert_verified_tickets_proposal_id( + tickets.iter().map(|t| t.ticket_id), + proposal_id as i64, + ) + .await?; + Ok(()) + } + + async fn clear_post_proposal_data( + &self, + proposal_id: u32, + resolved_at: OffsetDateTime, + rejected: bool, + ) -> Result<(), StorageError> { + // 1. update proposal metadata + self.ticket_manager + .update_redemption_proposal(proposal_id as i64, resolved_at, rejected) + .await?; + + // 2. remove ticket data rows (we can drop serial numbers) + self.ticket_manager + .remove_redeemed_tickets_data(proposal_id as i64) + .await?; + + // 3. remove verified tickets rows + self.ticket_manager + .remove_verified_tickets(proposal_id as i64) + .await?; + + Ok(()) + } + + async fn latest_proposal(&self) -> Result, StorageError> { + Ok(self.ticket_manager.get_latest_redemption_proposal().await?) + } + + async fn get_all_unverified_tickets(&self) -> Result, StorageError> { + self.ticket_manager + .get_unverified_tickets() + .await? + .into_iter() + .map(TryInto::try_into) + .collect() + } + + async fn get_all_unresolved_proposals(&self) -> Result, StorageError> { + Ok(self + .ticket_manager + .get_all_unresolved_redemption_proposal_ids() + .await?) + } + + async fn get_votes(&self, ticket_id: i64) -> Result, StorageError> { + Ok(self + .ticket_manager + .get_verification_votes(ticket_id) + .await?) + } + + async fn get_signers(&self, epoch_id: i64) -> Result, StorageError> { + Ok(self.ticket_manager.get_epoch_signers(epoch_id).await?) + } + + async fn insert_wireguard_peer( + &self, + peer: &defguard_wireguard_rs::host::Peer, + suspended: bool, + ) -> Result<(), StorageError> { + let mut peer = WireguardPeer::from(peer.clone()); + peer.suspended = suspended; + self.wireguard_peer_manager.insert_peer(&peer).await?; + Ok(()) + } + + async fn get_wireguard_peer( + &self, + peer_public_key: &str, + ) -> Result, StorageError> { + let peer = self + .wireguard_peer_manager + .retrieve_peer(peer_public_key) + .await?; + Ok(peer) + } + + async fn get_all_wireguard_peers(&self) -> Result, StorageError> { + let ret = self.wireguard_peer_manager.retrieve_all_peers().await?; + Ok(ret) + } + + async fn remove_wireguard_peer(&self, peer_public_key: &str) -> Result<(), StorageError> { + self.wireguard_peer_manager + .remove_peer(peer_public_key) + .await?; + Ok(()) + } +} diff --git a/common/gateway-storage/src/models.rs b/common/gateway-storage/src/models.rs new file mode 100644 index 0000000000..dc0229d1f3 --- /dev/null +++ b/common/gateway-storage/src/models.rs @@ -0,0 +1,179 @@ +// Copyright 2021-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::StorageError; +use nym_credentials_interface::{AvailableBandwidth, ClientTicket, CredentialSpendingData}; +use sqlx::FromRow; +use time::OffsetDateTime; + +pub struct PersistedSharedKeys { + #[allow(dead_code)] + pub id: i64, + + #[allow(dead_code)] + pub client_address_bs58: String, + pub derived_aes128_ctr_blake3_hmac_keys_bs58: String, +} + +pub struct StoredMessage { + pub id: i64, + #[allow(dead_code)] + pub client_address_bs58: String, + pub content: Vec, +} + +#[derive(Debug, Clone, FromRow)] +pub struct PersistedBandwidth { + #[allow(dead_code)] + pub client_id: i64, + pub available: i64, + pub expiration: Option, +} + +impl From for AvailableBandwidth { + fn from(value: PersistedBandwidth) -> Self { + AvailableBandwidth { + bytes: value.available, + expiration: value.expiration.unwrap_or(OffsetDateTime::UNIX_EPOCH), + } + } +} + +#[derive(FromRow)] +pub struct VerifiedTicket { + pub serial_number: Vec, + pub ticket_id: i64, +} + +#[derive(FromRow)] +pub struct RedemptionProposal { + pub proposal_id: i64, + pub created_at: OffsetDateTime, +} + +#[derive(FromRow)] +pub struct UnverifiedTicketData { + pub data: Vec, + pub ticket_id: i64, +} + +impl TryFrom for ClientTicket { + type Error = StorageError; + + fn try_from(value: UnverifiedTicketData) -> Result { + Ok(ClientTicket { + spending_data: CredentialSpendingData::try_from_bytes(&value.data).map_err(|_| { + StorageError::MalformedStoredTicketData { + ticket_id: value.ticket_id, + } + })?, + ticket_id: value.ticket_id, + }) + } +} + +#[derive(Debug, Clone, FromRow)] +pub struct WireguardPeer { + pub public_key: String, + pub preshared_key: Option, + pub protocol_version: Option, + pub endpoint: Option, + pub last_handshake: Option, + pub tx_bytes: i64, + pub rx_bytes: i64, + pub persistent_keepalive_interval: Option, + pub allowed_ips: Vec, + pub suspended: bool, +} + +impl From for WireguardPeer { + fn from(value: defguard_wireguard_rs::host::Peer) -> Self { + WireguardPeer { + public_key: value.public_key.to_string(), + preshared_key: value.preshared_key.as_ref().map(|k| k.to_string()), + protocol_version: value.protocol_version.map(|v| v as i64), + endpoint: value.endpoint.map(|e| e.to_string()), + last_handshake: value.last_handshake.and_then(|t| { + if let Ok(d) = t.duration_since(std::time::UNIX_EPOCH) { + if let Ok(millis) = d.as_millis().try_into() { + sqlx::types::chrono::DateTime::from_timestamp_millis(millis) + .map(|d| d.naive_utc()) + } else { + None + } + } else { + None + } + }), + tx_bytes: value.tx_bytes as i64, + rx_bytes: value.rx_bytes as i64, + persistent_keepalive_interval: value.persistent_keepalive_interval.map(|v| v as i64), + allowed_ips: bincode::Options::serialize( + bincode::DefaultOptions::new(), + &value.allowed_ips, + ) + .unwrap_or_default(), + suspended: false, + } + } +} + +impl TryFrom for defguard_wireguard_rs::host::Peer { + type Error = crate::error::StorageError; + + fn try_from(value: WireguardPeer) -> Result { + Ok(Self { + public_key: value + .public_key + .as_str() + .try_into() + .map_err(|e| Self::Error::TypeConversion(format!("public key {e}")))?, + preshared_key: value + .preshared_key + .as_deref() + .map(TryFrom::try_from) + .transpose() + .map_err(|e| Self::Error::TypeConversion(format!("preshared key {e}")))?, + protocol_version: value + .protocol_version + .map(TryFrom::try_from) + .transpose() + .map_err(|e| Self::Error::TypeConversion(format!("protocol version {e}")))?, + endpoint: value + .endpoint + .as_deref() + .map(|e| e.parse()) + .transpose() + .map_err(|e| Self::Error::TypeConversion(format!("endpoint {e}")))?, + last_handshake: value.last_handshake.and_then(|t| { + let unix_time = std::time::UNIX_EPOCH; + if let Ok(millis) = t.and_utc().timestamp_millis().try_into() { + let duration = std::time::Duration::from_millis(millis); + unix_time.checked_add(duration) + } else { + None + } + }), + tx_bytes: value + .tx_bytes + .try_into() + .map_err(|e| Self::Error::TypeConversion(format!("tx bytes {e}")))?, + rx_bytes: value + .rx_bytes + .try_into() + .map_err(|e| Self::Error::TypeConversion(format!("rx bytes {e}")))?, + persistent_keepalive_interval: value + .persistent_keepalive_interval + .map(TryFrom::try_from) + .transpose() + .map_err(|e| { + Self::Error::TypeConversion(format!("persistent keepalive interval {e}")) + })?, + allowed_ips: bincode::Options::deserialize( + bincode::DefaultOptions::new(), + &value.allowed_ips, + ) + .map_err(|e| Self::Error::TypeConversion(format!("allowed ips {e}")))?, + }) + } +} diff --git a/gateway/src/node/storage/shared_keys.rs b/common/gateway-storage/src/shared_keys.rs similarity index 64% rename from gateway/src/node/storage/shared_keys.rs rename to common/gateway-storage/src/shared_keys.rs index d04ab12492..97171eab85 100644 --- a/gateway/src/node/storage/shared_keys.rs +++ b/common/gateway-storage/src/shared_keys.rs @@ -1,7 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node::storage::models::PersistedSharedKeys; +use crate::models::PersistedSharedKeys; #[derive(Clone)] pub(crate) struct SharedKeysManager { @@ -18,6 +18,17 @@ impl SharedKeysManager { SharedKeysManager { connection_pool } } + pub(crate) async fn client_id(&self, client_address_bs58: &str) -> Result { + let client_id = sqlx::query!( + "SELECT id FROM shared_keys WHERE client_address_bs58 = ?", + client_address_bs58 + ) + .fetch_one(&self.connection_pool) + .await? + .id; + Ok(client_id) + } + /// Inserts provided derived shared keys into the database. /// If keys previously existed for the provided client, they are overwritten with the new data. /// @@ -26,13 +37,23 @@ impl SharedKeysManager { /// * `shared_keys`: shared encryption (AES128CTR) and mac (hmac-blake3) derived shared keys to store. pub(crate) async fn insert_shared_keys( &self, - shared_keys: PersistedSharedKeys, - ) -> Result<(), sqlx::Error> { - sqlx::query!("INSERT OR REPLACE INTO shared_keys(client_address_bs58, derived_aes128_ctr_blake3_hmac_keys_bs58) VALUES (?, ?)", - shared_keys.client_address_bs58, - shared_keys.derived_aes128_ctr_blake3_hmac_keys_bs58, + client_address_bs58: String, + derived_aes128_ctr_blake3_hmac_keys_bs58: String, + ) -> Result { + // https://stackoverflow.com/a/20310838 + // we don't want to be using `INSERT OR REPLACE INTO` due to the foreign key on `available_bandwidth` if the entry already exists + sqlx::query!( + r#" + INSERT OR IGNORE INTO shared_keys(client_address_bs58, derived_aes128_ctr_blake3_hmac_keys_bs58) VALUES (?, ?); + UPDATE shared_keys SET derived_aes128_ctr_blake3_hmac_keys_bs58 = ? WHERE client_address_bs58 = ? + "#, + client_address_bs58, + derived_aes128_ctr_blake3_hmac_keys_bs58, + derived_aes128_ctr_blake3_hmac_keys_bs58, + client_address_bs58, ).execute(&self.connection_pool).await?; - Ok(()) + + self.client_id(&client_address_bs58).await } /// Tries to retrieve shared keys stored for the particular client. diff --git a/common/gateway-storage/src/tickets.rs b/common/gateway-storage/src/tickets.rs new file mode 100644 index 0000000000..ce68d44514 --- /dev/null +++ b/common/gateway-storage/src/tickets.rs @@ -0,0 +1,358 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::models::{RedemptionProposal, UnverifiedTicketData, VerifiedTicket}; +use time::OffsetDateTime; + +#[derive(Clone)] +pub(crate) struct TicketStorageManager { + connection_pool: sqlx::SqlitePool, +} + +impl TicketStorageManager { + /// Creates new instance of the `CredentialManager` with the provided sqlite connection pool. + /// + /// # Arguments + /// + /// * `connection_pool`: database connection pool to use. + pub(crate) fn new(connection_pool: sqlx::SqlitePool) -> Self { + TicketStorageManager { connection_pool } + } + + pub(crate) async fn insert_ecash_signers( + &self, + epoch_id: i64, + signer_ids: Vec, + ) -> Result<(), sqlx::Error> { + let mut query_builder = + sqlx::QueryBuilder::new("INSERT INTO ecash_signer (epoch_id, signer_id) "); + + query_builder.push_values(signer_ids, |mut b, signer_id| { + b.push_bind(epoch_id).push_bind(signer_id); + }); + + query_builder.build().execute(&self.connection_pool).await?; + Ok(()) + } + + pub(crate) async fn insert_new_ticket( + &self, + client_id: i64, + received_at: OffsetDateTime, + ) -> Result { + Ok(sqlx::query!( + "INSERT INTO received_ticket (client_id, received_at) VALUES (?, ?)", + client_id, + received_at + ) + .execute(&self.connection_pool) + .await? + .last_insert_rowid()) + } + + pub(crate) async fn set_rejected_ticket(&self, ticket_id: i64) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE received_ticket SET rejected = true WHERE id = ?", + ticket_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn insert_ticket_data( + &self, + ticket_id: i64, + serial_number: &[u8], + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO ticket_data(ticket_id, serial_number, data) VALUES (?, ?, ?)", + ticket_id, + serial_number, + data + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn remove_ticket_data(&self, ticket_id: i64) -> Result<(), sqlx::Error> { + sqlx::query!("DELETE FROM ticket_data WHERE ticket_id = ?", ticket_id) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn has_ticket_data(&self, serial_number: &[u8]) -> Result { + sqlx::query!( + "SELECT EXISTS (SELECT 1 FROM ticket_data WHERE serial_number = ?) AS 'exists'", + serial_number + ) + .fetch_one(&self.connection_pool) + .await + .map(|result| result.exists == 1) + } + + pub(crate) async fn remove_binary_ticket_data( + &self, + ticket_id: i64, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE ticket_data SET data = NULL WHERE ticket_id = ?", + ticket_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn remove_redeemed_tickets_data( + &self, + proposal_id: i64, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + DELETE FROM ticket_data + WHERE ticket_id IN ( + SELECT ticket_id + FROM verified_tickets + WHERE proposal_id = ? + ) + "#, + proposal_id + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn insert_ticket_verification( + &self, + ticket_id: i64, + signer_id: i64, + verified_at: OffsetDateTime, + accepted: bool, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO ticket_verification (ticket_id, signer_id, verified_at, accepted) + VALUES (?, ?, ?, ?) + "#, + ticket_id, + signer_id, + verified_at, + accepted + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn remove_ticket_verification( + &self, + ticket_id: i64, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "DELETE FROM ticket_verification WHERE ticket_id = ?", + ticket_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn insert_verified_ticket(&self, ticket_id: i64) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO verified_tickets (ticket_id) VALUES (?)", + ticket_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + /// Attempts to retrieve ticket data for all tickets that that are **NOT** present in `verified_tickets` table + pub(crate) async fn get_unverified_tickets( + &self, + ) -> Result, sqlx::Error> { + // force not nullable `data` field as we explicitly ensured this via the query + sqlx::query_as!( + UnverifiedTicketData, + r#" + SELECT t1.ticket_id, t1.data as "data!" + FROM ticket_data as t1 + LEFT JOIN verified_tickets as t2 + ON t1.ticket_id = t2.ticket_id + WHERE + t2.ticket_id IS NULL + AND + t1.data IS NOT NULL + "# + ) + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn get_epoch_signers(&self, epoch_id: i64) -> Result, sqlx::Error> { + sqlx::query!( + "SELECT signer_id FROM ecash_signer WHERE epoch_id = ?", + epoch_id + ) + .fetch_all(&self.connection_pool) + .await + .map(|records| records.into_iter().map(|r| r.signer_id).collect()) + } + + pub(crate) async fn get_verification_votes( + &self, + ticket_id: i64, + ) -> Result, sqlx::Error> { + sqlx::query!( + "SELECT signer_id FROM ticket_verification WHERE ticket_id = ?", + ticket_id + ) + .fetch_all(&self.connection_pool) + .await + .map(|records| records.into_iter().map(|r| r.signer_id).collect()) + } + + pub(crate) async fn get_all_verified_tickets_with_sn( + &self, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + VerifiedTicket, + r#" + SELECT t1.ticket_id, t2.serial_number + FROM verified_tickets as t1 + JOIN ticket_data as t2 + ON t1.ticket_id = t2.ticket_id + JOIN received_ticket as t3 + ON t1.ticket_id = t3.id + + ORDER BY t3.received_at ASC + LIMIT 65535 + "# + ) + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn get_all_proposed_tickets_with_sn( + &self, + proposal_id: i64, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + VerifiedTicket, + r#" + SELECT t1.ticket_id, t2.serial_number + FROM verified_tickets as t1 + JOIN ticket_data as t2 + ON t1.ticket_id = t2.ticket_id + WHERE t1.proposal_id = ? + "#, + proposal_id + ) + .fetch_all(&self.connection_pool) + .await + } + + /// for each ticket in `verified_tickets` where the `ticket_id` is present in the provided iterator, + /// set the associated `proposal_id` to the provided value. + pub(crate) async fn insert_verified_tickets_proposal_id( + &self, + ticket_ids: I, + proposal_id: i64, + ) -> Result<(), sqlx::Error> + where + I: Iterator, + { + // UPDATE verified_tickets SET proposal_id = ... WHERE ticket_id IN (1,2,3,...) + let mut query_builder = + sqlx::QueryBuilder::new("UPDATE verified_tickets SET proposal_id = "); + query_builder + .push_bind(proposal_id) + .push("WHERE ticket_id IN ("); + + let mut separated = query_builder.separated(", "); + for ticket_id in ticket_ids { + separated.push_bind(ticket_id); + } + separated.push_unseparated(") "); + + query_builder.build().execute(&self.connection_pool).await?; + Ok(()) + } + + pub(crate) async fn remove_verified_tickets( + &self, + proposal_id: i64, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "DELETE FROM verified_tickets WHERE proposal_id = ?", + proposal_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn insert_redemption_proposal( + &self, + proposal_id: i64, + created_at: OffsetDateTime, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO redemption_proposals (proposal_id, created_at) VALUES (?, ?)", + proposal_id, + created_at + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn update_redemption_proposal( + &self, + proposal_id: i64, + resolved_at: OffsetDateTime, + rejected: bool, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE redemption_proposals SET resolved_at = ?, rejected = ? WHERE proposal_id = ?", + resolved_at, + rejected, + proposal_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_latest_redemption_proposal( + &self, + ) -> Result, sqlx::Error> { + sqlx::query_as( + r#" + SELECT proposal_id, created_at + FROM redemption_proposals + ORDER BY created_at DESC + LIMIT 1 + "#, + ) + .fetch_optional(&self.connection_pool) + .await + } + + pub(crate) async fn get_all_unresolved_redemption_proposal_ids( + &self, + ) -> Result, sqlx::Error> { + sqlx::query!("SELECT proposal_id FROM redemption_proposals WHERE resolved_at IS NULL") + .fetch_all(&self.connection_pool) + .await + .map(|records| records.into_iter().map(|r| r.proposal_id).collect()) + } +} diff --git a/common/gateway-storage/src/wireguard_peers.rs b/common/gateway-storage/src/wireguard_peers.rs new file mode 100644 index 0000000000..c90eb9647d --- /dev/null +++ b/common/gateway-storage/src/wireguard_peers.rs @@ -0,0 +1,98 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::models::WireguardPeer; + +#[derive(Clone)] +pub(crate) struct WgPeerManager { + connection_pool: sqlx::SqlitePool, +} + +impl WgPeerManager { + /// Creates new instance of the `WgPeersManager` with the provided sqlite connection pool. + /// + /// # Arguments + /// + /// * `connection_pool`: database connection pool to use. + pub(crate) fn new(connection_pool: sqlx::SqlitePool) -> Self { + WgPeerManager { connection_pool } + } + + /// Creates a new wireguard peer entry for its particular public key or + /// overwrittes the peer entry data if it already existed. + /// + /// # Arguments + /// + /// * `peer`: peer information needed by wireguard interface. + pub(crate) async fn insert_peer(&self, peer: &WireguardPeer) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT OR IGNORE INTO wireguard_peer(public_key, preshared_key, protocol_version, endpoint, last_handshake, tx_bytes, rx_bytes, persistent_keepalive_interval, allowed_ips, suspended) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + + UPDATE wireguard_peer + SET preshared_key = ?, protocol_version = ?, endpoint = ?, last_handshake = ?, tx_bytes = ?, rx_bytes = ?, persistent_keepalive_interval = ?, allowed_ips = ?, suspended = ? + WHERE public_key = ? + "#, + peer.public_key, peer.preshared_key, peer.protocol_version, peer.endpoint, peer.last_handshake, peer.tx_bytes, peer.rx_bytes, peer.persistent_keepalive_interval, peer.allowed_ips, peer.suspended, + + peer.preshared_key, peer.protocol_version, peer.endpoint, peer.last_handshake, peer.tx_bytes, peer.rx_bytes, peer.persistent_keepalive_interval, peer.allowed_ips, peer.suspended,peer.public_key, + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + /// Retrieve the wireguard peer with the provided public key from the storage. + /// + /// # Arguments + /// + /// * `public_key`: the unique public key of the wireguard peer. + pub(crate) async fn retrieve_peer( + &self, + public_key: &str, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + WireguardPeer, + r#" + SELECT * FROM wireguard_peer + WHERE public_key = ? + LIMIT 1 + "#, + public_key, + ) + .fetch_optional(&self.connection_pool) + .await + } + + /// Retrieve all wireguard peers. + pub(crate) async fn retrieve_all_peers(&self) -> Result, sqlx::Error> { + sqlx::query_as!( + WireguardPeer, + r#" + SELECT * + FROM wireguard_peer; + "#, + ) + .fetch_all(&self.connection_pool) + .await + } + + /// Retrieve the wireguard peer with the provided public key from the storage. + /// + /// # Arguments + /// + /// * `public_key`: the unique public key of the wireguard peer. + pub(crate) async fn remove_peer(&self, public_key: &str) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + DELETE FROM wireguard_peer + WHERE public_key = ? + "#, + public_key, + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } +} diff --git a/common/ip-packet-requests/src/v7/request.rs b/common/ip-packet-requests/src/v7/request.rs index 24f9819c2a..73266320e3 100644 --- a/common/ip-packet-requests/src/v7/request.rs +++ b/common/ip-packet-requests/src/v7/request.rs @@ -415,6 +415,8 @@ pub struct HealthRequest { #[cfg(test)] mod tests { + use time::macros::datetime; + use super::*; use std::net::{Ipv4Addr, Ipv6Addr}; use std::str::FromStr; @@ -432,7 +434,7 @@ mod tests { reply_to_hops: None, reply_to_avg_mix_delays: None, buffer_timeout: None, - timestamp: OffsetDateTime::now_utc(), + timestamp: datetime!(2024-01-01 12:59:59.5 UTC), }, signature: None, } diff --git a/common/ledger/Cargo.toml b/common/ledger/Cargo.toml index 993c37980f..72de9bb003 100644 --- a/common/ledger/Cargo.toml +++ b/common/ledger/Cargo.toml @@ -7,7 +7,7 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bip32 = { workspace = true } +bip32 = { workspace = true, features = ["secp256k1", "std"] } k256 = { workspace = true } ledger-transport = { workspace = true } ledger-transport-hid = { workspace = true } diff --git a/common/mobile-storage/Cargo.toml b/common/mobile-storage/Cargo.toml deleted file mode 100644 index 4b02821498..0000000000 --- a/common/mobile-storage/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "nym-mobile-storage" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -async-trait = { workspace = true } -thiserror = { workspace = true } - diff --git a/common/network-defaults/Cargo.toml b/common/network-defaults/Cargo.toml index 4de02982ec..03d09cf159 100644 --- a/common/network-defaults/Cargo.toml +++ b/common/network-defaults/Cargo.toml @@ -8,12 +8,16 @@ license.workspace = true repository.workspace = true [dependencies] -cfg-if = { workspace = true } -dotenvy = { workspace = true } -hex-literal = { workspace = true } -log = { workspace = true } -once_cell = { workspace = true } -schemars = { workspace = true, features = ["preserve_order"] } -serde = { workspace = true, features = ["derive"]} -thiserror = { workspace = true } -url = { workspace = true } +dotenvy = { workspace = true, optional = true } +log = { workspace = true, optional = true } +schemars = { workspace = true, features = ["preserve_order"], optional = true } +serde = { workspace = true, features = ["derive"], optional = true } +url = { workspace = true, optional = true } + +# please be extremely careful when adding new dependencies because this crate is imported by the ecash contract, +# so if anything new is added, consider feature-locking it and then just adding it to default feature + +[features] +default = ["env", "network"] +env = ["dotenvy", "log"] +network = ["schemars", "serde", "url"] \ No newline at end of file diff --git a/common/network-defaults/src/constants.rs b/common/network-defaults/src/constants.rs new file mode 100644 index 0000000000..91bb5c7931 --- /dev/null +++ b/common/network-defaults/src/constants.rs @@ -0,0 +1,57 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// re-export to not break existing imports +pub use deprecated::*; +pub use nyx::*; +pub use wireguard::*; + +// all of those should be obtained via nym-node, et al. crate instead +pub mod deprecated { + pub const DEFAULT_MIX_LISTENING_PORT: u16 = 1789; + + // 'GATEWAY' + pub const DEFAULT_CLIENT_LISTENING_PORT: u16 = 9000; + + // 'MIXNODE' + pub const DEFAULT_VERLOC_LISTENING_PORT: u16 = 1790; + pub const DEFAULT_HTTP_API_LISTENING_PORT: u16 = 8000; + + // 'CLIENT' + pub const DEFAULT_WEBSOCKET_LISTENING_PORT: u16 = 1977; + + // 'SOCKS5' CLIENT + pub const DEFAULT_SOCKS5_LISTENING_PORT: u16 = 1080; + + // NYM-API + pub const DEFAULT_NYM_API_PORT: u16 = 8080; + + pub const NYM_API_VERSION: &str = "v1"; + + // NYM-NODE + pub const DEFAULT_NYM_NODE_HTTP_PORT: u16 = 8080; +} + +pub mod nyx { + /// Defaults Cosmos Hub/ATOM path + pub const COSMOS_DERIVATION_PATH: &str = "m/44'/118'/0'/0/0"; + + // as set by validators in their configs + // (note that the 'amount' postfix is relevant here as the full gas price also includes denom) + pub const GAS_PRICE_AMOUNT: f64 = 0.025; + + // TODO: is there a way to get this from the chain + pub const TOTAL_SUPPLY: u128 = 1_000_000_000_000_000; +} + +pub mod wireguard { + use std::net::{IpAddr, Ipv4Addr}; + + pub const WG_PORT: u16 = 51822; + + // The interface used to route traffic + pub const WG_TUN_BASE_NAME: &str = "nymwg"; + pub const WG_TUN_DEVICE_ADDRESS: &str = "10.1.0.1"; + pub const WG_TUN_DEVICE_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 1)); + pub const WG_TUN_DEVICE_NETMASK: &str = "255.255.255.0"; +} diff --git a/common/network-defaults/src/ecash.rs b/common/network-defaults/src/ecash.rs new file mode 100644 index 0000000000..21d1ddfe85 --- /dev/null +++ b/common/network-defaults/src/ecash.rs @@ -0,0 +1,68 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +/// Specifies the maximum validity of the issued ticketbooks. +pub const TICKETBOOK_VALIDITY_DAYS: u32 = 7; + +/// Specifies the number of tickets in each issued ticketbook. +pub const TICKETBOOK_SIZE: u64 = 50; + +/// This type is defined mostly for the purposes of having constants (like sizes) associated with given variants +/// It's not meant to be serialised or have any fancy traits defined on it (in this crate) +#[derive(Default, Copy, Clone, Debug, PartialEq)] +#[repr(u8)] +pub enum TicketTypeRepr { + #[default] + V1MixnetEntry = 0, + V1MixnetExit = 1, + V1WireguardEntry = 2, + V1WireguardExit = 3, +} + +impl TicketTypeRepr { + pub const WIREGUARD_ENTRY_TICKET_SIZE: u64 = 500 * 1024 * 1024; // 500 MB + + // TBD: + pub const WIREGUARD_EXIT_TICKET_SIZE: u64 = Self::WIREGUARD_ENTRY_TICKET_SIZE; + pub const MIXNET_ENTRY_TICKET_SIZE: u64 = Self::WIREGUARD_ENTRY_TICKET_SIZE; + pub const MIXNET_EXIT_TICKET_SIZE: u64 = Self::WIREGUARD_ENTRY_TICKET_SIZE; + + /// How much bandwidth (in bytes) one ticket can grant + pub const fn bandwidth_value(&self) -> u64 { + match self { + TicketTypeRepr::V1MixnetEntry => Self::MIXNET_ENTRY_TICKET_SIZE, + TicketTypeRepr::V1MixnetExit => Self::MIXNET_EXIT_TICKET_SIZE, + TicketTypeRepr::V1WireguardEntry => Self::WIREGUARD_ENTRY_TICKET_SIZE, + TicketTypeRepr::V1WireguardExit => Self::WIREGUARD_EXIT_TICKET_SIZE, + } + } +} + +// Constants for bloom filter for double spending detection +//Chosen for FP of +//Calculator at https://hur.st/bloomfilter/ +pub const ECASH_DS_BLOOMFILTER_PARAMS: BloomfilterParameters = BloomfilterParameters { + num_hashes: 10, + bitmap_size: 1_500_000_000, + sip_keys: [ + (12345678910111213141, 1415926535897932384), + (7182818284590452353, 3571113171923293137), + ], +}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct BloomfilterParameters { + pub num_hashes: u32, + pub bitmap_size: u64, + pub sip_keys: [(u64, u64); 2], +} + +impl BloomfilterParameters { + pub const fn byte_size(&self) -> u64 { + self.bitmap_size / 8 + } + + pub const fn default_ecash() -> Self { + ECASH_DS_BLOOMFILTER_PARAMS + } +} diff --git a/common/network-defaults/src/env_setup.rs b/common/network-defaults/src/env_setup.rs new file mode 100644 index 0000000000..4ab07259a7 --- /dev/null +++ b/common/network-defaults/src/env_setup.rs @@ -0,0 +1,67 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::var_names; +use crate::var_names::{DEPRECATED_API_VALIDATOR, DEPRECATED_NYMD_VALIDATOR, NYM_API, NYXD}; +use std::path::Path; + +fn fix_deprecated_environmental_variables() { + // if we're using the outdated environmental variables, set the updated ones to preserve compatibility + if let Ok(nyxd) = std::env::var(DEPRECATED_NYMD_VALIDATOR) { + if std::env::var(NYXD).is_err() { + std::env::set_var(NYXD, nyxd) + } + } + if let Ok(nym_apis) = std::env::var(DEPRECATED_API_VALIDATOR) { + if std::env::var(NYM_API).is_err() { + std::env::set_var(NYM_API, nym_apis) + } + } +} + +// Read the variables from the file and log what the corresponding values in the environment are. +fn print_env_vars_with_keys_in_file + Copy>(config_env_file: P) { + let items = dotenvy::from_path_iter(config_env_file) + .expect("Invalid path to environment configuration file"); + for item in items { + let (key, val) = item.expect("Invalid item in environment configuration file"); + log::debug!("{}: {}", key, val); + } +} + +pub fn setup_env>(config_env_file: Option

) { + match std::env::var(var_names::CONFIGURED) { + // if the configuration is not already set in the env vars + Err(std::env::VarError::NotPresent) => { + if let Some(config_env_file) = &config_env_file { + log::debug!( + "Loading environment variables from {:?}", + config_env_file.as_ref() + ); + dotenvy::from_path(config_env_file) + .expect("Invalid path to environment configuration file"); + fix_deprecated_environmental_variables(); + } else { + // if nothing is set, the use mainnet defaults + // if the user has not set `CONFIGURED`, then even if they set any of the env variables, + // overwrite them + log::debug!("Loading mainnet defaults"); + crate::mainnet::export_to_env(); + } + } + Err(_) => { + log::debug!("Environment variables already set. Using them"); + crate::mainnet::export_to_env() + } + _ => { + fix_deprecated_environmental_variables(); + } + } + + // if we haven't explicitly defined any of the constants, fallback to defaults + crate::mainnet::export_to_env_if_not_set(); + + if let Some(config_env_file) = &config_env_file { + print_env_vars_with_keys_in_file(config_env_file); + } +} diff --git a/common/network-defaults/src/lib.rs b/common/network-defaults/src/lib.rs index 33a416e4ae..d02b0bdf15 100644 --- a/common/network-defaults/src/lib.rs +++ b/common/network-defaults/src/lib.rs @@ -1,485 +1,28 @@ // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::var_names::{DEPRECATED_API_VALIDATOR, DEPRECATED_NYMD_VALIDATOR, NYM_API, NYXD}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::net::{IpAddr, Ipv4Addr}; -use std::path::Path; -use std::{ - env::{var, VarError}, - ffi::OsStr, - ops::Not, -}; -use url::Url; +pub mod constants; +pub mod ecash; +// if you haven't read the Cargo.toml file, the reason for all the feature-locking is that +// this crate is imported into the ecash contract +// +// so if you're thinking of adding a new thing, consider feature-locking it and then just adding it to default feature + +#[cfg(all(feature = "env", feature = "network"))] +pub mod env_setup; pub mod mainnet; +#[cfg(feature = "network")] +pub mod network; + +#[cfg(feature = "env")] pub mod var_names; -#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)] -pub struct ChainDetails { - pub bech32_account_prefix: String, - pub mix_denom: DenomDetailsOwned, - pub stake_denom: DenomDetailsOwned, -} +pub use ecash::*; -#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)] -pub struct NymContracts { - pub mixnet_contract_address: Option, - pub vesting_contract_address: Option, - pub coconut_bandwidth_contract_address: Option, - pub group_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 -// with `NetworkDetails` defined in all.rs... -#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)] -pub struct NymNetworkDetails { - pub network_name: String, - pub chain_details: ChainDetails, - pub endpoints: Vec, - pub contracts: NymContracts, - pub explorer_api: Option, -} - -// by default we assume the same defaults as mainnet, i.e. same prefixes and denoms -impl Default for NymNetworkDetails { - fn default() -> Self { - NymNetworkDetails::new_mainnet() - } -} - -impl NymNetworkDetails { - pub fn new_empty() -> Self { - NymNetworkDetails { - network_name: Default::default(), - chain_details: ChainDetails { - bech32_account_prefix: Default::default(), - mix_denom: DenomDetailsOwned { - base: Default::default(), - display: Default::default(), - display_exponent: Default::default(), - }, - stake_denom: DenomDetailsOwned { - base: Default::default(), - display: Default::default(), - display_exponent: Default::default(), - }, - }, - endpoints: Default::default(), - contracts: Default::default(), - explorer_api: Default::default(), - } - } - - pub fn new_from_env() -> Self { - fn get_optional_env>(env: K) -> Option { - match var(env) { - Ok(var) => { - if var.is_empty() { - None - } else { - Some(var) - } - } - Err(VarError::NotPresent) => None, - err => panic!("Unable to set: {:?}", err), - } - } - - NymNetworkDetails::new_empty() - .with_network_name(var(var_names::NETWORK_NAME).expect("network name not set")) - .with_bech32_account_prefix( - var(var_names::BECH32_PREFIX).expect("bech32 prefix not set"), - ) - .with_mix_denom(DenomDetailsOwned { - base: var(var_names::MIX_DENOM).expect("mix denomination base not set"), - display: var(var_names::MIX_DENOM_DISPLAY) - .expect("mix denomination display not set"), - display_exponent: var(var_names::DENOMS_EXPONENT) - .expect("denomination exponent not set") - .parse() - .expect("denomination exponent is not u32"), - }) - .with_stake_denom(DenomDetailsOwned { - base: var(var_names::STAKE_DENOM).expect("stake denomination base not set"), - display: var(var_names::STAKE_DENOM_DISPLAY) - .expect("stake denomination display not set"), - display_exponent: var(var_names::DENOMS_EXPONENT) - .expect("denomination exponent not set") - .parse() - .expect("denomination exponent is not u32"), - }) - .with_additional_validator_endpoint(ValidatorDetails::new( - var(var_names::NYXD).expect("nyxd validator not set"), - Some(var(var_names::NYM_API).expect("nym api not set")), - get_optional_env(var_names::NYXD_WEBSOCKET), - )) - .with_mixnet_contract(get_optional_env(var_names::MIXNET_CONTRACT_ADDRESS)) - .with_vesting_contract(get_optional_env(var_names::VESTING_CONTRACT_ADDRESS)) - .with_coconut_bandwidth_contract(get_optional_env( - var_names::COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - )) - .with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS)) - .with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS)) - .with_coconut_dkg_contract(get_optional_env(var_names::COCONUT_DKG_CONTRACT_ADDRESS)) - .with_explorer_api(get_optional_env(var_names::EXPLORER_API)) - } - - pub fn new_mainnet() -> Self { - fn parse_optional_str(raw: &str) -> Option { - raw.is_empty().not().then(|| raw.into()) - } - - // Consider caching this process (lazy static) - NymNetworkDetails { - network_name: mainnet::NETWORK_NAME.into(), - chain_details: ChainDetails { - bech32_account_prefix: mainnet::BECH32_PREFIX.into(), - mix_denom: mainnet::MIX_DENOM.into(), - stake_denom: mainnet::STAKE_DENOM.into(), - }, - endpoints: mainnet::validators(), - contracts: NymContracts { - mixnet_contract_address: parse_optional_str(mainnet::MIXNET_CONTRACT_ADDRESS), - vesting_contract_address: parse_optional_str(mainnet::VESTING_CONTRACT_ADDRESS), - coconut_bandwidth_contract_address: parse_optional_str( - mainnet::COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - ), - group_contract_address: parse_optional_str(mainnet::GROUP_CONTRACT_ADDRESS), - multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS), - coconut_dkg_contract_address: parse_optional_str( - mainnet::COCONUT_DKG_CONTRACT_ADDRESS, - ), - }, - explorer_api: parse_optional_str(mainnet::EXPLORER_API), - } - } - - pub fn default_gas_price_amount(&self) -> f64 { - GAS_PRICE_AMOUNT - } - - #[must_use] - pub fn with_network_name(mut self, network_name: String) -> Self { - self.network_name = network_name; - self - } - - #[must_use] - pub fn with_chain_details(mut self, chain_details: ChainDetails) -> Self { - self.chain_details = chain_details; - self - } - - #[must_use] - pub fn with_bech32_account_prefix>(mut self, prefix: S) -> Self { - self.chain_details.bech32_account_prefix = prefix.into(); - self - } - - #[must_use] - pub fn with_mix_denom(mut self, mix_denom: DenomDetailsOwned) -> Self { - self.chain_details.mix_denom = mix_denom; - self - } - - #[must_use] - pub fn with_stake_denom(mut self, stake_denom: DenomDetailsOwned) -> Self { - self.chain_details.stake_denom = stake_denom; - self - } - - #[must_use] - pub fn with_base_mix_denom>(mut self, base_mix_denom: S) -> Self { - self.chain_details.mix_denom = DenomDetailsOwned::base_only(base_mix_denom.into()); - self - } - - #[must_use] - pub fn with_base_stake_denom>(mut self, base_stake_denom: S) -> Self { - self.chain_details.stake_denom = DenomDetailsOwned::base_only(base_stake_denom.into()); - self - } - - #[must_use] - pub fn with_additional_validator_endpoint(mut self, endpoint: ValidatorDetails) -> Self { - self.endpoints.push(endpoint); - self - } - - #[must_use] - pub fn with_validator_endpoint(mut self, endpoint: ValidatorDetails) -> Self { - self.endpoints = vec![endpoint]; - self - } - - #[must_use] - pub fn with_contracts(mut self, contracts: NymContracts) -> Self { - self.contracts = contracts; - self - } - - #[must_use] - pub fn with_mixnet_contract>(mut self, contract: Option) -> Self { - self.contracts.mixnet_contract_address = contract.map(Into::into); - self - } - - #[must_use] - pub fn with_vesting_contract>(mut self, contract: Option) -> Self { - self.contracts.vesting_contract_address = contract.map(Into::into); - self - } - - #[must_use] - pub fn with_coconut_bandwidth_contract>(mut self, contract: Option) -> Self { - self.contracts.coconut_bandwidth_contract_address = contract.map(Into::into); - self - } - - #[must_use] - pub fn with_group_contract>(mut self, contract: Option) -> Self { - self.contracts.group_contract_address = contract.map(Into::into); - self - } - - #[must_use] - pub fn with_multisig_contract>(mut self, contract: Option) -> Self { - 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 - } - - #[must_use] - pub fn with_explorer_api>(mut self, endpoint: Option) -> Self { - self.explorer_api = endpoint.map(Into::into); - self - } -} - -#[derive(Debug, Copy, Serialize, Deserialize, Clone, PartialEq, Eq)] -pub struct DenomDetails { - pub base: &'static str, - pub display: &'static str, - // i.e. display_amount * 10^display_exponent = base_amount - pub display_exponent: u32, -} - -impl DenomDetails { - pub const fn new(base: &'static str, display: &'static str, display_exponent: u32) -> Self { - DenomDetails { - base, - display, - display_exponent, - } - } -} - -#[derive(Debug, Serialize, Deserialize, Hash, Clone, PartialEq, Eq, JsonSchema)] -pub struct DenomDetailsOwned { - pub base: String, - pub display: String, - // i.e. display_amount * 10^display_exponent = base_amount - pub display_exponent: u32, -} - -impl From for DenomDetailsOwned { - fn from(details: DenomDetails) -> Self { - DenomDetailsOwned { - base: details.base.to_owned(), - display: details.display.to_owned(), - display_exponent: details.display_exponent, - } - } -} - -impl DenomDetailsOwned { - pub fn base_only(base: String) -> Self { - DenomDetailsOwned { - base: base.clone(), - display: base, - display_exponent: 0, - } - } -} - -#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)] -pub struct ValidatorDetails { - // it is assumed those values are always valid since they're being provided in our defaults file - pub nyxd_url: String, - // - pub websocket_url: Option, - - // Right now api_url is optional as we are not running the api reliably on all validators - // however, later on it should be a mandatory field - pub api_url: Option, - // TODO: I'd argue this one should also have a field like `gas_price` since its a validator-specific setting -} - -impl ValidatorDetails { - pub fn new>(nyxd_url: S, api_url: Option, websocket_url: Option) -> Self { - ValidatorDetails { - nyxd_url: nyxd_url.into(), - websocket_url: websocket_url.map(Into::into), - api_url: api_url.map(Into::into), - } - } - - pub fn new_nyxd_only>(nyxd_url: S) -> Self { - ValidatorDetails { - nyxd_url: nyxd_url.into(), - websocket_url: None, - api_url: None, - } - } - - pub fn nyxd_url(&self) -> Url { - self.nyxd_url - .parse() - .expect("the provided nyxd url is invalid!") - } - - pub fn api_url(&self) -> Option { - self.api_url - .as_ref() - .map(|url| url.parse().expect("the provided api url is invalid!")) - } - - pub fn websocket_url(&self) -> Option { - self.websocket_url - .as_ref() - .map(|url| url.parse().expect("the provided websocket url is invalid!")) - } -} - -fn fix_deprecated_environmental_variables() { - // if we're using the outdated environmental variables, set the updated ones to preserve compatibility - if let Ok(nyxd) = std::env::var(DEPRECATED_NYMD_VALIDATOR) { - if std::env::var(NYXD).is_err() { - std::env::set_var(NYXD, nyxd) - } - } - if let Ok(nym_apis) = std::env::var(DEPRECATED_API_VALIDATOR) { - if std::env::var(NYM_API).is_err() { - std::env::set_var(NYM_API, nym_apis) - } - } -} - -// Read the variables from the file and log what the corresponding values in the environment are. -fn print_env_vars_with_keys_in_file + Copy>(config_env_file: P) { - let items = dotenvy::from_path_iter(config_env_file) - .expect("Invalid path to environment configuration file"); - for item in items { - let (key, val) = item.expect("Invalid item in environment configuration file"); - log::debug!("{}: {}", key, val); - } -} - -pub fn setup_env>(config_env_file: Option

) { - match std::env::var(var_names::CONFIGURED) { - // if the configuration is not already set in the env vars - Err(std::env::VarError::NotPresent) => { - if let Some(config_env_file) = &config_env_file { - log::debug!( - "Loading environment variables from {:?}", - config_env_file.as_ref() - ); - dotenvy::from_path(config_env_file) - .expect("Invalid path to environment configuration file"); - fix_deprecated_environmental_variables(); - } else { - // if nothing is set, the use mainnet defaults - // if the user has not set `CONFIGURED`, then even if they set any of the env variables, - // overwrite them - log::debug!("Loading mainnet defaults"); - crate::mainnet::export_to_env(); - } - } - Err(_) => { - log::debug!("Environment variables already set. Using them"); - crate::mainnet::export_to_env() - } - _ => { - fix_deprecated_environmental_variables(); - } - } - - // if we haven't explicitly defined any of the constants, fallback to defaults - crate::mainnet::export_to_env_if_not_set(); - - if let Some(config_env_file) = &config_env_file { - print_env_vars_with_keys_in_file(config_env_file); - } -} - -/// How much bandwidth (in bytes) one token can buy -pub const BYTES_PER_UTOKEN: u64 = 1024; -/// How much bandwidth (in bytes) one freepass provides -pub const BYTES_PER_FREEPASS: u64 = 1024 * 1024 * 1024; // 1GB -/// Threshold for claiming more bandwidth: 1 MB -pub const REMAINING_BANDWIDTH_THRESHOLD: i64 = 1024 * 1024; -/// How many tokens should be burned to buy bandwidth -pub const TOKENS_TO_BURN: u64 = 1; -/// How many ERC20 utokens should be burned to buy bandwidth -pub const UTOKENS_TO_BURN: u64 = TOKENS_TO_BURN * 1000000; -/// Default bandwidth (in bytes) that we try to buy -pub const BANDWIDTH_VALUE: u64 = UTOKENS_TO_BURN * BYTES_PER_UTOKEN; - -/// Defaults Cosmos Hub/ATOM path -pub const COSMOS_DERIVATION_PATH: &str = "m/44'/118'/0'/0/0"; -// as set by validators in their configs -// (note that the 'amount' postfix is relevant here as the full gas price also includes denom) -pub const GAS_PRICE_AMOUNT: f64 = 0.025; - -pub const DEFAULT_MIX_LISTENING_PORT: u16 = 1789; - -// 'GATEWAY' -pub const DEFAULT_CLIENT_LISTENING_PORT: u16 = 9000; - -// 'MIXNODE' -pub const DEFAULT_VERLOC_LISTENING_PORT: u16 = 1790; -pub const DEFAULT_HTTP_API_LISTENING_PORT: u16 = 8000; - -// 'CLIENT' -pub const DEFAULT_WEBSOCKET_LISTENING_PORT: u16 = 1977; - -// 'SOCKS5' CLIENT -pub const DEFAULT_SOCKS5_LISTENING_PORT: u16 = 1080; - -// NYM-API -pub const DEFAULT_NYM_API_PORT: u16 = 8080; - -pub const NYM_API_VERSION: &str = "v1"; - -// NYM-NODE -pub const DEFAULT_NYM_NODE_HTTP_PORT: u16 = 8080; - -// REWARDING - -/// We'll be assuming a few more things, profit margin and cost function. Since we don't have reliable package measurement, we'll be using uptime. We'll also set the value of 1 Nym to 1 $, to be able to translate interval costs to Nyms. We'll also assume a cost of 40$ per interval(month), converting that to Nym at our 1$ rate translates to 40_000_000 uNyms -// pub const DEFAULT_OPERATOR_INTERVAL_COST: u64 = 40_000_000; // 40$/(30 days) at 1 Nym == 1$ -// pub const DEFAULT_OPERATOR_INTERVAL_COST: u64 = 55_556; // 40$/1hr at 1 Nym == 1$ -// pub const DEFAULT_OPERATOR_INTERVAL_COST: u64 = 9259; // 40$/1hr/6 at 1 Nym == 1$ - -// TODO: is there a way to get this from the chain -pub const TOTAL_SUPPLY: u128 = 1_000_000_000_000_000; - -pub const DEFAULT_PROFIT_MARGIN: u8 = 10; - -// WIREGUARD -pub const WG_PORT: u16 = 51822; - -// The interface used to route traffic -pub const WG_TUN_BASE_NAME: &str = "nymwg"; -pub const WG_TUN_DEVICE_ADDRESS: &str = "10.1.0.1"; -pub const WG_TUN_DEVICE_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 1)); -pub const WG_TUN_DEVICE_NETMASK: &str = "255.255.255.0"; +// re-export everything to not break existing imports +pub use constants::*; +#[cfg(all(feature = "env", feature = "network"))] +pub use env_setup::*; +#[cfg(feature = "network")] +pub use network::*; diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index a9200d0239..8ebe2e044e 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -1,23 +1,24 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::var_names; +#[cfg(feature = "network")] use crate::{DenomDetails, ValidatorDetails}; -use std::str::FromStr; pub const NETWORK_NAME: &str = "mainnet"; pub const BECH32_PREFIX: &str = "n"; +#[cfg(feature = "network")] pub const MIX_DENOM: DenomDetails = DenomDetails::new("unym", "nym", 6); +#[cfg(feature = "network")] pub const STAKE_DENOM: DenomDetails = DenomDetails::new("unyx", "nyx", 6); pub const MIXNET_CONTRACT_ADDRESS: &str = "n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr"; pub const VESTING_CONTRACT_ADDRESS: &str = "n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw"; - -pub const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = ""; +pub const ECASH_CONTRACT_ADDRESS: &str = + "n1r7s6aksyc6pqardx88k3rkgfagwvj4z4zum9mmz2sfk3zm2mha0sd4dnun"; pub const GROUP_CONTRACT_ADDRESS: &str = "n1e2zq4886zzewpvpucmlw8v9p7zv692f6yck4zjzxh699dkcmlrfqk2knsr"; pub const MULTISIG_CONTRACT_ADDRESS: &str = @@ -31,11 +32,13 @@ pub const NYXD_URL: &str = "https://rpc.nymtech.net"; pub const NYM_API: &str = "https://validator.nymtech.net/api/"; pub const NYXD_WS: &str = "wss://rpc.nymtech.net/websocket"; pub const EXPLORER_API: &str = "https://explorer.nymtech.net/api/"; +pub const NYM_VPN_API: &str = "https://nymvpn.net/api/"; // I'm making clippy mad on purpose, because that url HAS TO be updated and deployed before merging pub const EXIT_POLICY_URL: &str = "https://nymtech.net/.wellknown/network-requester/exit-policy.txt"; +#[cfg(feature = "network")] pub(crate) fn validators() -> Vec { vec![ValidatorDetails::new( NYXD_URL, @@ -44,23 +47,28 @@ pub(crate) fn validators() -> Vec { )] } +#[cfg(feature = "env")] const DEFAULT_SUFFIX: &str = "_MAINNET_DEFAULT"; +#[cfg(all(feature = "env", feature = "network"))] fn set_var_to_default(var: &str, value: &str) { std::env::set_var(var, value); std::env::set_var(format!("{var}{DEFAULT_SUFFIX}"), "1") } +#[cfg(all(feature = "env", feature = "network"))] fn set_var_conditionally_to_default(var: &str, value: &str) { if std::env::var(var).is_err() { set_var_to_default(var, value) } } +#[cfg(feature = "env")] pub fn uses_default(var: &str) -> bool { std::env::var(format!("{var}{DEFAULT_SUFFIX}")).is_ok() } +#[cfg(feature = "env")] pub fn read_var_if_not_default(var: &str) -> Option { if uses_default(var) { None @@ -69,13 +77,19 @@ pub fn read_var_if_not_default(var: &str) -> Option { } } -pub fn read_parsed_var_if_not_default(var: &str) -> Option> { +#[cfg(feature = "env")] +pub fn read_parsed_var_if_not_default( + var: &str, +) -> Option> { read_var_if_not_default(var) .as_deref() - .map(FromStr::from_str) + .map(std::str::FromStr::from_str) } +#[cfg(all(feature = "env", feature = "network"))] pub fn export_to_env() { + use crate::var_names; + set_var_to_default(var_names::CONFIGURED, "true"); set_var_to_default(var_names::NETWORK_NAME, NETWORK_NAME); set_var_to_default(var_names::BECH32_PREFIX, BECH32_PREFIX); @@ -92,10 +106,7 @@ pub fn export_to_env() { var_names::VESTING_CONTRACT_ADDRESS, VESTING_CONTRACT_ADDRESS, ); - set_var_to_default( - var_names::COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - ); + set_var_to_default(var_names::ECASH_CONTRACT_ADDRESS, ECASH_CONTRACT_ADDRESS); set_var_to_default(var_names::GROUP_CONTRACT_ADDRESS, GROUP_CONTRACT_ADDRESS); set_var_to_default( var_names::MULTISIG_CONTRACT_ADDRESS, @@ -114,9 +125,13 @@ pub fn export_to_env() { set_var_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS); set_var_to_default(var_names::EXPLORER_API, EXPLORER_API); set_var_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL); + set_var_to_default(var_names::NYM_VPN_API, NYM_VPN_API); } +#[cfg(all(feature = "env", feature = "network"))] pub fn export_to_env_if_not_set() { + use crate::var_names; + set_var_conditionally_to_default(var_names::CONFIGURED, "true"); set_var_conditionally_to_default(var_names::NETWORK_NAME, NETWORK_NAME); set_var_conditionally_to_default(var_names::BECH32_PREFIX, BECH32_PREFIX); @@ -133,10 +148,7 @@ pub fn export_to_env_if_not_set() { var_names::VESTING_CONTRACT_ADDRESS, VESTING_CONTRACT_ADDRESS, ); - set_var_conditionally_to_default( - var_names::COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - ); + set_var_conditionally_to_default(var_names::ECASH_CONTRACT_ADDRESS, ECASH_CONTRACT_ADDRESS); set_var_conditionally_to_default(var_names::GROUP_CONTRACT_ADDRESS, GROUP_CONTRACT_ADDRESS); set_var_conditionally_to_default( var_names::MULTISIG_CONTRACT_ADDRESS, diff --git a/common/network-defaults/src/network.rs b/common/network-defaults/src/network.rs new file mode 100644 index 0000000000..9dbb5d9bc2 --- /dev/null +++ b/common/network-defaults/src/network.rs @@ -0,0 +1,370 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{mainnet, GAS_PRICE_AMOUNT}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::ops::Not; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)] +pub struct ChainDetails { + pub bech32_account_prefix: String, + pub mix_denom: DenomDetailsOwned, + pub stake_denom: DenomDetailsOwned, +} + +#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)] +pub struct NymContracts { + pub mixnet_contract_address: Option, + pub vesting_contract_address: Option, + pub ecash_contract_address: Option, + pub group_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 +// with `NetworkDetails` defined in all.rs... +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)] +pub struct NymNetworkDetails { + pub network_name: String, + pub chain_details: ChainDetails, + pub endpoints: Vec, + pub contracts: NymContracts, + pub explorer_api: Option, + pub nym_vpn_api_url: Option, +} + +// by default we assume the same defaults as mainnet, i.e. same prefixes and denoms +impl Default for NymNetworkDetails { + fn default() -> Self { + NymNetworkDetails::new_mainnet() + } +} + +impl NymNetworkDetails { + pub fn new_empty() -> Self { + NymNetworkDetails { + network_name: Default::default(), + chain_details: ChainDetails { + bech32_account_prefix: Default::default(), + mix_denom: DenomDetailsOwned { + base: Default::default(), + display: Default::default(), + display_exponent: Default::default(), + }, + stake_denom: DenomDetailsOwned { + base: Default::default(), + display: Default::default(), + display_exponent: Default::default(), + }, + }, + endpoints: Default::default(), + contracts: Default::default(), + explorer_api: Default::default(), + nym_vpn_api_url: Default::default(), + } + } + + #[cfg(feature = "env")] + pub fn new_from_env() -> Self { + use crate::var_names; + use std::env::{var, VarError}; + use std::ffi::OsStr; + + fn get_optional_env>(env: K) -> Option { + match var(env) { + Ok(var) => { + if var.is_empty() { + None + } else { + Some(var) + } + } + Err(VarError::NotPresent) => None, + err => panic!("Unable to set: {:?}", err), + } + } + + NymNetworkDetails::new_empty() + .with_network_name(var(var_names::NETWORK_NAME).expect("network name not set")) + .with_bech32_account_prefix( + var(var_names::BECH32_PREFIX).expect("bech32 prefix not set"), + ) + .with_mix_denom(DenomDetailsOwned { + base: var(var_names::MIX_DENOM).expect("mix denomination base not set"), + display: var(var_names::MIX_DENOM_DISPLAY) + .expect("mix denomination display not set"), + display_exponent: var(var_names::DENOMS_EXPONENT) + .expect("denomination exponent not set") + .parse() + .expect("denomination exponent is not u32"), + }) + .with_stake_denom(DenomDetailsOwned { + base: var(var_names::STAKE_DENOM).expect("stake denomination base not set"), + display: var(var_names::STAKE_DENOM_DISPLAY) + .expect("stake denomination display not set"), + display_exponent: var(var_names::DENOMS_EXPONENT) + .expect("denomination exponent not set") + .parse() + .expect("denomination exponent is not u32"), + }) + .with_additional_validator_endpoint(ValidatorDetails::new( + var(var_names::NYXD).expect("nyxd validator not set"), + Some(var(var_names::NYM_API).expect("nym api not set")), + get_optional_env(var_names::NYXD_WEBSOCKET), + )) + .with_mixnet_contract(get_optional_env(var_names::MIXNET_CONTRACT_ADDRESS)) + .with_vesting_contract(get_optional_env(var_names::VESTING_CONTRACT_ADDRESS)) + .with_ecash_contract(get_optional_env(var_names::ECASH_CONTRACT_ADDRESS)) + .with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS)) + .with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS)) + .with_coconut_dkg_contract(get_optional_env(var_names::COCONUT_DKG_CONTRACT_ADDRESS)) + .with_explorer_api(get_optional_env(var_names::EXPLORER_API)) + .with_nym_vpn_api_url(get_optional_env(var_names::NYM_VPN_API)) + } + + pub fn new_mainnet() -> Self { + fn parse_optional_str(raw: &str) -> Option { + raw.is_empty().not().then(|| raw.into()) + } + + // Consider caching this process (lazy static) + NymNetworkDetails { + network_name: mainnet::NETWORK_NAME.into(), + chain_details: ChainDetails { + bech32_account_prefix: mainnet::BECH32_PREFIX.into(), + mix_denom: mainnet::MIX_DENOM.into(), + stake_denom: mainnet::STAKE_DENOM.into(), + }, + endpoints: mainnet::validators(), + contracts: NymContracts { + mixnet_contract_address: parse_optional_str(mainnet::MIXNET_CONTRACT_ADDRESS), + vesting_contract_address: parse_optional_str(mainnet::VESTING_CONTRACT_ADDRESS), + ecash_contract_address: parse_optional_str(mainnet::ECASH_CONTRACT_ADDRESS), + group_contract_address: parse_optional_str(mainnet::GROUP_CONTRACT_ADDRESS), + multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS), + coconut_dkg_contract_address: parse_optional_str( + mainnet::COCONUT_DKG_CONTRACT_ADDRESS, + ), + }, + explorer_api: parse_optional_str(mainnet::EXPLORER_API), + nym_vpn_api_url: parse_optional_str(mainnet::NYM_VPN_API), + } + } + + pub fn default_gas_price_amount(&self) -> f64 { + GAS_PRICE_AMOUNT + } + + #[must_use] + pub fn with_network_name(mut self, network_name: String) -> Self { + self.network_name = network_name; + self + } + + #[must_use] + pub fn with_chain_details(mut self, chain_details: ChainDetails) -> Self { + self.chain_details = chain_details; + self + } + + #[must_use] + pub fn with_bech32_account_prefix>(mut self, prefix: S) -> Self { + self.chain_details.bech32_account_prefix = prefix.into(); + self + } + + #[must_use] + pub fn with_mix_denom(mut self, mix_denom: DenomDetailsOwned) -> Self { + self.chain_details.mix_denom = mix_denom; + self + } + + #[must_use] + pub fn with_stake_denom(mut self, stake_denom: DenomDetailsOwned) -> Self { + self.chain_details.stake_denom = stake_denom; + self + } + + #[must_use] + pub fn with_base_mix_denom>(mut self, base_mix_denom: S) -> Self { + self.chain_details.mix_denom = DenomDetailsOwned::base_only(base_mix_denom.into()); + self + } + + #[must_use] + pub fn with_base_stake_denom>(mut self, base_stake_denom: S) -> Self { + self.chain_details.stake_denom = DenomDetailsOwned::base_only(base_stake_denom.into()); + self + } + + #[must_use] + pub fn with_additional_validator_endpoint(mut self, endpoint: ValidatorDetails) -> Self { + self.endpoints.push(endpoint); + self + } + + #[must_use] + pub fn with_validator_endpoint(mut self, endpoint: ValidatorDetails) -> Self { + self.endpoints = vec![endpoint]; + self + } + + #[must_use] + pub fn with_contracts(mut self, contracts: NymContracts) -> Self { + self.contracts = contracts; + self + } + + #[must_use] + pub fn with_mixnet_contract>(mut self, contract: Option) -> Self { + self.contracts.mixnet_contract_address = contract.map(Into::into); + self + } + + #[must_use] + pub fn with_vesting_contract>(mut self, contract: Option) -> Self { + self.contracts.vesting_contract_address = contract.map(Into::into); + self + } + + #[must_use] + pub fn with_ecash_contract>(mut self, contract: Option) -> Self { + self.contracts.ecash_contract_address = contract.map(Into::into); + self + } + + #[must_use] + pub fn with_group_contract>(mut self, contract: Option) -> Self { + self.contracts.group_contract_address = contract.map(Into::into); + self + } + + #[must_use] + pub fn with_multisig_contract>(mut self, contract: Option) -> Self { + 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 + } + + #[must_use] + pub fn with_explorer_api>(mut self, endpoint: Option) -> Self { + self.explorer_api = endpoint.map(Into::into); + self + } + + #[must_use] + pub fn with_nym_vpn_api_url>(mut self, endpoint: Option) -> Self { + self.nym_vpn_api_url = endpoint.map(Into::into); + self + } + + pub fn nym_vpn_api_url(&self) -> Option { + self.nym_vpn_api_url.as_ref().map(|url| { + url.parse() + .expect("the provided nym-vpn api url is invalid!") + }) + } +} + +#[derive(Debug, Copy, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct DenomDetails { + pub base: &'static str, + pub display: &'static str, + // i.e. display_amount * 10^display_exponent = base_amount + pub display_exponent: u32, +} + +impl DenomDetails { + pub const fn new(base: &'static str, display: &'static str, display_exponent: u32) -> Self { + DenomDetails { + base, + display, + display_exponent, + } + } +} + +#[derive(Debug, Serialize, Deserialize, Hash, Clone, PartialEq, Eq, JsonSchema)] +pub struct DenomDetailsOwned { + pub base: String, + pub display: String, + // i.e. display_amount * 10^display_exponent = base_amount + pub display_exponent: u32, +} + +impl From for DenomDetailsOwned { + fn from(details: DenomDetails) -> Self { + DenomDetailsOwned { + base: details.base.to_owned(), + display: details.display.to_owned(), + display_exponent: details.display_exponent, + } + } +} + +impl DenomDetailsOwned { + pub fn base_only(base: String) -> Self { + DenomDetailsOwned { + base: base.clone(), + display: base, + display_exponent: 0, + } + } +} + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)] +pub struct ValidatorDetails { + // it is assumed those values are always valid since they're being provided in our defaults file + pub nyxd_url: String, + // + pub websocket_url: Option, + + // Right now api_url is optional as we are not running the api reliably on all validators + // however, later on it should be a mandatory field + pub api_url: Option, + // TODO: I'd argue this one should also have a field like `gas_price` since its a validator-specific setting +} + +impl ValidatorDetails { + pub fn new>(nyxd_url: S, api_url: Option, websocket_url: Option) -> Self { + ValidatorDetails { + nyxd_url: nyxd_url.into(), + websocket_url: websocket_url.map(Into::into), + api_url: api_url.map(Into::into), + } + } + + pub fn new_nyxd_only>(nyxd_url: S) -> Self { + ValidatorDetails { + nyxd_url: nyxd_url.into(), + websocket_url: None, + api_url: None, + } + } + + pub fn nyxd_url(&self) -> Url { + self.nyxd_url + .parse() + .expect("the provided nyxd url is invalid!") + } + + pub fn api_url(&self) -> Option { + self.api_url + .as_ref() + .map(|url| url.parse().expect("the provided api url is invalid!")) + } + + pub fn websocket_url(&self) -> Option { + self.websocket_url + .as_ref() + .map(|url| url.parse().expect("the provided websocket url is invalid!")) + } +} diff --git a/common/network-defaults/src/var_names.rs b/common/network-defaults/src/var_names.rs index 8a629ee314..4831e82122 100644 --- a/common/network-defaults/src/var_names.rs +++ b/common/network-defaults/src/var_names.rs @@ -14,7 +14,7 @@ pub const STAKE_DENOM_DISPLAY: &str = "STAKE_DENOM_DISPLAY"; pub const DENOMS_EXPONENT: &str = "DENOMS_EXPONENT"; pub const MIXNET_CONTRACT_ADDRESS: &str = "MIXNET_CONTRACT_ADDRESS"; pub const VESTING_CONTRACT_ADDRESS: &str = "VESTING_CONTRACT_ADDRESS"; -pub const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = "COCONUT_BANDWIDTH_CONTRACT_ADDRESS"; +pub const ECASH_CONTRACT_ADDRESS: &str = "ECASH_CONTRACT_ADDRESS"; pub const GROUP_CONTRACT_ADDRESS: &str = "GROUP_CONTRACT_ADDRESS"; pub const MULTISIG_CONTRACT_ADDRESS: &str = "MULTISIG_CONTRACT_ADDRESS"; pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = "COCONUT_DKG_CONTRACT_ADDRESS"; @@ -24,6 +24,7 @@ pub const NYM_API: &str = "NYM_API"; pub const NYXD_WEBSOCKET: &str = "NYXD_WS"; pub const EXPLORER_API: &str = "EXPLORER_API"; pub const EXIT_POLICY_URL: &str = "EXIT_POLICY"; +pub const NYM_VPN_API: &str = "NYM_VPN_API"; pub const DKG_TIME_CONFIGURATION: &str = "DKG_TIME_CONFIGURATION"; diff --git a/common/nym-id/src/error.rs b/common/nym-id/src/error.rs index 2edc03fd0e..780658944e 100644 --- a/common/nym-id/src/error.rs +++ b/common/nym-id/src/error.rs @@ -3,7 +3,7 @@ use std::error::Error; use thiserror::Error; -use time::OffsetDateTime; +use time::Date; #[derive(Debug, Error)] pub enum NymIdError { @@ -11,7 +11,7 @@ pub enum NymIdError { CredentialDeserializationFailure { source: nym_credentials::Error }, #[error("attempted to import an expired credential (it expired on {expiration})")] - ExpiredCredentialImport { expiration: OffsetDateTime }, + ExpiredCredentialImport { expiration: Date }, #[error("failed to store credential in the provided store: {source}")] StorageError { diff --git a/common/nym-id/src/import_credential.rs b/common/nym-id/src/import_credential.rs index 0d3a9abce4..867db15749 100644 --- a/common/nym-id/src/import_credential.rs +++ b/common/nym-id/src/import_credential.rs @@ -2,10 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 use crate::NymIdError; -use nym_credential_storage::models::StorableIssuedCredential; use nym_credential_storage::storage::Storage; -use nym_credentials::coconut::bandwidth::issued::BandwidthCredentialIssuedDataVariant; -use nym_credentials::IssuedBandwidthCredential; +use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise; +use nym_credentials::ecash::utils::EcashTime; +use nym_credentials::IssuedTicketBook; use time::OffsetDateTime; use tracing::{debug, warn}; use zeroize::Zeroizing; @@ -14,7 +14,7 @@ pub async fn import_credential( credentials_store: S, raw_credential: Vec, credential_version: impl Into>, -) -> Result, NymIdError> +) -> Result where S: Storage, ::StorageError: Send + Sync + 'static, @@ -22,54 +22,28 @@ where let raw_credential = Zeroizing::new(raw_credential); // note: the type itself implements ZeroizeOnDrop - let credential = IssuedBandwidthCredential::try_unpack(&raw_credential, credential_version) + let ticketbook = IssuedTicketBook::try_unpack(&raw_credential, credential_version) .map_err(|source| NymIdError::CredentialDeserializationFailure { source })?; debug!( - "attempting to import credential of type {}", - credential.typ() + "attempting to import credential with expiration date at {}", + ticketbook.expiration_date() ); - let expiry_date = match credential.variant_data() { - BandwidthCredentialIssuedDataVariant::Voucher(voucher_info) => { - debug!("with value of {}", voucher_info.value()); - None - } - BandwidthCredentialIssuedDataVariant::FreePass(freepass_info) => { - debug!("with expiry at {}", freepass_info.expiry_date()); - if freepass_info.expired() { - warn!("the free pass has already expired!"); + if ticketbook.expired() { + warn!("the credential has already expired!"); - // technically we can import it, but the gateway will just reject it so what's the point - return Err(NymIdError::ExpiredCredentialImport { - expiration: freepass_info.expiry_date(), - }); - } else { - Some(freepass_info.expiry_date()) - } - } - }; - - // SAFETY: - // for the epoch to run over u32::MAX, we'd have to advance it for few centuries every block... - // the alternative is a very particularly malformed serialized data, but at that point blowing up is the right call - // because we can't rely on it anyway - #[allow(clippy::expect_used)] - let storable = StorableIssuedCredential { - serialization_revision: credential.current_serialization_revision(), - credential_data: &raw_credential, - credential_type: credential.typ().to_string(), - epoch_id: credential - .epoch_id() - .try_into() - .expect("our epoch is has run over u32::MAX!"), - }; + // technically we can import it, but the gateway will just reject it so what's the point + return Err(NymIdError::ExpiredCredentialImport { + expiration: ticketbook.expiration_date(), + }); + } credentials_store - .insert_issued_credential(storable) + .insert_issued_ticketbook(&ticketbook) .await .map_err(|source| NymIdError::StorageError { source: Box::new(source), })?; - Ok(expiry_date) + Ok(ticketbook.expiration_date().ecash_datetime()) } diff --git a/common/nym_offline_compact_ecash/Cargo.toml b/common/nym_offline_compact_ecash/Cargo.toml new file mode 100644 index 0000000000..19bf70cf62 --- /dev/null +++ b/common/nym_offline_compact_ecash/Cargo.toml @@ -0,0 +1,65 @@ +# Copyright 2024 - Nym Technologies SA +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "nym-compact-ecash" +version = "0.1.0" +authors = ["Ania Piotrowska "] +edition = "2021" +license = { workspace = true } + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bls12_381 = { workspace = true, features = ["alloc", "pairings", "experimental", "zeroize", "experimental_serde"] } +bincode.workspace = true +cfg-if.workspace = true +itertools = "0.12.1" +digest = "0.9" +rand = { workspace = true } +thiserror = { workspace = true } +sha2 = "0.9" +bs58 = { workspace = true } +serde = { workspace = true, features = ["derive"] } +rayon = { version = "1.5.0", optional = true } +zeroize = { workspace = true, features = ["zeroize_derive"] } +ff = { workspace = true } +group = { workspace = true } + +nym-pemstore = { path = "../pemstore" } +nym-network-defaults = { path = "../network-defaults", default-features = false } + +[dev-dependencies] +criterion = { version = "0.5.1", features = ["html_reports"] } + + +[[bench]] +name = "benchmarks_group_operations" +path = "benches/benchmarks_group_operations.rs" +harness = false + +[[bench]] +name = "benchmarks_expiration_date_signatures" +path = "benches/benchmarks_expiration_date_signatures.rs" +harness = false + +[[bench]] +name = "benchmarks_coin_indices_signatures" +path = "benches/benchmarks_coin_indices_signatures.rs" +harness = false + +[[bench]] +name = "benchmarks_ecash_e2e" +path = "benches/benchmarks_ecash_e2e.rs" +harness = false + +[features] +# for 1000 coin indices it goes from ~50ms to ~400ms, but we only have to issue them once per epoch +# so it's not really worth it +par_signing = ["rayon"] + +# for this one there's an argument for it since the verification of 1000 indices can take over 6s, +# but given it's not done very frequently, it shouldn't be too much of a problem +# furthermore, we can't and shouldn't dedicate the entire nym-api CPU just for verification, +# but this feature might potentially be desirable for clients. +par_verify = ["rayon"] \ No newline at end of file diff --git a/common/nym_offline_compact_ecash/benches/benchmarks_coin_indices_signatures.rs b/common/nym_offline_compact_ecash/benches/benchmarks_coin_indices_signatures.rs new file mode 100644 index 0000000000..79f72efe7c --- /dev/null +++ b/common/nym_offline_compact_ecash/benches/benchmarks_coin_indices_signatures.rs @@ -0,0 +1,116 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use criterion::{criterion_group, criterion_main, Criterion}; +use nym_compact_ecash::scheme::coin_indices_signatures::{ + aggregate_indices_signatures, sign_coin_indices, verify_coin_indices_signatures, + CoinIndexSignatureShare, +}; +use nym_compact_ecash::scheme::keygen::SecretKeyAuth; +use nym_compact_ecash::setup::Parameters; +use nym_compact_ecash::{aggregate_verification_keys, ttp_keygen, VerificationKeyAuth}; + +fn bench_coin_signing(c: &mut Criterion) { + let mut group = c.benchmark_group("benchmark-sign-verify-coin-signing"); + + let ll = 32; + let params = Parameters::new(ll); + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + + // Pick one authority to do the signing + let sk_i_auth = authorities_keypairs[0].secret_key(); + let vk_i_auth = authorities_keypairs[0].verification_key(); + + // list of verification keys of each authority + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + // the global master verification key + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap(); + + let partial_signatures = sign_coin_indices(¶ms, &verification_key, sk_i_auth).unwrap(); + + // ISSUING AUTHORITY BENCHMARK: issue a set of (partial) signatures for coin indices + group.bench_function( + format!( + "[IssuingAuthority] sign_coin_indices_L_{}", + params.get_total_coins() + ), + |b| b.iter(|| sign_coin_indices(¶ms, &verification_key, sk_i_auth)), + ); + + // CLIENT: verify the correctness of the (partial)) signatures for coin indices + assert!( + verify_coin_indices_signatures(&verification_key, &vk_i_auth, &partial_signatures).is_ok() + ); + group.bench_function( + format!( + "[Client] verify_coin_indices_signatures_L_{}", + params.get_total_coins() + ), + |b| { + b.iter(|| { + verify_coin_indices_signatures(&verification_key, &vk_i_auth, &partial_signatures) + }) + }, + ); +} + +fn bench_aggregate_coin_indices_signatures(c: &mut Criterion) { + let mut group = c.benchmark_group("benchmark-aggregate-coin-signing"); + + let ll = 32; + let params = Parameters::new(ll); + let authorities_keypairs = ttp_keygen(7, 10).unwrap(); + let indices: [u64; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + + // list of secret keys of each authority + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + // list of verification keys of each authority + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + // the global master verification key + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap(); + + // create the partial signatures from each authority + let partial_signatures: Vec> = secret_keys_authorities + .iter() + .map(|sk_auth| sign_coin_indices(¶ms, &verification_key, sk_auth).unwrap()) + .collect(); + + let combined_data: Vec<_> = indices + .iter() + .zip(verification_keys_auth.iter().zip(partial_signatures.iter())) + .map(|(i, (vk, sigs))| CoinIndexSignatureShare { + index: *i, + key: vk.clone(), + signatures: sigs.clone(), + }) + .collect(); + + // CLIENT: verify all the partial signature vectors and aggregate into a single vector of signed coin indices + group.bench_function( + format!( + "[Client] aggregate_coin_indices_signatures_from_{}_issuing_authorities_L_{}", + authorities_keypairs.len(), + params.get_total_coins(), + ), + |b| b.iter(|| aggregate_indices_signatures(¶ms, &verification_key, &combined_data)), + ); +} + +criterion_group!( + benches, + bench_coin_signing, + bench_aggregate_coin_indices_signatures +); +criterion_main!(benches); diff --git a/common/nym_offline_compact_ecash/benches/benchmarks_ecash_e2e.rs b/common/nym_offline_compact_ecash/benches/benchmarks_ecash_e2e.rs new file mode 100644 index 0000000000..f7ea0cf9f3 --- /dev/null +++ b/common/nym_offline_compact_ecash/benches/benchmarks_ecash_e2e.rs @@ -0,0 +1,300 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use criterion::{criterion_group, criterion_main, Criterion}; +use itertools::izip; +use nym_compact_ecash::identify::{identify, IdentifyResult}; +use nym_compact_ecash::scheme::keygen::SecretKeyAuth; +use nym_compact_ecash::setup::Parameters; +use nym_compact_ecash::tests::helpers::{ + generate_coin_indices_signatures, generate_expiration_date_signatures, +}; +use nym_compact_ecash::{ + aggregate_verification_keys, aggregate_wallets, generate_keypair_user, issue, issue_verify, + ttp_keygen, withdrawal_request, PartialWallet, PayInfo, PublicKeyUser, SecretKeyUser, + VerificationKeyAuth, +}; +use nym_network_defaults::TicketTypeRepr; +use rand::seq::SliceRandom; + +struct BenchCase { + num_authorities: u64, + threshold_p: f32, + ll: u64, + spend_vv: u64, + case_nr_pub_keys: u64, +} + +fn bench_compact_ecash(c: &mut Criterion) { + let mut group = c.benchmark_group("benchmark-compact-ecash"); + // group.sample_size(300); + // group.measurement_time(Duration::from_secs(1500)); + + let spend_date = 1701907200; // Dec 07 2023 00:00:00 + let expiration_date = 1702166400; // Dec 10 2023 00:00:00 + let encoded_ticket_type = TicketTypeRepr::default() as u8; + + let case = BenchCase { + num_authorities: 100, + threshold_p: 0.7, + ll: 50, + spend_vv: 1, + case_nr_pub_keys: 99, + }; + + // SETUP PHASE and KEY GENERATION + let params = Parameters::new(case.ll); + + let grp = params.grp(); + let user_keypair = generate_keypair_user(); + let threshold = (case.threshold_p * case.num_authorities as f32).round() as u64; + let authorities_keypairs = ttp_keygen(threshold, case.num_authorities).unwrap(); + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + + let indices: Vec = (1..case.num_authorities + 1).collect(); + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap(); + + // PRE-GENERATION OF THE EXPORATION DATE SIGNATURES AND THE COIN INDICES SIGNATURES + // generate valid dates signatures + let dates_signatures = generate_expiration_date_signatures( + expiration_date, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + // generate coin indices signatures + let coin_indices_signatures = generate_coin_indices_signatures( + ¶ms, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + // ISSUANCE PHASE + let (req, req_info) = withdrawal_request( + user_keypair.secret_key(), + expiration_date, + encoded_ticket_type, + ) + .unwrap(); + + // CLIENT BENCHMARK: prepare a single withdrawal request + group.bench_function( + format!( + "[Client] withdrawal_request_{}_authorities_{}_L_{}_threshold", + case.num_authorities, case.ll, case.threshold_p, + ), + |b| { + b.iter(|| { + withdrawal_request( + user_keypair.secret_key(), + expiration_date, + encoded_ticket_type, + ) + .unwrap() + }) + }, + ); + + // ISSUING AUTHRORITY BENCHMARK: Benchmark the issue function + // called by an authority to issue a blind signature on a partial wallet + let mut rng = rand::thread_rng(); + let keypair = authorities_keypairs.choose(&mut rng).unwrap(); + group.bench_function( + format!( + "[Issuing Authority] issue_partial_wallet_with_L_{}", + case.ll, + ), + |b| { + b.iter(|| { + issue( + keypair.secret_key(), + user_keypair.public_key(), + &req, + expiration_date, + encoded_ticket_type, + ) + }) + }, + ); + + let mut wallet_blinded_signatures = Vec::new(); + for auth_keypair in &authorities_keypairs { + let blind_signature = issue( + auth_keypair.secret_key(), + user_keypair.public_key(), + &req, + expiration_date, + encoded_ticket_type, + ); + wallet_blinded_signatures.push(blind_signature.unwrap()); + } + + // CLIENT BENCHMARK: verify the issued partial wallet + let w = wallet_blinded_signatures.first().unwrap(); + let vk = verification_keys_auth.first().unwrap(); + group.bench_function( + format!("[Client] issue_verify_a_partial_wallet_with_L_{}", case.ll,), + |b| b.iter(|| issue_verify(vk, user_keypair.secret_key(), w, &req_info, 1).unwrap()), + ); + + let unblinded_wallet_shares: Vec = izip!( + wallet_blinded_signatures.iter(), + verification_keys_auth.iter() + ) + .enumerate() + .map(|(idx, (w, vk))| { + issue_verify(vk, user_keypair.secret_key(), w, &req_info, idx as u64 + 1).unwrap() + }) + .collect(); + + // CLIENT BENCHMARK: aggregating all partial wallets + group.bench_function( + format!( + "[Client] aggregate_wallets_with_L_{}_threshold_{}", + case.ll, case.threshold_p, + ), + |b| { + b.iter(|| { + aggregate_wallets( + &verification_key, + user_keypair.secret_key(), + &unblinded_wallet_shares, + &req_info, + ) + .unwrap() + }) + }, + ); + + // Aggregate partial wallets + let mut aggr_wallet = aggregate_wallets( + &verification_key, + user_keypair.secret_key(), + &unblinded_wallet_shares, + &req_info, + ) + .unwrap(); + let wallet_clone = aggr_wallet.clone(); + + // SPENDING PHASE + let pay_info = PayInfo { + pay_info_bytes: [6u8; 72], + }; + // CLIENT BENCHMARK: spend a single coin from the wallet + group.bench_function( + format!( + "[Client] spend_a_single_coin_L_{}_threshold_{}", + case.ll, case.threshold_p, + ), + |b| { + b.iter(|| { + aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info, + case.spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap() + }) + }, + ); + + let payment = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info, + case.spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + // MERCHANT BENCHMARK: verify whether the submitted payment is legit + group.bench_function( + format!( + "[Merchant] spend_verify_of_a_single_payment_L_{}_threshold_{}", + case.ll, case.threshold_p, + ), + |b| { + b.iter(|| { + payment + .spend_verify(&verification_key, &pay_info, spend_date) + .unwrap() + }) + }, + ); + + // BENCHMARK IDENTIFICATION + // Let's generate a double spending payment + + // let's reverse the spending counter in the wallet to create a double spending payment + let mut aggr_wallet = wallet_clone; + + let pay_info2 = PayInfo { + pay_info_bytes: [7u8; 72], + }; + let payment2 = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info2, + case.spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + // GENERATE KEYS FOR OTHER USERS + let mut public_keys: Vec = Default::default(); + for _ in 0..case.case_nr_pub_keys { + let sk = grp.random_scalar(); + let sk_user = SecretKeyUser::from_bytes(&sk.to_bytes()).unwrap(); + let pk_user = sk_user.public_key(); + public_keys.push(pk_user); + } + public_keys.push(user_keypair.public_key()); + + // MERCHANT BENCHMARK: identify double spending + group.bench_function( + format!( + "[Merchant] identify_L_{}_threshold_{}_spend_vv_{}_pks_{}", + case.ll, + case.threshold_p, + case.spend_vv, + public_keys.len() + ), + |b| b.iter(|| identify(&payment, &payment2, pay_info, pay_info2)), + ); + let identify_result = identify(&payment, &payment2, pay_info, pay_info2); + assert_eq!( + identify_result, + IdentifyResult::DoubleSpendingPublicKeys(user_keypair.public_key()) + ); +} + +criterion_group!(benches, bench_compact_ecash); +criterion_main!(benches); diff --git a/common/nym_offline_compact_ecash/benches/benchmarks_expiration_date_signatures.rs b/common/nym_offline_compact_ecash/benches/benchmarks_expiration_date_signatures.rs new file mode 100644 index 0000000000..2acb7cbfff --- /dev/null +++ b/common/nym_offline_compact_ecash/benches/benchmarks_expiration_date_signatures.rs @@ -0,0 +1,102 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use criterion::{criterion_group, criterion_main, Criterion}; +use nym_compact_ecash::constants; +use nym_compact_ecash::scheme::expiration_date_signatures::{ + aggregate_expiration_signatures, sign_expiration_date, verify_valid_dates_signatures, + ExpirationDateSignatureShare, +}; +use nym_compact_ecash::scheme::keygen::SecretKeyAuth; +use nym_compact_ecash::{aggregate_verification_keys, ttp_keygen, VerificationKeyAuth}; + +fn bench_partial_sign_expiration_date(c: &mut Criterion) { + let mut group = c.benchmark_group("benchmark-sign-verify-expiration-date"); + let expiration_date = 1703183958; + + let authorities_keys = ttp_keygen(2, 3).unwrap(); + let sk_i_auth = authorities_keys[0].secret_key(); + let vk_i_auth = authorities_keys[0].verification_key(); + let partial_exp_sig = sign_expiration_date(sk_i_auth, expiration_date).unwrap(); + + // ISSUING AUTHORITY BENCHMARK: issue a set of (partial) signatures for a given expiration date + group.bench_function( + format!( + "[IssuingAuthority] sign_expiration_date_{}_validity_period", + constants::CRED_VALIDITY_PERIOD_DAYS, + ), + |b| b.iter(|| sign_expiration_date(sk_i_auth, expiration_date)), + ); + + // CLIENT: verify the correctness of the set of (partial) signatures for a given expiration date + assert!(verify_valid_dates_signatures(&vk_i_auth, &partial_exp_sig, expiration_date).is_ok()); + group.bench_function( + format!( + "[Client] verify_valid_dates_signatures_{}_validity_period", + constants::CRED_VALIDITY_PERIOD_DAYS, + ), + |b| b.iter(|| verify_valid_dates_signatures(&vk_i_auth, &partial_exp_sig, expiration_date)), + ); +} + +fn bench_aggregate_expiration_date_signatures(c: &mut Criterion) { + let mut group = c.benchmark_group("benchmark-aggregate-verify-expiration-date-signatures"); + let expiration_date = 1703183958; + + let authorities_keypairs = ttp_keygen(7, 10).unwrap(); + let indices: [u64; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + // list of secret keys of each authority + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + // list of verification keys of each authority + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + // the global master verification key + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap(); + + let mut partial_signatures: Vec> = + Vec::with_capacity(constants::CRED_VALIDITY_PERIOD_DAYS as usize); + for sk in secret_keys_authorities.iter() { + let sign = sign_expiration_date(sk, expiration_date).unwrap(); + partial_signatures.push(sign); + } + + let combined_data: Vec<_> = indices + .iter() + .zip(verification_keys_auth.iter().zip(partial_signatures.iter())) + .map(|(i, (vk, sigs))| ExpirationDateSignatureShare { + index: *i, + key: vk.clone(), + signatures: sigs.clone(), + }) + .collect(); + + // CLIENT: verify all the partial signature vectors and aggregate into a single vector of signed valid dates + group.bench_function( + format!( + "[Client] aggregate_expiration_signatures_from_{}_issuing_authorities_{}_validity_period", + constants::CRED_VALIDITY_PERIOD_DAYS, authorities_keypairs.len(), + ), + |b| { + b.iter(|| { + aggregate_expiration_signatures( + &verification_key, + expiration_date, + &combined_data, + ) + }) + }, + ); +} + +criterion_group!( + benches, + bench_partial_sign_expiration_date, + bench_aggregate_expiration_date_signatures +); +criterion_main!(benches); diff --git a/common/nym_offline_compact_ecash/benches/benchmarks_group_operations.rs b/common/nym_offline_compact_ecash/benches/benchmarks_group_operations.rs new file mode 100644 index 0000000000..73c74472e7 --- /dev/null +++ b/common/nym_offline_compact_ecash/benches/benchmarks_group_operations.rs @@ -0,0 +1,189 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::ops::Neg; +use std::time::Duration; + +use bls12_381::{ + multi_miller_loop, G1Affine, G1Projective, G2Affine, G2Prepared, G2Projective, Gt, Scalar, +}; +use criterion::{criterion_group, criterion_main, Criterion}; +use ff::Field; +use group::{Curve, Group}; +use nym_compact_ecash::utils::check_bilinear_pairing; + +#[allow(unused)] +fn double_pairing(g11: &G1Affine, g21: &G2Affine, g12: &G1Affine, g22: &G2Affine) { + let gt1 = bls12_381::pairing(g11, g21); + let gt2 = bls12_381::pairing(g12, g22); + assert_eq!(gt1, gt2) +} + +#[allow(unused)] +fn single_pairing(g11: &G1Affine, g21: &G2Affine) { + let gt1 = bls12_381::pairing(g11, g21); +} + +#[allow(unused)] +fn exponent_in_g1(g1: G1Projective, r: Scalar) { + let g11 = (g1 * r); +} + +#[allow(unused)] +fn exponent_in_g2(g2: G2Projective, r: Scalar) { + let g22 = (g2 * r); +} + +#[allow(unused)] +fn exponent_in_gt(gt: Gt, r: Scalar) { + let gtt = (gt * r); +} + +#[allow(unused)] +fn multi_miller_pairing_affine(g11: &G1Affine, g21: &G2Affine, g12: &G1Affine, g22: &G2Affine) { + let miller_loop_result = multi_miller_loop(&[ + (g11, &G2Prepared::from(*g21)), + (&g12.neg(), &G2Prepared::from(*g22)), + ]); + assert!(bool::from( + miller_loop_result.final_exponentiation().is_identity() + )) +} + +#[allow(unused)] +fn multi_miller_pairing_with_prepared( + g11: &G1Affine, + g21: &G2Prepared, + g12: &G1Affine, + g22: &G2Prepared, +) { + let miller_loop_result = multi_miller_loop(&[(g11, g21), (&g12.neg(), g22)]); + assert!(bool::from( + miller_loop_result.final_exponentiation().is_identity() + )) +} + +// the case of being able to prepare G2 generator +#[allow(unused)] +fn multi_miller_pairing_with_semi_prepared( + g11: &G1Affine, + g21: &G2Affine, + g12: &G1Affine, + g22: &G2Prepared, +) { + let miller_loop_result = + multi_miller_loop(&[(g11, &G2Prepared::from(*g21)), (&g12.neg(), g22)]); + assert!(bool::from( + miller_loop_result.final_exponentiation().is_identity() + )) +} + +#[allow(unused)] +fn bench_group_operations(c: &mut Criterion) { + let mut group = c.benchmark_group("bench_group_operations"); + group.measurement_time(Duration::from_secs(200)); + + let mut rng = rand::thread_rng(); + + let g1 = G1Affine::generator(); + let g2 = G2Affine::generator(); + let r = Scalar::random(&mut rng); + let s = Scalar::random(&mut rng); + + let g11 = (g1 * r).to_affine(); + let g21 = (g2 * s).to_affine(); + let g21_prep = G2Prepared::from(g21); + + let g12 = (g1 * s).to_affine(); + let g22 = (g2 * r).to_affine(); + let g22_prep = G2Prepared::from(g22); + + let gt = bls12_381::pairing(&g11, &g21); + let gen1 = G1Projective::generator(); + let gen2 = G2Projective::generator(); + + group.bench_function("exponent operation in G1", |b| { + b.iter(|| exponent_in_g1(gen1, r)) + }); + + group.bench_function("exponent operation in G2", |b| { + b.iter(|| exponent_in_g2(gen2, r)) + }); + + group.bench_function("exponent operation in Gt", |b| { + b.iter(|| exponent_in_gt(gt, r)) + }); + + group.bench_function("single pairing", |b| b.iter(|| single_pairing(&g11, &g21))); + + group.bench_function("double pairing", |b| { + b.iter(|| double_pairing(&g11, &g21, &g12, &g22)) + }); + + group.bench_function("multi miller in affine", |b| { + b.iter(|| multi_miller_pairing_affine(&g11, &g21, &g12, &g22)) + }); + + group.bench_function("multi miller with prepared g2", |b| { + b.iter(|| multi_miller_pairing_with_prepared(&g11, &g21_prep, &g12, &g22_prep)) + }); + + group.bench_function("multi miller with semi-prepared g2", |b| { + b.iter(|| multi_miller_pairing_with_semi_prepared(&g11, &g21, &g12, &g22_prep)) + }); + + // bench_checking_vk_pairing + // assume key of size 5 + let scalars = [ + Scalar::random(&mut rng), + Scalar::random(&mut rng), + Scalar::random(&mut rng), + Scalar::random(&mut rng), + Scalar::random(&mut rng), + ]; + let gen1 = G1Affine::generator(); + let gen2_prep = G2Prepared::from(G2Affine::generator()); + + let g1 = scalars + .iter() + .map(|s| G1Affine::generator() * s) + .collect::>(); + let g2 = scalars + .iter() + .map(|s| G2Affine::generator() * s) + .collect::>(); + + group.bench_function("individual pairings", |b| { + b.iter(|| { + for (g1, g2) in g1.iter().zip(g2.iter()) { + let _ = check_bilinear_pairing( + &gen1, + &G2Prepared::from(g2.to_affine()), + &g1.to_affine(), + &gen2_prep, + ); + } + }) + }); + + group.bench_function("miller loop with duplicate elements", |b| { + b.iter(|| { + let mut terms = vec![]; + let neg_g1 = gen1.neg(); + for (g1, g2) in g1.iter().zip(g2.iter()) { + // TODO: optimise refs + terms.push((neg_g1, G2Prepared::from(g2.to_affine()))); + terms.push((g1.to_affine(), gen2_prep.clone())); + } + let terms_refs = terms.iter().map(|(g1, g2)| (g1, g2)).collect::>(); + + let _: bool = multi_miller_loop(&terms_refs) + .final_exponentiation() + .is_identity() + .into(); + }) + }); +} + +criterion_group!(benches, bench_group_operations); +criterion_main!(benches); diff --git a/common/nym_offline_compact_ecash/src/common_types.rs b/common/nym_offline_compact_ecash/src/common_types.rs new file mode 100644 index 0000000000..6aaf81f673 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/common_types.rs @@ -0,0 +1,97 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash_group_parameters; +use crate::error::Result; +use crate::helpers::{g1_tuple_to_bytes, recover_g1_tuple}; +use bls12_381::{G1Projective, Scalar}; +use serde::{Deserialize, Serialize}; + +pub type SignerIndex = u64; + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct Signature { + pub(crate) h: G1Projective, + pub(crate) s: G1Projective, +} + +pub type PartialSignature = Signature; + +impl Signature { + pub(crate) fn sig1(&self) -> &G1Projective { + &self.h + } + + pub(crate) fn sig2(&self) -> &G1Projective { + &self.s + } + + /// Function randomises the signature. + /// + /// # Returns + /// + /// A tuple containing the randomised signature and the blinding scalar. + pub fn blind_and_randomise(&self) -> (Signature, Scalar) { + let params = ecash_group_parameters(); + + // Generate random blinding scalars + let r = params.random_scalar(); + let r_prime = params.random_scalar(); + + // Calculate h_prime and s_prime using the random scalars + let h_prime = self.h * r_prime; + let s_prime = (self.s * r_prime) + (h_prime * r); + ( + Signature { + h: h_prime, + s: s_prime, + }, + r, + ) + } + + pub fn to_bytes(self) -> [u8; 96] { + g1_tuple_to_bytes((self.h, self.s)) + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let (h, s) = recover_g1_tuple::(bytes)?; + Ok(Signature { h, s }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct BlindedSignature { + pub(crate) h: G1Projective, + pub(crate) c: G1Projective, +} + +impl BlindedSignature { + pub fn to_bytes(self) -> [u8; 96] { + g1_tuple_to_bytes((self.h, self.c)) + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let (h, c) = recover_g1_tuple::(bytes)?; + Ok(BlindedSignature { h, c }) + } +} + +pub struct SignatureShare { + signature: Signature, + index: SignerIndex, +} + +impl SignatureShare { + pub fn new(signature: Signature, index: SignerIndex) -> Self { + SignatureShare { signature, index } + } + + pub fn signature(&self) -> &Signature { + &self.signature + } + + pub fn index(&self) -> SignerIndex { + self.index + } +} diff --git a/common/nym_offline_compact_ecash/src/constants.rs b/common/nym_offline_compact_ecash/src/constants.rs new file mode 100644 index 0000000000..0902c2fa0f --- /dev/null +++ b/common/nym_offline_compact_ecash/src/constants.rs @@ -0,0 +1,30 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bls12_381::Scalar; +use nym_network_defaults::ecash::TICKETBOOK_VALIDITY_DAYS; +use nym_network_defaults::TICKETBOOK_SIZE; + +pub const PUBLIC_ATTRIBUTES_LEN: usize = 2; //expiration date and ticket type +pub const PRIVATE_ATTRIBUTES_LEN: usize = 2; //user and wallet secret +pub const ATTRIBUTES_LEN: usize = PUBLIC_ATTRIBUTES_LEN + PRIVATE_ATTRIBUTES_LEN; // number of attributes encoded in a single zk-nym credential + +pub const CRED_VALIDITY_PERIOD_DAYS: u32 = TICKETBOOK_VALIDITY_DAYS; + +pub(crate) const SECONDS_PER_DAY: u32 = 86400; + +/// Total number of tickets in each issued ticket book. +pub const NB_TICKETS: u64 = TICKETBOOK_SIZE; + +pub const TYPE_EXP: Scalar = Scalar::from_raw([ + u64::from_le_bytes(*b"ZKNYMEXP"), + u64::from_le_bytes(*b"IRATIOND"), + u64::from_le_bytes(*b"ATE4llCB"), + u64::from_le_bytes(*b"MEypAxr3"), +]); +pub const TYPE_IDX: Scalar = Scalar::from_raw([ + u64::from_le_bytes(*b"ZKNYMSIN"), + u64::from_le_bytes(*b"DICESh^7"), + u64::from_le_bytes(*b"gTYbhnap"), + u64::from_le_bytes(*b"*12n5GG6"), +]); diff --git a/common/nym_offline_compact_ecash/src/error.rs b/common/nym_offline_compact_ecash/src/error.rs new file mode 100644 index 0000000000..0d49581ec6 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/error.rs @@ -0,0 +1,125 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Error, Debug)] +pub enum CompactEcashError { + #[error("failed to verify expiration date signatures")] + ExpirationDateSignatureVerification, + + #[error("failed to validate expiration date signatures")] + ExpirationDateSignatureValidity, + + #[error("empty set for aggregation")] + AggregationEmptySet, + + #[error("duplicate indices for aggregation")] + AggregationDuplicateIndices, + + #[error("aggregation verification error")] + AggregationVerification, + + #[error("different element size for aggregation")] + AggregationSizeMismatch, + + #[error("withdrawal request failed to verify")] + WithdrawalRequestVerification, + + #[error("invalid key generation parameters")] + KeygenParameters, + + #[error("signing authority's key is too short")] + KeyTooShort, + + #[error("empty/incomplete set of coordinates for interpolation")] + InterpolationSetSize, + + #[error("issuance verification failed")] + IssuanceVerification, + + #[error("trying to spend more than what's available. Spending : {spending}, available : {remaining}")] + SpendExceedsAllowance { spending: u64, remaining: u64 }, + + #[error("signature failed validity check")] + SpendSignaturesValidity, + + #[error("signature failed verification check")] + SpendSignaturesVerification, + + #[error("duplicate serial number in the payment")] + SpendDuplicateSerialNumber, + + #[error("given spend date is too late")] + SpendDateTooLate, + + #[error("given spend date is too early")] + SpendDateTooEarly, + + #[error("ZK proof failed to verify")] + SpendZKProofVerification, + + #[error("could not decode base 58 string - {0}")] + MalformedString(#[from] bs58::decode::Error), + + #[error("failed to verify coin indices signatures")] + CoinIndicesSignatureVerification, + + #[error("failed to deserialize a {object}")] + DeserializationFailure { object: String }, + + #[error("failed to deserialise {type_name}: {source}")] + BinaryDeserialisationFailure { + type_name: String, + source: bincode::Error, + }, + + #[error( + "deserialization error, expected at least {} bytes, got {}", + min, + actual + )] + DeserializationMinLength { min: usize, actual: usize }, + + #[error("{type_name} deserialization error, expected {expected} bytes, got {actual}")] + DeserializationLengthMismatch { + type_name: String, + expected: usize, + actual: usize, + }, + + #[error("tried to deserialize {object} with bytes of invalid length. Expected {actual} < {target} or {modulus_target} % {modulus} == 0")] + DeserializationInvalidLength { + actual: usize, + target: usize, + modulus_target: usize, + modulus: usize, + object: String, + }, + + #[error("failed to deserialize scalar from the received bytes - it might not have been canonically encoded")] + ScalarDeserializationFailure, + + #[error("failed to deserialize G1Projective point from the received bytes - it might not have been canonically encoded")] + G1ProjectiveDeserializationFailure, + + #[error("failed to deserialize G2Projective point from the received bytes - it might not have been canonically encoded")] + G2ProjectiveDeserializationFailure, + + #[error("verification key is invalid for this operation")] + VerificationKeyTooShort, + + #[error("did not provide the sufficient number of coin index signatures")] + InsufficientNumberOfIndexSignatures, + + #[error("did not provide the sufficient number of expiration date signatures")] + InsufficientNumberOfExpirationSignatures, + + //context : This can happen only if the wallet secret `v` was picked such that `v + coin_index + 1 == 0`. + //The chance of this happening is of the order 2^-381 and not failing is waay too much work. + //TLDR: this event can happen, but with probability 0 + #[error("you're one of the most unluck person on your planet and your wallet cannot complete this payment")] + UnluckiestError, +} diff --git a/common/nym_offline_compact_ecash/src/helpers.rs b/common/nym_offline_compact_ecash/src/helpers.rs new file mode 100644 index 0000000000..af741d5f34 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/helpers.rs @@ -0,0 +1,61 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::utils::try_deserialize_g1_projective; +use crate::{CompactEcashError, EncodedDate, EncodedTicketType}; +use bls12_381::{G1Projective, Scalar}; +use group::Curve; +use std::any::{type_name, Any}; + +pub(crate) fn g1_tuple_to_bytes(el: (G1Projective, G1Projective)) -> [u8; 96] { + let mut bytes = [0u8; 96]; + bytes[..48].copy_from_slice(&el.0.to_affine().to_compressed()); + bytes[48..].copy_from_slice(&el.1.to_affine().to_compressed()); + bytes +} + +pub(crate) fn recover_g1_tuple( + bytes: &[u8], +) -> crate::error::Result<(G1Projective, G1Projective)> { + if bytes.len() != 96 { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: type_name::().into(), + expected: 96, + actual: bytes.len(), + }); + } + //SAFETY : [0..48] into 48 sized array and [48..96] into 48 sized array + #[allow(clippy::unwrap_used)] + let first_bytes: &[u8; 48] = &bytes[..48].try_into().unwrap(); + #[allow(clippy::unwrap_used)] + let second_bytes: &[u8; 48] = &bytes[48..].try_into().unwrap(); + + let first = try_deserialize_g1_projective(first_bytes)?; + let second = try_deserialize_g1_projective(second_bytes)?; + + Ok((first, second)) +} + +pub fn date_scalar(date: EncodedDate) -> Scalar { + Scalar::from(date as u64) +} + +// TODO: this will not work for **all** scalars, +// but timestamps have extremely (relatively speaking) limited range, +// so this should be fine +pub(crate) fn scalar_date(scalar: &Scalar) -> EncodedDate { + let b = scalar.to_bytes(); + u64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]) as EncodedDate +} + +pub fn type_scalar(t_type: EncodedTicketType) -> Scalar { + Scalar::from(t_type as u64) +} + +// TODO: this will not work for **all** scalars, +// but ticket types have extremely (relatively speaking) limited range, +// so this should be fine +pub(crate) fn scalar_type(scalar: &Scalar) -> EncodedTicketType { + let b = scalar.to_bytes(); + u64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]) as EncodedTicketType +} diff --git a/common/nym_offline_compact_ecash/src/lib.rs b/common/nym_offline_compact_ecash/src/lib.rs new file mode 100644 index 0000000000..c6346a159f --- /dev/null +++ b/common/nym_offline_compact_ecash/src/lib.rs @@ -0,0 +1,65 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] +#![warn(clippy::todo)] +#![warn(clippy::dbg_macro)] + +use bls12_381::Scalar; +use std::sync::OnceLock; + +pub use crate::error::CompactEcashError; +pub use crate::traits::Bytable; +pub use bls12_381::G1Projective; +pub use common_types::{BlindedSignature, Signature}; +pub use helpers::{date_scalar, type_scalar}; +pub use scheme::aggregation::aggregate_verification_keys; +pub use scheme::aggregation::aggregate_wallets; +pub use scheme::identify; +pub use scheme::keygen::ttp_keygen; +pub use scheme::keygen::{generate_keypair_user, generate_keypair_user_from_seed}; +pub use scheme::keygen::{ + KeyPairAuth, PublicKeyUser, SecretKeyAuth, SecretKeyUser, VerificationKeyAuth, +}; +pub use scheme::setup; +pub use scheme::withdrawal::issue; +pub use scheme::withdrawal::issue_verify; +pub use scheme::withdrawal::withdrawal_request; +pub use scheme::withdrawal::WithdrawalRequest; +pub use scheme::PartialWallet; +pub use scheme::PayInfo; +pub use setup::GroupParameters; +pub use traits::Base58; + +pub mod common_types; +pub mod constants; +pub mod error; +mod helpers; +mod proofs; +pub mod scheme; +pub mod tests; +mod traits; +pub mod utils; + +pub type Attribute = Scalar; +pub type EncodedTicketType = u8; +pub type EncodedDate = u32; + +pub fn ecash_parameters() -> &'static setup::Parameters { + static ECASH_PARAMS: OnceLock = OnceLock::new(); + ECASH_PARAMS.get_or_init(|| setup::Parameters::new(constants::NB_TICKETS)) +} + +pub fn ecash_group_parameters() -> &'static setup::GroupParameters { + static ECASH_PARAMS: OnceLock = OnceLock::new(); + ECASH_PARAMS.get_or_init(|| setup::GroupParameters::new(constants::ATTRIBUTES_LEN)) +} + +// if anything changes here you MUST correctly increase semver of this library +pub(crate) fn binary_serialiser() -> impl bincode::Options { + use bincode::Options; + bincode::DefaultOptions::new() + .with_big_endian() + .with_varint_encoding() +} diff --git a/common/nym_offline_compact_ecash/src/proofs/mod.rs b/common/nym_offline_compact_ecash/src/proofs/mod.rs new file mode 100644 index 0000000000..5133c2bedd --- /dev/null +++ b/common/nym_offline_compact_ecash/src/proofs/mod.rs @@ -0,0 +1,59 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::borrow::Borrow; + +use bls12_381::Scalar; +use digest::generic_array::typenum::Unsigned; +use digest::Digest; +use sha2::Sha256; + +pub mod proof_spend; +pub mod proof_withdrawal; + +type ChallengeDigest = Sha256; + +/// Generates a Scalar [or Fp] challenge by hashing a number of elliptic curve points. +fn compute_challenge(iter: I) -> Scalar +where + D: Digest, + I: Iterator, + B: AsRef<[u8]>, +{ + let mut h = D::new(); + for point_representation in iter { + h.update(point_representation); + } + let digest = h.finalize(); + + // TODO: I don't like the 0 padding here (though it's what we've been using before, + // but we never had a security audit anyway...) + // instead we could maybe use the `from_bytes` variant and adding some suffix + // when computing the digest until we produce a valid scalar. + let mut bytes = [0u8; 64]; + let pad_size = 64usize + .checked_sub(D::OutputSize::to_usize()) + .unwrap_or_default(); + + bytes[pad_size..].copy_from_slice(&digest); + + Scalar::from_bytes_wide(&bytes) +} + +fn produce_response(witness_replacement: &Scalar, challenge: &Scalar, secret: &Scalar) -> Scalar { + witness_replacement - challenge * secret +} + +// note: it's caller's responsibility to ensure witnesses.len() = secrets.len() +fn produce_responses(witnesses: &[Scalar], challenge: &Scalar, secrets: &[S]) -> Vec +where + S: Borrow, +{ + debug_assert_eq!(witnesses.len(), secrets.len()); + + witnesses + .iter() + .zip(secrets.iter()) + .map(|(w, x)| produce_response(w, challenge, x.borrow())) + .collect() +} diff --git a/common/nym_offline_compact_ecash/src/proofs/proof_spend.rs b/common/nym_offline_compact_ecash/src/proofs/proof_spend.rs new file mode 100644 index 0000000000..966dd2211c --- /dev/null +++ b/common/nym_offline_compact_ecash/src/proofs/proof_spend.rs @@ -0,0 +1,397 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash_group_parameters; +use crate::proofs::{compute_challenge, produce_response, produce_responses, ChallengeDigest}; +use crate::scheme::keygen::VerificationKeyAuth; +use crate::scheme::PayInfo; +use bls12_381::{G1Projective, G2Projective, Scalar}; +use group::GroupEncoding; +use serde::{Deserialize, Serialize}; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +pub struct SpendInstance { + pub kappa: G2Projective, + pub cc: G1Projective, + pub aa: Vec, + pub ss: Vec, + pub tt: Vec, + pub kappa_k: Vec, + pub kappa_e: G2Projective, +} + +#[derive(Zeroize, ZeroizeOnDrop)] +pub struct SpendWitness<'a> { + // includes skUser, v, t + #[zeroize(skip)] + pub(crate) attributes: &'a [&'a Scalar], + + // signature randomizing element + pub(crate) r: Scalar, + pub(crate) o_c: Scalar, + pub(crate) lk: Vec, + pub(crate) o_a: Vec, + pub(crate) mu: Vec, + pub(crate) o_mu: Vec, + pub(crate) r_k: Vec, + pub(crate) r_e: Scalar, +} + +pub struct WitnessReplacement { + pub r_attributes: Vec, + pub r_r: Scalar, + pub r_r_e: Scalar, + pub r_o_c: Scalar, + pub r_r_k: Vec, + pub r_lk: Vec, + pub r_o_a: Vec, + pub r_mu: Vec, + pub r_o_mu: Vec, +} + +pub struct InstanceCommitments { + pub tt_kappa: G2Projective, + pub tt_kappa_e: G2Projective, + pub tt_cc: G1Projective, + pub tt_aa: Vec, + pub tt_ss: Vec, + pub tt_tt: Vec, + pub tt_gamma1: Vec, + pub tt_kappa_k: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SpendProof { + challenge: Scalar, + response_r: Scalar, + response_r_e: Scalar, + responses_r_k: Vec, + responses_l: Vec, + responses_o_a: Vec, + response_o_c: Scalar, + responses_mu: Vec, + responses_o_mu: Vec, + responses_attributes: Vec, +} + +pub fn generate_witness_replacement(witness: &SpendWitness) -> WitnessReplacement { + let grp_params = ecash_group_parameters(); + let r_attributes = grp_params.n_random_scalars(witness.attributes.len()); + let r_r = grp_params.random_scalar(); + let r_r_e = grp_params.random_scalar(); + let r_o_c = grp_params.random_scalar(); + + let r_r_k = grp_params.n_random_scalars(witness.r_k.len()); + let r_lk = grp_params.n_random_scalars(witness.lk.len()); + let r_o_a = grp_params.n_random_scalars(witness.o_a.len()); + let r_mu = grp_params.n_random_scalars(witness.mu.len()); + let r_o_mu = grp_params.n_random_scalars(witness.o_mu.len()); + WitnessReplacement { + r_attributes, + r_r, + r_r_e, + r_o_c, + r_r_k, + r_lk, + r_o_a, + r_mu, + r_o_mu, + } +} + +pub fn compute_instance_commitments( + witness_replacement: &WitnessReplacement, + instance: &SpendInstance, + verification_key: &VerificationKeyAuth, + rr: &[Scalar], +) -> InstanceCommitments { + let grp_params = ecash_group_parameters(); + let g1 = *grp_params.gen1(); + //SAFETY: grp_params is static with length 3 + #[allow(clippy::unwrap_used)] + let gamma1 = grp_params.gamma_idx(1).unwrap(); + + let tt_kappa = grp_params.gen2() * witness_replacement.r_r + + verification_key.alpha + + witness_replacement + .r_attributes + .iter() + .zip(verification_key.beta_g2.iter()) + .map(|(attr, beta_i)| beta_i * attr) + .sum::(); + + let tt_cc = g1 * witness_replacement.r_o_c + gamma1 * witness_replacement.r_attributes[1]; + + let tt_kappa_e = grp_params.gen2() * witness_replacement.r_r_e + + verification_key.alpha + + verification_key.beta_g2[0] * witness_replacement.r_attributes[2]; + + let tt_aa: Vec = witness_replacement + .r_o_a + .iter() + .zip(witness_replacement.r_lk.iter()) + .map(|(r_o_a_k, r_l_k)| g1 * r_o_a_k + gamma1 * r_l_k) + .collect::>(); + + let tt_kappa_k = witness_replacement + .r_lk + .iter() + .zip(witness_replacement.r_r_k.iter()) + .map(|(r_l_k, r_r_k)| { + verification_key.alpha + verification_key.beta_g2[0] * r_l_k + grp_params.gen2() * r_r_k + }) + .collect::>(); + + let tt_ss = witness_replacement + .r_mu + .iter() + .map(|r_mu_k| grp_params.delta() * r_mu_k) + .collect::>(); + + let tt_tt = rr + .iter() + .zip(witness_replacement.r_mu.iter()) + .map(|(rr_k, r_mu_k)| g1 * witness_replacement.r_attributes[0] + (g1 * rr_k) * r_mu_k) + .collect::>(); + + let tt_gamma1 = instance + .aa + .iter() + .zip(witness_replacement.r_mu.iter()) + .zip(witness_replacement.r_o_mu.iter()) + .map(|((aa_k, r_mu_k), r_o_mu_k)| (aa_k + instance.cc + gamma1) * r_mu_k + g1 * r_o_mu_k) + .collect::>(); + + InstanceCommitments { + tt_kappa, + tt_kappa_e, + tt_cc, + tt_aa, + tt_ss, + tt_tt, + tt_gamma1, + tt_kappa_k, + } +} + +impl SpendProof { + pub fn construct( + instance: &SpendInstance, + witness: &SpendWitness, + verification_key: &VerificationKeyAuth, + rr: &[Scalar], + pay_info: &PayInfo, + spend_value: u64, + ) -> Self { + let grp_params = ecash_group_parameters(); + // generate random values to replace each witness + let witness_replacement = generate_witness_replacement(witness); + + // compute zkp commitment for each instance + let instance_commitments = + compute_instance_commitments(&witness_replacement, instance, verification_key, rr); + + let tt_aa_bytes = instance_commitments + .tt_aa + .iter() + .map(|x| x.to_bytes()) + .collect::>(); + let tt_ss_bytes = instance_commitments + .tt_ss + .iter() + .map(|x| x.to_bytes()) + .collect::>(); + let tt_tt_bytes = instance_commitments + .tt_tt + .iter() + .map(|x| x.to_bytes()) + .collect::>(); + let tt_gamma1_bytes = instance_commitments + .tt_gamma1 + .iter() + .map(|x| x.to_bytes()) + .collect::>(); + let tt_kappa_k_bytes = instance_commitments + .tt_kappa_k + .iter() + .map(|x| x.to_bytes()) + .collect::>(); + + // compute the challenge + let challenge = compute_challenge::( + std::iter::once(grp_params.gen1().to_bytes().as_ref()) + .chain(std::iter::once(grp_params.gen2().to_bytes().as_ref())) + .chain(std::iter::once(grp_params.gammas_to_bytes().as_ref())) + .chain(std::iter::once(verification_key.to_bytes().as_ref())) + .chain(std::iter::once(instance.to_bytes().as_ref())) + .chain(std::iter::once( + instance_commitments.tt_kappa.to_bytes().as_ref(), + )) + .chain(std::iter::once( + instance_commitments.tt_kappa_e.to_bytes().as_ref(), + )) + .chain(std::iter::once( + instance_commitments.tt_cc.to_bytes().as_ref(), + )) + .chain(tt_aa_bytes.iter().map(|x| x.as_ref())) + .chain(tt_ss_bytes.iter().map(|x| x.as_ref())) + .chain(tt_kappa_k_bytes.iter().map(|x| x.as_ref())) + .chain(tt_gamma1_bytes.iter().map(|x| x.as_ref())) + .chain(tt_tt_bytes.iter().map(|x| x.as_ref())) + .chain(std::iter::once(pay_info.pay_info_bytes.as_ref())) + .chain(std::iter::once(spend_value.to_le_bytes().as_ref())), + ); + + // compute response for each witness + let responses_attributes = produce_responses( + &witness_replacement.r_attributes, + &challenge, + witness.attributes, + ); + let response_r = produce_response(&witness_replacement.r_r, &challenge, &witness.r); + let response_r_e = produce_response(&witness_replacement.r_r_e, &challenge, &witness.r_e); + let response_o_c = produce_response(&witness_replacement.r_o_c, &challenge, &witness.o_c); + + let responses_r_k = produce_responses(&witness_replacement.r_r_k, &challenge, &witness.r_k); + let responses_l = produce_responses(&witness_replacement.r_lk, &challenge, &witness.lk); + let responses_o_a = produce_responses(&witness_replacement.r_o_a, &challenge, &witness.o_a); + let responses_mu = produce_responses(&witness_replacement.r_mu, &challenge, &witness.mu); + let responses_o_mu = + produce_responses(&witness_replacement.r_o_mu, &challenge, &witness.o_mu); + + SpendProof { + challenge, + response_r, + response_r_e, + responses_r_k, + responses_l, + responses_o_a, + response_o_c, + responses_mu, + responses_o_mu, + responses_attributes, + } + } + + pub fn verify( + &self, + instance: &SpendInstance, + verification_key: &VerificationKeyAuth, + rr: &[Scalar], + pay_info: &PayInfo, + spend_value: u64, + ) -> bool { + let grp_params = ecash_group_parameters(); + let g1 = *grp_params.gen1(); + //SAFETY: grp_params is static with length 3 + #[allow(clippy::unwrap_used)] + let gamma1 = grp_params.gamma_idx(1).unwrap(); + + // re-compute each zkp commitment + let tt_kappa = instance.kappa * self.challenge + + verification_key.alpha * (self.challenge.neg()) + + verification_key.alpha + + grp_params.gen2() * self.response_r + + self + .responses_attributes + .iter() + .zip(verification_key.beta_g2.iter()) + .map(|(attr, beta_i)| beta_i * attr) + .sum::(); + + let tt_cc = g1 * self.response_o_c + + gamma1 * self.responses_attributes[1] + + instance.cc * self.challenge; + + let tt_kappa_e = instance.kappa_e * self.challenge + + verification_key.alpha * (self.challenge.neg()) + + verification_key.alpha + + verification_key.beta_g2[0] * self.responses_attributes[2] + + grp_params.gen2() * self.response_r_e; + + let tt_aa = self + .responses_o_a + .iter() + .zip(self.responses_l.iter()) + .zip(instance.aa.iter()) + .map(|((resp_o_a_k, resp_l_k), aa_k)| { + g1 * resp_o_a_k + gamma1 * resp_l_k + aa_k * self.challenge + }) + .collect::>(); + + let tt_aa_bytes = tt_aa.iter().map(|x| x.to_bytes()).collect::>(); + + let tt_ss = self + .responses_mu + .iter() + .zip(instance.ss.iter()) + .map(|(resp_mu_k, ss_k)| grp_params.delta() * resp_mu_k + ss_k * self.challenge) + .collect::>(); + + let tt_ss_bytes = tt_ss.iter().map(|x| x.to_bytes()).collect::>(); + + let tt_tt = self + .responses_mu + .iter() + .zip(rr.iter()) + .zip(instance.tt.iter()) + .map(|((resp_mu_k, rr_k), tt_k)| { + g1 * self.responses_attributes[0] + (g1 * rr_k) * resp_mu_k + tt_k * self.challenge + }) + .collect::>(); + + let tt_tt_bytes = tt_tt.iter().map(|x| x.to_bytes()).collect::>(); + + let tt_gamma1 = instance + .aa + .iter() + .zip(self.responses_mu.iter()) + .zip(self.responses_o_mu.iter()) + .map(|((aa_k, resp_mu_k), resp_o_mu_k)| { + (aa_k + instance.cc + gamma1) * resp_mu_k + + g1 * resp_o_mu_k + + gamma1 * self.challenge + }) + .collect::>(); + + let tt_gamma1_bytes = tt_gamma1.iter().map(|x| x.to_bytes()).collect::>(); + + let tt_kappa_k = instance + .kappa_k + .iter() + .zip(self.responses_r_k.iter()) + .zip(self.responses_l.iter()) + .map(|((kappa_k, resp_r_k), resp_r_l_k)| { + kappa_k * self.challenge + + grp_params.gen2() * resp_r_k + + verification_key.alpha * (Scalar::one() - self.challenge) + + verification_key.beta_g2[0] * resp_r_l_k + }) + .collect::>(); + + let tt_kappa_k_bytes = tt_kappa_k.iter().map(|x| x.to_bytes()).collect::>(); + + // re-compute the challenge + let challenge = compute_challenge::( + std::iter::once(grp_params.gen1().to_bytes().as_ref()) + .chain(std::iter::once(grp_params.gen2().to_bytes().as_ref())) + .chain(std::iter::once(grp_params.gammas_to_bytes().as_ref())) + .chain(std::iter::once(verification_key.to_bytes().as_ref())) + .chain(std::iter::once(instance.to_bytes().as_ref())) + .chain(std::iter::once(tt_kappa.to_bytes().as_ref())) + .chain(std::iter::once(tt_kappa_e.to_bytes().as_ref())) + .chain(std::iter::once(tt_cc.to_bytes().as_ref())) + .chain(tt_aa_bytes.iter().map(|x| x.as_ref())) + .chain(tt_ss_bytes.iter().map(|x| x.as_ref())) + .chain(tt_kappa_k_bytes.iter().map(|x| x.as_ref())) + .chain(tt_gamma1_bytes.iter().map(|x| x.as_ref())) + .chain(tt_tt_bytes.iter().map(|x| x.as_ref())) + .chain(std::iter::once(pay_info.pay_info_bytes.as_ref())) + .chain(std::iter::once(spend_value.to_le_bytes().as_ref())), + ); + + challenge == self.challenge + } +} diff --git a/common/nym_offline_compact_ecash/src/proofs/proof_withdrawal.rs b/common/nym_offline_compact_ecash/src/proofs/proof_withdrawal.rs new file mode 100644 index 0000000000..cba7161713 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/proofs/proof_withdrawal.rs @@ -0,0 +1,248 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash_group_parameters; +use crate::proofs::{compute_challenge, produce_response, produce_responses, ChallengeDigest}; +use crate::scheme::keygen::PublicKeyUser; +use bls12_381::{G1Projective, Scalar}; +use group::GroupEncoding; +use itertools::izip; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +// instance: g, gamma1, gamma2, gamma3, com, h, com1, com2, com3, pkUser +pub struct WithdrawalReqInstance { + // Joined commitment to all attributes + pub(crate) joined_commitment: G1Projective, + // Hash of the joined commitment com + pub(crate) joined_commitment_hash: G1Projective, + // Pedersen commitments to each attribute + pub(crate) private_attributes_commitments: Vec, + // Public key of a user + pub(crate) pk_user: PublicKeyUser, +} + +// witness: m1, m2, m3, o, o1, o2, o3, +pub struct WithdrawalReqWitness { + pub private_attributes: Vec, + // Opening for the joined commitment com + pub joined_commitment_opening: Scalar, + // Openings for the pedersen commitments of private attributes + pub private_attributes_openings: Vec, +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct WithdrawalReqProof { + challenge: Scalar, + response_opening: Scalar, + response_openings: Vec, + response_attributes: Vec, +} + +impl WithdrawalReqProof { + pub(crate) fn construct( + instance: &WithdrawalReqInstance, + witness: &WithdrawalReqWitness, + ) -> Self { + let params = ecash_group_parameters(); + // generate random values to replace the witnesses + let r_com_opening = params.random_scalar(); + let r_pedcom_openings = params.n_random_scalars(witness.private_attributes_openings.len()); + let r_attributes = params.n_random_scalars(witness.private_attributes.len()); + + // compute zkp commitments for each instance + let zkcm_com = params.gen1() * r_com_opening + + r_attributes + .iter() + .zip(params.gammas().iter()) + .map(|(rm_i, gamma_i)| gamma_i * rm_i) + .sum::(); + + let zkcm_pedcom = r_pedcom_openings + .iter() + .zip(r_attributes.iter()) + .map(|(o_j, m_j)| params.gen1() * o_j + instance.joined_commitment_hash * m_j) + .collect::>(); + + let zkcm_user_sk = params.gen1() * r_attributes[0]; + + // covert to bytes + let gammas_bytes = params + .gammas() + .iter() + .map(|gamma| gamma.to_bytes()) + .collect::>(); + + let zkcm_pedcom_bytes = zkcm_pedcom + .iter() + .map(|cm| cm.to_bytes()) + .collect::>(); + + // compute zkp challenge using g1, gammas, c, h, c1, c2, c3, zk commitments + let challenge = compute_challenge::( + std::iter::once(params.gen1().to_bytes().as_ref()) + .chain(gammas_bytes.iter().map(|gamma| gamma.as_ref())) + .chain(std::iter::once(instance.to_bytes().as_ref())) + .chain(std::iter::once(zkcm_com.to_bytes().as_ref())) + .chain(zkcm_pedcom_bytes.iter().map(|c| c.as_ref())) + .chain(std::iter::once(zkcm_user_sk.to_bytes().as_ref())), + ); + + // compute response + let response_opening = produce_response( + &r_com_opening, + &challenge, + &witness.joined_commitment_opening, + ); + let response_openings = produce_responses( + &r_pedcom_openings, + &challenge, + &witness + .private_attributes_openings + .iter() + .collect::>(), + ); + let response_attributes = produce_responses( + &r_attributes, + &challenge, + &witness.private_attributes.iter().collect::>(), + ); + + WithdrawalReqProof { + challenge, + response_opening, + response_openings, + response_attributes, + } + } + + pub(crate) fn verify(&self, instance: &WithdrawalReqInstance) -> bool { + let params = ecash_group_parameters(); + // recompute zk commitments for each instance + let zkcm_com = instance.joined_commitment * self.challenge + + params.gen1() * self.response_opening + + self + .response_attributes + .iter() + .zip(params.gammas().iter()) + .map(|(m_i, gamma_i)| gamma_i * m_i) + .sum::(); + + let zkcm_pedcom = izip!( + instance.private_attributes_commitments.iter(), + self.response_openings.iter(), + self.response_attributes.iter() + ) + .map(|(cm_j, resp_o_j, resp_m_j)| { + cm_j * self.challenge + + params.gen1() * resp_o_j + + instance.joined_commitment_hash * resp_m_j + }) + .collect::>(); + + let zk_commitment_user_sk = + instance.pk_user.pk * self.challenge + params.gen1() * self.response_attributes[0]; + + // covert to bytes + let gammas_bytes = params + .gammas() + .iter() + .map(|gamma| gamma.to_bytes()) + .collect::>(); + + let zkcm_pedcom_bytes = zkcm_pedcom + .iter() + .map(|cm| cm.to_bytes()) + .collect::>(); + + // recompute zkp challenge + let challenge = compute_challenge::( + std::iter::once(params.gen1().to_bytes().as_ref()) + .chain(gammas_bytes.iter().map(|hs| hs.as_ref())) + .chain(std::iter::once(instance.to_bytes().as_ref())) + .chain(std::iter::once(zkcm_com.to_bytes().as_ref())) + .chain(zkcm_pedcom_bytes.iter().map(|c| c.as_ref())) + .chain(std::iter::once(zk_commitment_user_sk.to_bytes().as_ref())), + ); + + challenge == self.challenge + } +} + +#[cfg(test)] +mod tests { + use group::Group; + use rand::thread_rng; + + use crate::GroupParameters; + use crate::{constants, utils::hash_g1}; + + use super::*; + + #[test] + fn withdrawal_request_instance_roundtrip() { + let mut rng = thread_rng(); + let params = GroupParameters::new(constants::ATTRIBUTES_LEN); + let instance = WithdrawalReqInstance { + joined_commitment: G1Projective::random(&mut rng), + joined_commitment_hash: G1Projective::random(&mut rng), + private_attributes_commitments: vec![ + G1Projective::random(&mut rng), + G1Projective::random(&mut rng), + G1Projective::random(&mut rng), + ], + pk_user: PublicKeyUser { + pk: params.gen1() * params.random_scalar(), + }, + }; + + let instance_bytes = instance.to_bytes(); + let instance_p = WithdrawalReqInstance::from_bytes(&instance_bytes).unwrap(); + assert_eq!(instance, instance_p) + } + + #[test] + fn withdrawal_proof_construct_and_verify() { + let _rng = thread_rng(); + let params = GroupParameters::new(constants::ATTRIBUTES_LEN); + let sk = params.random_scalar(); + let pk_user = PublicKeyUser { + pk: params.gen1() * sk, + }; + let v = params.random_scalar(); + let t = params.random_scalar(); + let private_attributes = vec![sk, v, t]; + + let joined_commitment_opening = params.random_scalar(); + let joined_commitment = params.gen1() * joined_commitment_opening + + private_attributes + .iter() + .zip(params.gammas()) + .map(|(&m, gamma)| gamma * m) + .sum::(); + let joined_commitment_hash = hash_g1(joined_commitment.to_bytes()); + + let private_attributes_openings = params.n_random_scalars(private_attributes.len()); + let private_attributes_commitments = private_attributes_openings + .iter() + .zip(private_attributes.iter()) + .map(|(o_j, m_j)| params.gen1() * o_j + joined_commitment_hash * m_j) + .collect::>(); + + let instance = WithdrawalReqInstance { + joined_commitment, + joined_commitment_hash, + private_attributes_commitments, + pk_user, + }; + + let witness = WithdrawalReqWitness { + private_attributes, + joined_commitment_opening, + private_attributes_openings, + }; + let zk_proof = WithdrawalReqProof::construct(&instance, &witness); + assert!(zk_proof.verify(&instance)) + } +} diff --git a/common/nym_offline_compact_ecash/src/scheme/aggregation.rs b/common/nym_offline_compact_ecash/src/scheme/aggregation.rs new file mode 100644 index 0000000000..4d7c116121 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/scheme/aggregation.rs @@ -0,0 +1,162 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::common_types::{PartialSignature, Signature, SignatureShare, SignerIndex}; +use crate::error::{CompactEcashError, Result}; +use crate::helpers::{scalar_date, scalar_type}; +use crate::scheme::keygen::{SecretKeyUser, VerificationKeyAuth}; +use crate::scheme::withdrawal::RequestInfo; +use crate::scheme::{PartialWallet, Wallet, WalletSignatures}; +use crate::utils::{check_bilinear_pairing, perform_lagrangian_interpolation_at_origin}; +use crate::{ecash_group_parameters, Attribute}; +use bls12_381::{G2Prepared, G2Projective, Scalar}; +use core::iter::Sum; +use core::ops::Mul; +use group::Curve; +use itertools::Itertools; + +pub(crate) trait Aggregatable: Sized { + fn aggregate(aggregatable: &[Self], indices: Option<&[SignerIndex]>) -> Result; + + fn check_unique_indices(indices: &[SignerIndex]) -> bool { + // if aggregation is a threshold one, all indices should be unique + indices.iter().unique_by(|&index| index).count() == indices.len() + } +} + +impl Aggregatable for T +where + T: Sum, + for<'a> T: Sum<&'a T>, + for<'a> &'a T: Mul, +{ + fn aggregate(aggregatable: &[T], indices: Option<&[u64]>) -> Result { + if aggregatable.is_empty() { + return Err(CompactEcashError::AggregationEmptySet); + } + + if let Some(indices) = indices { + if !Self::check_unique_indices(indices) { + return Err(CompactEcashError::AggregationDuplicateIndices); + } + perform_lagrangian_interpolation_at_origin(indices, aggregatable) + } else { + // non-threshold + Ok(aggregatable.iter().sum()) + } + } +} + +impl Aggregatable for PartialSignature { + fn aggregate(sigs: &[PartialSignature], indices: Option<&[u64]>) -> Result { + let h = sigs + .first() + .ok_or(CompactEcashError::AggregationEmptySet)? + .sig1(); + + // TODO: is it possible to avoid this allocation? + let sigmas = sigs.iter().map(|sig| *sig.sig2()).collect::>(); + let aggr_sigma = Aggregatable::aggregate(&sigmas, indices)?; + + Ok(Signature { + h: *h, + s: aggr_sigma, + }) + } +} + +/// Ensures all provided verification keys were generated to verify the same number of attributes. +fn check_same_key_size(keys: &[VerificationKeyAuth]) -> bool { + keys.iter().map(|vk| vk.beta_g1.len()).all_equal() + && keys.iter().map(|vk| vk.beta_g2.len()).all_equal() +} + +pub fn aggregate_verification_keys( + keys: &[VerificationKeyAuth], + indices: Option<&[SignerIndex]>, +) -> Result { + if !check_same_key_size(keys) { + return Err(CompactEcashError::AggregationSizeMismatch); + } + Aggregatable::aggregate(keys, indices) +} + +pub fn aggregate_signature_shares( + verification_key: &VerificationKeyAuth, + attributes: &[Attribute], + shares: &[SignatureShare], +) -> Result { + let (signatures, indices): (Vec<_>, Vec<_>) = shares + .iter() + .map(|share| (*share.signature(), share.index())) + .unzip(); + + aggregate_signatures(verification_key, attributes, &signatures, Some(&indices)) +} + +pub fn aggregate_signatures( + verification_key: &VerificationKeyAuth, + attributes: &[Attribute], + signatures: &[PartialSignature], + indices: Option<&[SignerIndex]>, +) -> Result { + let params = ecash_group_parameters(); + // aggregate the signature + + let signature = match Aggregatable::aggregate(signatures, indices) { + Ok(res) => res, + Err(err) => return Err(err), + }; + + // Verify the signature + let tmp = attributes + .iter() + .zip(verification_key.beta_g2.iter()) + .map(|(attr, beta_i)| beta_i * attr) + .sum::(); + + if !check_bilinear_pairing( + &signature.h.to_affine(), + &G2Prepared::from((verification_key.alpha + tmp).to_affine()), + &signature.s.to_affine(), + params.prepared_miller_g2(), + ) { + return Err(CompactEcashError::AggregationVerification); + } + Ok(signature) +} + +pub fn aggregate_wallets( + verification_key: &VerificationKeyAuth, + sk_user: &SecretKeyUser, + wallets: &[PartialWallet], + req_info: &RequestInfo, +) -> Result { + // Aggregate partial wallets + let signature_shares: Vec = wallets + .iter() + .map(|wallet| SignatureShare::new(*wallet.signature(), wallet.index())) + .collect(); + + let attributes = vec![ + sk_user.sk, + *req_info.get_v(), + *req_info.get_expiration_date(), + *req_info.get_t_type(), + ]; + let aggregated_signature = + aggregate_signature_shares(verification_key, &attributes, &signature_shares)?; + + let expiration_date_timestamp = req_info.get_expiration_date(); + let t_type = req_info.get_t_type(); + + Ok(Wallet { + signatures: WalletSignatures { + sig: aggregated_signature, + v: *req_info.get_v(), + expiration_date_timestamp: scalar_date(expiration_date_timestamp), + t_type: scalar_type(t_type), + }, + tickets_spent: 0, + }) +} diff --git a/common/nym_offline_compact_ecash/src/scheme/coin_indices_signatures.rs b/common/nym_offline_compact_ecash/src/scheme/coin_indices_signatures.rs new file mode 100644 index 0000000000..b08ab368dc --- /dev/null +++ b/common/nym_offline_compact_ecash/src/scheme/coin_indices_signatures.rs @@ -0,0 +1,439 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::common_types::{Signature, SignerIndex}; +use crate::constants; +use crate::error::{CompactEcashError, Result}; +use crate::scheme::keygen::{SecretKeyAuth, VerificationKeyAuth}; +use crate::scheme::setup::Parameters; +use crate::utils::generate_lagrangian_coefficients_at_origin; +use crate::utils::{batch_verify_signatures, hash_g1}; +use bls12_381::{G1Projective, Scalar}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; + +pub type CoinIndexSignature = Signature; +pub type PartialCoinIndexSignature = CoinIndexSignature; + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct AnnotatedCoinIndexSignature { + pub signature: CoinIndexSignature, + pub index: u64, +} + +impl Borrow for AnnotatedCoinIndexSignature { + fn borrow(&self) -> &CoinIndexSignature { + &self.signature + } +} + +impl From for CoinIndexSignature { + fn from(value: AnnotatedCoinIndexSignature) -> Self { + value.signature + } +} + +pub struct CoinIndexSignatureShare +where + B: Borrow, +{ + pub index: SignerIndex, + pub key: VerificationKeyAuth, + pub signatures: Vec, +} + +/// Signs coin indices. +/// +/// This function takes cryptographic parameters, a global verification key, and a secret key of the signing authority, +/// and generates partial coin index signatures for a specified number of indices using a parallel fold operation. +/// +/// # Arguments +/// +/// * `params` - The cryptographic parameters used in the signing process. +/// * `vk` - The global verification key. +/// * `sk_auth` - The secret key associated with the individual signing authority. +/// +/// # Returns +/// +/// A vector containing partial coin index signatures. +pub fn sign_coin_indices( + params: &Parameters, + vk: &VerificationKeyAuth, + sk_auth: &SecretKeyAuth, +) -> Result> { + if sk_auth.ys.len() < 3 { + return Err(CompactEcashError::KeyTooShort); + } + let m1: Scalar = constants::TYPE_IDX; + let m2: Scalar = constants::TYPE_IDX; + + let vk_bytes = vk.to_bytes(); + let partial_s_exponent = sk_auth.x + sk_auth.ys[1] * m1 + sk_auth.ys[2] * m2; + + let sign_index = |index: u64| { + let m0: Scalar = Scalar::from(index); + // Compute the hash h + let mut concatenated_bytes = Vec::with_capacity(vk_bytes.len() + index.to_le_bytes().len()); + concatenated_bytes.extend_from_slice(&vk_bytes); + concatenated_bytes.extend_from_slice(&index.to_le_bytes()); + let h = hash_g1(concatenated_bytes); + + // Sign the attributes + let s_exponent = partial_s_exponent + sk_auth.ys[0] * m0; + + // Create the signature struct + let signature = PartialCoinIndexSignature { + h, + s: h * s_exponent, + }; + AnnotatedCoinIndexSignature { signature, index } + }; + + cfg_if::cfg_if! { + if #[cfg(feature = "par_signing")] { + use rayon::prelude::*; + + Ok((0..params.get_total_coins()) + .into_par_iter() + .map(sign_index) + .collect()) + } else { + Ok((0..params.get_total_coins()).map(sign_index).collect()) + } + } +} + +/// Verifies coin index signatures using parallel iterators. +/// +/// This function takes cryptographic parameters, verification keys, and a list of coin index +/// signatures. It verifies each signature's commitment hash and performs a bilinear pairing check. +/// +/// # Arguments +/// +/// * `params` - The cryptographic parameters used in the verification process. +/// * `vk` - The global verification key. +/// * `vk_auth` - The verification key associated with the authority which issued the partial signatures. +/// * `signatures` - A slice containing coin index signatures to be verified. +/// +/// # Returns +/// +/// Returns `Ok(())` if all signatures are valid, otherwise returns an error with a description +/// of the verification failure. +pub fn verify_coin_indices_signatures( + vk: &VerificationKeyAuth, + vk_auth: &VerificationKeyAuth, + signatures: &[B], +) -> Result<()> +where + B: Borrow, +{ + if vk_auth.beta_g2.len() < 3 { + return Err(CompactEcashError::KeyTooShort); + } + let m1: Scalar = constants::TYPE_IDX; + let m2: Scalar = constants::TYPE_IDX; + let partially_signed = vk_auth.alpha + vk_auth.beta_g2[1] * m1 + vk_auth.beta_g2[2] * m2; + let vk_bytes = vk.to_bytes(); + + let mut pairing_terms = Vec::with_capacity(signatures.len()); + + for (i, sig) in signatures.iter().enumerate() { + let l = i as u64; + let mut concatenated_bytes = Vec::with_capacity(vk_bytes.len() + l.to_le_bytes().len()); + concatenated_bytes.extend_from_slice(&vk_bytes); + concatenated_bytes.extend_from_slice(&l.to_le_bytes()); + + // Compute the hash h + let h = hash_g1(concatenated_bytes.clone()); + + let sig = *sig.borrow(); + // Check if the hash is matching + if sig.h != h { + return Err(CompactEcashError::CoinIndicesSignatureVerification); + } + + let m0 = Scalar::from(l); + // push elements for computing + // e(h1, X1) * e(s1, g2^-1) * ... * e(hi, Xi) * e(si, g2^-1) + // where + // h: H(vk, l) + // si: h^{xi + yi[0] * mi0 + yi[1] * m1 + yi[2] * m2} + // X: g2^{x + y[0] * mi0 + yi[1] * m1 + yi[2] * m2} + pairing_terms.push((sig, vk_auth.beta_g2[0] * m0 + partially_signed)); + } + + // computing all pairings in parallel using rayon makes it go from ~45ms to ~30ms, + // but given this function is called very infrequently, the possible interference up the stack is not worth it + if !batch_verify_signatures(pairing_terms.iter()) { + return Err(CompactEcashError::CoinIndicesSignatureVerification); + } + + Ok(()) +} + +fn _aggregate_indices_signatures( + params: &Parameters, + vk: &VerificationKeyAuth, + signatures_shares: &[CoinIndexSignatureShare], + validate_shares: bool, +) -> Result> +where + B: Borrow, +{ + // Check if all indices are unique + if signatures_shares + .iter() + .map(|share| share.index) + .unique() + .count() + != signatures_shares.len() + { + return Err(CompactEcashError::AggregationDuplicateIndices); + } + + // Evaluate at 0 the Lagrange basis polynomials k_i + let coefficients = generate_lagrangian_coefficients_at_origin( + &signatures_shares + .iter() + .map(|share| share.index) + .collect::>(), + ); + + // Verify that all signatures are valid + if validate_shares { + cfg_if::cfg_if! { + if #[cfg(feature = "par_verify")] { + use rayon::prelude::*; + + signatures_shares.par_iter().try_for_each(|share| { + verify_coin_indices_signatures(vk, &share.key, &share.signatures) + })?; + } else { + + signatures_shares.iter().try_for_each(|share| verify_coin_indices_signatures(vk, &share.key, &share.signatures))?; + } + } + } + + // Pre-allocate vectors + let mut aggregated_coin_signatures: Vec = + Vec::with_capacity(params.get_total_coins() as usize); + + let vk_bytes = vk.to_bytes(); + for l in 0..params.get_total_coins() { + // Compute the hash h + let mut concatenated_bytes = Vec::with_capacity(vk_bytes.len() + l.to_le_bytes().len()); + concatenated_bytes.extend_from_slice(&vk_bytes); + concatenated_bytes.extend_from_slice(&l.to_le_bytes()); + let h = hash_g1(concatenated_bytes); + + // Collect the partial signatures for the same coin index + let collected_at_l: Vec<_> = signatures_shares + .iter() + .filter_map(|share| share.signatures.get(l as usize)) + .collect(); + + // Aggregate partial signatures for each coin index + let aggr_s: G1Projective = coefficients + .iter() + .zip(collected_at_l.iter()) + .map(|(coeff, &sig)| sig.borrow().s * coeff) + .sum(); + let aggr_sig = CoinIndexSignature { h, s: aggr_s }; + aggregated_coin_signatures.push(aggr_sig); + } + verify_coin_indices_signatures(vk, vk, &aggregated_coin_signatures)?; + Ok(aggregated_coin_signatures) +} + +/// Aggregates and verifies partial coin index signatures. +/// +/// This function takes cryptographic parameters, a master verification key, and a list of tuples +/// containing indices, verification keys, and partial coin index signatures from different authorities. +/// It aggregates these partial signatures into a final set of coin index signatures, and verifying the +/// final aggregated signatures. +/// +/// # Arguments +/// +/// * `params` - The cryptographic parameters used in the aggregation process. +/// * `vk` - The master verification key against which the partial signatures are verified. +/// * `signatures` - A slice of tuples, where each tuple contains an index, a verification key, and +/// a vector of partial coin index signatures from a specific authority. +/// +/// # Returns +/// +/// Returns a vector of aggregated coin index signatures if the aggregation is successful. +/// Otherwise, returns an error describing the nature of the failure. +pub fn aggregate_indices_signatures( + params: &Parameters, + vk: &VerificationKeyAuth, + signatures_shares: &[CoinIndexSignatureShare], +) -> Result> +where + B: Borrow, +{ + _aggregate_indices_signatures(params, vk, signatures_shares, true) +} + +/// Perform aggregation and verification of partial coin index signatures with +/// an additional check ensuring correct ordering of provided shares +/// +/// It further annotates the result with index information +pub fn aggregate_annotated_indices_signatures( + params: &Parameters, + vk: &VerificationKeyAuth, + signature_shares: &[CoinIndexSignatureShare], +) -> Result> { + // it's sufficient to just verify the first share as if the rest of them don't match, + // the aggregation will fail anyway + let Some(share) = signature_shares.first() else { + return Ok(Vec::new()); + }; + + if share.signatures.len() != params.get_total_coins() as usize { + return Err(CompactEcashError::CoinIndicesSignatureVerification); + } + + for (idx, sig) in share.signatures.iter().enumerate() { + if idx != sig.index as usize { + return Err(CompactEcashError::CoinIndicesSignatureVerification); + } + } + + let aggregated = aggregate_indices_signatures(params, vk, signature_shares)?; + Ok(aggregated + .into_iter() + .enumerate() + .map(|(index, signature)| AnnotatedCoinIndexSignature { + signature, + index: index as u64, + }) + .collect()) +} + +/// An unchecked variant of `aggregate_indices_signatures` that does not perform +/// validation of intermediate signatures. +/// +/// It is expected the caller has already pre-validated them via manual calls to `verify_coin_indices_signatures` +pub fn unchecked_aggregate_indices_signatures( + params: &Parameters, + vk: &VerificationKeyAuth, + signatures_shares: &[CoinIndexSignatureShare], +) -> Result> { + _aggregate_indices_signatures(params, vk, signatures_shares, false) +} + +/// Generates parameters for the scheme setup. +/// +/// # Arguments +/// +/// * `total_coins` - it is the number of coins in a freshly generated wallet. It is the public parameter of the scheme. +/// +/// # Returns +/// +/// A `Parameters` struct containing group parameters, public key, the number of signatures (`total_coins`), +/// and a map of signatures for each index `l`. +/// + +#[cfg(test)] +mod tests { + use super::*; + use crate::scheme::aggregation::aggregate_verification_keys; + use crate::scheme::keygen::ttp_keygen; + + #[test] + fn test_sign_coins() { + let total_coins = 32; + let params = Parameters::new(total_coins); + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + + // Pick one authority to do the signing + let sk_i_auth = authorities_keypairs[0].secret_key(); + let vk_i_auth = authorities_keypairs[0].verification_key(); + + // list of verification keys of each authority + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + // the global master verification key + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap(); + + let partial_signatures = sign_coin_indices(¶ms, &verification_key, sk_i_auth).unwrap(); + assert!( + verify_coin_indices_signatures(&verification_key, &vk_i_auth, &partial_signatures) + .is_ok() + ); + } + + #[test] + fn test_sign_coins_fail() { + let total_coins = 32; + let params = Parameters::new(total_coins); + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + + // Pick one authority to do the signing + let sk_0_auth = authorities_keypairs[0].secret_key(); + let vk_1_auth = authorities_keypairs[1].verification_key(); + + // list of verification keys of each authority + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + // the global master verification key + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap(); + + let partial_signatures = sign_coin_indices(¶ms, &verification_key, sk_0_auth).unwrap(); + // Since we used a non matching verification key to verify the signature, the verification should fail + assert!( + verify_coin_indices_signatures(&verification_key, &vk_1_auth, &partial_signatures) + .is_err() + ); + } + + #[test] + fn test_aggregate_coin_indices_signatures() { + let total_coins = 32; + let params = Parameters::new(total_coins); + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + + // list of secret keys of each authority + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + // list of verification keys of each authority + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + // the global master verification key + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap(); + + // create the partial signatures from each authority + let partial_signatures: Vec> = secret_keys_authorities + .iter() + .map(|sk_auth| sign_coin_indices(¶ms, &verification_key, sk_auth).unwrap()) + .collect(); + + let combined_data = indices + .iter() + .zip(verification_keys_auth.iter().zip(partial_signatures.iter())) + .map(|(i, (vk, sigs))| CoinIndexSignatureShare { + index: *i, + key: vk.clone(), + signatures: sigs.clone(), + }) + .collect::>(); + + assert!(aggregate_indices_signatures(¶ms, &verification_key, &combined_data).is_ok()); + } +} diff --git a/common/nym_offline_compact_ecash/src/scheme/expiration_date_signatures.rs b/common/nym_offline_compact_ecash/src/scheme/expiration_date_signatures.rs new file mode 100644 index 0000000000..ca0078b012 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/scheme/expiration_date_signatures.rs @@ -0,0 +1,489 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::common_types::{Signature, SignerIndex}; +use crate::error::{CompactEcashError, Result}; +use crate::helpers::date_scalar; +use crate::scheme::keygen::{SecretKeyAuth, VerificationKeyAuth}; +use crate::utils::generate_lagrangian_coefficients_at_origin; +use crate::utils::{batch_verify_signatures, hash_g1}; +use crate::{constants, EncodedDate}; +use bls12_381::{G1Projective, Scalar}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; + +/// A structure representing an expiration date signature. +pub type ExpirationDateSignature = Signature; +pub type PartialExpirationDateSignature = ExpirationDateSignature; + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct AnnotatedExpirationDateSignature { + pub signature: ExpirationDateSignature, + pub expiration_timestamp: EncodedDate, + pub spending_timestamp: EncodedDate, +} + +impl Borrow for AnnotatedExpirationDateSignature { + fn borrow(&self) -> &ExpirationDateSignature { + &self.signature + } +} + +impl From for ExpirationDateSignature { + fn from(value: AnnotatedExpirationDateSignature) -> Self { + value.signature + } +} + +pub struct ExpirationDateSignatureShare +where + B: Borrow, +{ + pub index: SignerIndex, + pub key: VerificationKeyAuth, + pub signatures: Vec, +} + +/// Signs given expiration date for a specified validity period using the given secret key of a single authority. +/// +/// # Arguments +/// +/// * `params` - The cryptographic parameters used in the signing process. +/// * `sk_auth` - The secret key of the signing authority. +/// * `expiration_unix_timestamp` - The expiration date for which signatures will be generated (as unix timestamp). +/// +/// # Returns +/// +/// A vector containing partial signatures for each date within the validity period (i.e., +/// from expiration_date - CRED_VALIDITY_PERIOD till expiration_date. +/// +/// # Note +/// +/// This function is executed by a single singing authority and generates partial expiration date +/// signatures for a specified validity period. Each signature is created by combining cryptographic +/// attributes derived from the expiration date, and the resulting vector contains signatures for +/// each date within the defined validity period till expiration date. +/// The validity period is determined by the constant `CRED_VALIDITY_PERIOD` in the `constants` module. +pub fn sign_expiration_date( + sk_auth: &SecretKeyAuth, + expiration_unix_timestamp: EncodedDate, +) -> Result> { + if sk_auth.ys.len() < 3 { + return Err(CompactEcashError::KeyTooShort); + } + let m0: Scalar = date_scalar(expiration_unix_timestamp); + let m2: Scalar = constants::TYPE_EXP; + + let partial_s_exponent = sk_auth.x + sk_auth.ys[0] * m0 + sk_auth.ys[2] * m2; + + let sign_expiration = |offset: u32| { + // we produce tuples of (assuming CRED_VALIDITY_PERIOD_DAYS = 30): + // (expiration, expiration - 29) + // (expiration, expiration - 28) + // ... + // (expiration, expiration) + let spending_unix_timestamp = expiration_unix_timestamp + - ((constants::CRED_VALIDITY_PERIOD_DAYS - offset - 1) * constants::SECONDS_PER_DAY); + let m1: Scalar = date_scalar(spending_unix_timestamp); + // Compute the hash + let h = hash_g1([m0.to_bytes(), m1.to_bytes()].concat()); + // Sign the attributes by performing scalar-point multiplications and accumulating the result + let s_exponent = partial_s_exponent + sk_auth.ys[1] * m1; + + // Create the signature struct on the expiration date + let signature = PartialExpirationDateSignature { + h, + s: h * s_exponent, + }; + + AnnotatedExpirationDateSignature { + signature, + expiration_timestamp: expiration_unix_timestamp, + spending_timestamp: spending_unix_timestamp, + } + }; + + cfg_if::cfg_if! { + if #[cfg(feature = "par_signing")] { + use rayon::prelude::*; + + Ok((0..constants::CRED_VALIDITY_PERIOD_DAYS) + .into_par_iter() + .map(sign_expiration) + .collect()) + } else { + Ok((0..constants::CRED_VALIDITY_PERIOD_DAYS).map(sign_expiration).collect()) + } + } +} + +/// Verifies the expiration date signatures against the given verification key. +/// +/// This function iterates over the provided valid date signatures and verifies each one +/// against the provided verification key. It computes the hash and checks the correctness of the +/// signature using bilinear pairings. +/// +/// # Arguments +/// +/// * `vkey` - The verification key of the signing authority. +/// * `signatures` - The list of date signatures to be verified. +/// * `expiration_date` - The expiration date for which signatures are being issued (as unix timestamp). +/// +/// # Returns +/// +/// Returns `Ok(true)` if all signatures are verified successfully, otherwise returns an error +/// +pub fn verify_valid_dates_signatures( + vk: &VerificationKeyAuth, + signatures: &[B], + expiration_date: EncodedDate, +) -> Result<()> +where + B: Borrow, +{ + let m0: Scalar = date_scalar(expiration_date); + let m2: Scalar = constants::TYPE_EXP; + + let partially_signed = vk.alpha + vk.beta_g2[0] * m0 + vk.beta_g2[2] * m2; + let mut pairing_terms = Vec::with_capacity(signatures.len()); + + for (i, sig) in signatures.iter().enumerate() { + let l = i as u32; + let valid_date = expiration_date + - ((constants::CRED_VALIDITY_PERIOD_DAYS - l - 1) * constants::SECONDS_PER_DAY); + let m1: Scalar = date_scalar(valid_date); + + // Compute the hash + let h = hash_g1([m0.to_bytes(), m1.to_bytes()].concat()); + + let sig = *sig.borrow(); + // Check if the hash is matching + if sig.h != h { + return Err(CompactEcashError::ExpirationDateSignatureVerification); + } + + // let partially_signed_attributes = partially_signed + vk.beta_g2[1] * m1; + pairing_terms.push((sig, partially_signed + vk.beta_g2[1] * m1)); + } + + if !batch_verify_signatures(pairing_terms.iter()) { + return Err(CompactEcashError::ExpirationDateSignatureVerification); + } + Ok(()) +} + +/// Aggregates partial expiration date signatures into a list of aggregated expiration date signatures. +/// +/// # Arguments +/// +/// * `vk_auth` - The global verification key. +/// * `expiration_date` - The expiration date for which the signatures are being aggregated (as unix timestamp). +/// * `signatures_shares` - A list of tuples containing unique indices, verification keys, and partial expiration date signatures corresponding to the signing authorities. +/// +/// # Returns +/// +/// A `Result` containing a vector of `ExpirationDateSignature` if the aggregation is successful, +/// or an `Err` variant with a description of the encountered error. +/// +/// # Errors +/// +/// This function returns an error if there is a mismatch in the lengths of `signatures`. This occurs +/// when the number of tuples in `signatures` is not equal to the expected number of signing authorities. +/// Each tuple should contain a unique index, a verification key, and a list of partial signatures. +/// +/// It also returns an error if there are not enough unique indices. This happens when the number +/// of unique indices in the tuples is less than the total number of signing authorities. +/// +/// Additionally, an error is returned if the verification of the partial or aggregated signatures fails. +/// This can occur if the cryptographic verification process fails for any of the provided signatures. +/// +fn _aggregate_expiration_signatures( + vk: &VerificationKeyAuth, + expiration_date: EncodedDate, + signatures_shares: &[ExpirationDateSignatureShare], + validate_shares: bool, +) -> Result> +where + B: Borrow, +{ + // Check if all indices are unique + if signatures_shares + .iter() + .map(|share| share.index) + .unique() + .count() + != signatures_shares.len() + { + return Err(CompactEcashError::AggregationDuplicateIndices); + } + + // Evaluate at 0 the Lagrange basis polynomials k_i + let coefficients = generate_lagrangian_coefficients_at_origin( + &signatures_shares + .iter() + .map(|share| share.index) + .collect::>(), + ); + + // Verify that all signatures are valid + if validate_shares { + cfg_if::cfg_if! { + if #[cfg(feature = "par_verify")] { + use rayon::prelude::*; + + signatures_shares.par_iter().try_for_each(|share| { + verify_valid_dates_signatures(&share.key, &share.signatures, expiration_date) + })?; + } else { + signatures_shares.iter().try_for_each(|share| verify_valid_dates_signatures(&share.key, &share.signatures, expiration_date))?; + } + } + } + + // Pre-allocate vectors + let mut aggregated_date_signatures: Vec = + Vec::with_capacity(constants::CRED_VALIDITY_PERIOD_DAYS as usize); + + let m0: Scalar = date_scalar(expiration_date); + + for l in 0..constants::CRED_VALIDITY_PERIOD_DAYS { + let valid_date = expiration_date + - ((constants::CRED_VALIDITY_PERIOD_DAYS - l - 1) * constants::SECONDS_PER_DAY); + let m1: Scalar = date_scalar(valid_date); + // Compute the hash + let h = hash_g1([m0.to_bytes(), m1.to_bytes()].concat()); + + // Collect the partial signatures for the same valid date + let collected_at_l: Vec<_> = signatures_shares + .iter() + .filter_map(|share| share.signatures.get(l as usize)) + .collect(); + + // Aggregate partial signatures for each validity date + let aggr_s: G1Projective = coefficients + .iter() + .zip(collected_at_l.iter()) + .map(|(coeff, &sig)| sig.borrow().s * coeff) + .sum(); + let aggr_sig = ExpirationDateSignature { h, s: aggr_s }; + aggregated_date_signatures.push(aggr_sig); + } + verify_valid_dates_signatures(vk, &aggregated_date_signatures, expiration_date)?; + Ok(aggregated_date_signatures) +} + +/// Aggregates partial expiration date signatures into a list of aggregated expiration date signatures. +/// +/// # Arguments +/// +/// * `vk_auth` - The global verification key. +/// * `expiration_date` - The expiration date for which the signatures are being aggregated (as unix timestamp). +/// * `signatures_shares` - A list of tuples containing unique indices, verification keys, and partial expiration date signatures corresponding to the signing authorities. +/// +/// # Returns +/// +/// A `Result` containing a vector of `ExpirationDateSignature` if the aggregation is successful, +/// or an `Err` variant with a description of the encountered error. +/// +/// # Errors +/// +/// This function returns an error if there is a mismatch in the lengths of `signatures`. This occurs +/// when the number of tuples in `signatures` is not equal to the expected number of signing authorities. +/// Each tuple should contain a unique index, a verification key, and a list of partial signatures. +/// +/// It also returns an error if there are not enough unique indices. This happens when the number +/// of unique indices in the tuples is less than the total number of signing authorities. +/// +/// Additionally, an error is returned if the verification of the partial or aggregated signatures fails. +/// This can occur if the cryptographic verification process fails for any of the provided signatures. +/// +pub fn aggregate_expiration_signatures( + vk: &VerificationKeyAuth, + expiration_date: EncodedDate, + signatures_shares: &[ExpirationDateSignatureShare], +) -> Result> +where + B: Borrow, +{ + _aggregate_expiration_signatures(vk, expiration_date, signatures_shares, true) +} + +/// Perform aggregation and verification of partial expiration date signatures with +/// an additional check ensuring correct ordering of provided shares +/// +/// It further annotates the result with timestamp information +pub fn aggregate_annotated_expiration_signatures( + vk: &VerificationKeyAuth, + expiration_date: EncodedDate, + signatures_shares: &[ExpirationDateSignatureShare], +) -> Result> { + // it's sufficient to just verify the first share as if the rest of them don't match, + // the aggregation will fail anyway + let Some(share) = signatures_shares.first() else { + return Ok(Vec::new()); + }; + + if share.signatures.len() != constants::CRED_VALIDITY_PERIOD_DAYS as usize { + return Err(CompactEcashError::ExpirationDateSignatureVerification); + } + + for (i, sig) in share.signatures.iter().enumerate() { + if sig.expiration_timestamp != expiration_date { + return Err(CompactEcashError::ExpirationDateSignatureVerification); + } + + let l = i as u32; + let expected_spending = sig.expiration_timestamp + - ((constants::CRED_VALIDITY_PERIOD_DAYS - l - 1) * constants::SECONDS_PER_DAY); + + if sig.spending_timestamp != expected_spending { + return Err(CompactEcashError::ExpirationDateSignatureVerification); + } + } + + let aggregated = aggregate_expiration_signatures(vk, expiration_date, signatures_shares)?; + assert_eq!(aggregated.len(), share.signatures.len()); + + Ok(aggregated + .into_iter() + .zip(share.signatures.iter()) + .map(|(signature, sh)| AnnotatedExpirationDateSignature { + signature, + expiration_timestamp: sh.expiration_timestamp, + spending_timestamp: sh.spending_timestamp, + }) + .collect()) +} + +/// An unchecked variant of `aggregate_expiration_signatures` that does not perform +/// validation of intermediate signatures. +/// +/// It is expected the caller has already pre-validated them via manual calls to `verify_valid_dates_signatures` +pub fn unchecked_aggregate_expiration_signatures( + vk: &VerificationKeyAuth, + expiration_date: EncodedDate, + signatures_shares: &[ExpirationDateSignatureShare], +) -> Result> { + _aggregate_expiration_signatures(vk, expiration_date, signatures_shares, false) +} + +/// Finds the index corresponding to the given spend date based on the expiration date. +/// +/// This function calculates the index such that the following equality holds: +/// `spend_date = expiration_date - 30 + index` +/// This index is used to retrieve a corresponding signature. +/// +/// # Arguments +/// +/// * `spend_date` - The spend date for which to find the index. +/// * `expiration_date` - The expiration date used in the calculation. +/// +/// # Returns +/// +/// If a valid index is found, returns `Ok(index)`. If no valid index is found +/// (i.e., `spend_date` is earlier than `expiration_date - 30`), returns `Err(InvalidDateError)`. +/// +pub fn find_index(spend_date: EncodedDate, expiration_date: EncodedDate) -> Result { + let start_date = + expiration_date - ((constants::CRED_VALIDITY_PERIOD_DAYS - 1) * constants::SECONDS_PER_DAY); + + if spend_date >= start_date { + let index_a = ((spend_date - start_date) / constants::SECONDS_PER_DAY) as usize; + if index_a as u32 >= constants::CRED_VALIDITY_PERIOD_DAYS { + Err(CompactEcashError::SpendDateTooLate) + } else { + Ok(index_a) + } + } else { + Err(CompactEcashError::SpendDateTooEarly) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::scheme::aggregation::aggregate_verification_keys; + use crate::scheme::keygen::ttp_keygen; + + #[test] + fn test_find_index() { + let expiration_date = 1701993600; // Dec 8 2023 + for i in 0..constants::CRED_VALIDITY_PERIOD_DAYS { + let current_spend_date = expiration_date - i * 86400; + assert_eq!( + find_index(current_spend_date, expiration_date).unwrap(), + (constants::CRED_VALIDITY_PERIOD_DAYS - 1 - i) as usize + ) + } + + let late_spend_date = expiration_date + 86400; + assert!(find_index(late_spend_date, expiration_date).is_err()); + + let early_spend_date = expiration_date - (constants::CRED_VALIDITY_PERIOD_DAYS) * 86400; + assert!(find_index(early_spend_date, expiration_date).is_err()); + } + + #[test] + fn test_sign_expiration_date() { + let expiration_date = 1702050209; // Dec 8 2023 + + let authorities_keys = ttp_keygen(2, 3).unwrap(); + let sk_i_auth = authorities_keys[0].secret_key(); + let vk_i_auth = authorities_keys[0].verification_key(); + let partial_exp_sig = sign_expiration_date(sk_i_auth, expiration_date).unwrap(); + + assert!( + verify_valid_dates_signatures(&vk_i_auth, &partial_exp_sig, expiration_date).is_ok() + ); + } + + #[test] + fn test_aggregate_expiration_signatures() { + let expiration_date = 1702050209; // Dec 8 2023 + + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + // list of secret keys of each authority + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + // list of verification keys of each authority + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + // the global master verification key + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap(); + + let mut edt_partial_signatures: Vec> = + Vec::with_capacity(constants::CRED_VALIDITY_PERIOD_DAYS as usize); + for sk_auth in secret_keys_authorities.iter() { + let sign = sign_expiration_date(sk_auth, expiration_date).unwrap(); + edt_partial_signatures.push(sign); + } + + let combined_data = indices + .iter() + .zip( + verification_keys_auth + .iter() + .zip(edt_partial_signatures.iter()), + ) + .map(|(i, (vk, sigs))| ExpirationDateSignatureShare { + index: *i, + key: vk.clone(), + signatures: sigs.clone(), + }) + .collect::>(); + + assert!(aggregate_expiration_signatures( + &verification_key, + expiration_date, + &combined_data, + ) + .is_ok()); + } +} diff --git a/common/nym_offline_compact_ecash/src/scheme/identify.rs b/common/nym_offline_compact_ecash/src/scheme/identify.rs new file mode 100644 index 0000000000..38489e28c9 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/scheme/identify.rs @@ -0,0 +1,585 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::scheme::keygen::PublicKeyUser; +use crate::scheme::{compute_pay_info_hash, Payment}; + +use crate::PayInfo; + +#[derive(Debug, Eq, PartialEq)] +pub enum IdentifyResult { + NotADuplicatePayment, + DuplicatePayInfo(PayInfo), + DoubleSpendingPublicKeys(PublicKeyUser), +} + +pub fn identify( + payment1: &Payment, + payment2: &Payment, + pay_info1: PayInfo, + pay_info2: PayInfo, +) -> IdentifyResult { + let mut k = 0; + let mut j = 0; + for (id1, pay1_ss) in payment1.ss.iter().enumerate() { + for (id2, pay2_ss) in payment2.ss.iter().enumerate() { + if pay1_ss == pay2_ss { + k = id1; + j = id2; + break; + } + } + } + if payment1 + .ss + .iter() + .any(|pay1_ss| payment2.ss.contains(pay1_ss)) + { + if pay_info1 == pay_info2 { + IdentifyResult::DuplicatePayInfo(pay_info1) + } else { + let rr_k_payment1 = compute_pay_info_hash(&pay_info1, k as u64); + let rr_j_payment2 = compute_pay_info_hash(&pay_info2, j as u64); + let rr_diff = rr_k_payment1 - rr_j_payment2; + //SAFETY: `pay_info1` and `pay_info2` are different here, so rr_diff will not be zero, invert is then fine + let pk = (payment2.tt[j] * rr_k_payment1 - payment1.tt[k] * rr_j_payment2) + * rr_diff.invert().unwrap(); + let pk_user = PublicKeyUser { pk }; + IdentifyResult::DoubleSpendingPublicKeys(pk_user) + } + } else { + IdentifyResult::NotADuplicatePayment + } +} + +#[cfg(test)] +mod tests { + use crate::scheme::identify::{identify, IdentifyResult}; + use crate::scheme::keygen::{PublicKeyUser, SecretKeyAuth, SecretKeyUser}; + use crate::setup::Parameters; + use crate::tests::helpers::{ + generate_coin_indices_signatures, generate_expiration_date_signatures, + }; + use crate::{ + aggregate_verification_keys, aggregate_wallets, generate_keypair_user, issue, issue_verify, + ttp_keygen, withdrawal_request, PartialWallet, PayInfo, VerificationKeyAuth, + }; + use itertools::izip; + + #[test] + fn duplicate_payments_with_the_same_pay_info() { + let total_coins = 32; + let params = Parameters::new(total_coins); + // NOTE: Make sure that the date timestamp are calculated at 00:00:00!! + let spend_date = 1701907200; // Dec 07 2023 00:00:00 + let expiration_date = 1702166400; // Dec 10 2023 00:00:00 + let t_type = 1; + + let user_keypair = generate_keypair_user(); + + let (req, req_info) = + withdrawal_request(user_keypair.secret_key(), expiration_date, t_type).unwrap(); + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&[1, 2, 3])).unwrap(); + + // generate valid dates signatures + let dates_signatures = generate_expiration_date_signatures( + expiration_date, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + // generate coin indices signatures + let coin_indices_signatures = generate_coin_indices_signatures( + ¶ms, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + let mut wallet_blinded_signatures = Vec::new(); + for auth_keypair in authorities_keypairs { + let blind_signature = issue( + auth_keypair.secret_key(), + user_keypair.public_key(), + &req, + expiration_date, + t_type, + ); + wallet_blinded_signatures.push(blind_signature.unwrap()); + } + + let unblinded_wallet_shares: Vec = izip!( + wallet_blinded_signatures.iter(), + verification_keys_auth.iter() + ) + .enumerate() + .map(|(idx, (w, vk))| { + issue_verify(vk, user_keypair.secret_key(), w, &req_info, idx as u64 + 1).unwrap() + }) + .collect(); + + // Aggregate partial wallets + let mut aggr_wallet = aggregate_wallets( + &verification_key, + user_keypair.secret_key(), + &unblinded_wallet_shares, + &req_info, + ) + .unwrap(); + + // Let's try to spend some coins + let pay_info1 = PayInfo { + pay_info_bytes: [6u8; 72], + }; + let spend_vv = 1; + + let payment1 = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info1, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + assert!(payment1 + .spend_verify(&verification_key, &pay_info1, spend_date) + .is_ok()); + + let payment2 = payment1.clone(); + assert!(payment2 + .spend_verify(&verification_key, &pay_info1, spend_date) + .is_ok()); + + let identify_result = identify(&payment1, &payment2, pay_info1, pay_info1); + assert_eq!(identify_result, IdentifyResult::DuplicatePayInfo(pay_info1)); + } + + #[test] + fn ok_if_two_different_payments() { + let total_coins = 32; + let params = Parameters::new(total_coins); + // NOTE: Make sure that the date timestamp are calculated at 00:00:00!! + let spend_date = 1701907200; // Dec 07 2023 00:00:00 + let expiration_date = 1702166400; // Dec 10 2023 00:00:00 + let t_type = 1; + + let user_keypair = generate_keypair_user(); + + let (req, req_info) = + withdrawal_request(user_keypair.secret_key(), expiration_date, t_type).unwrap(); + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&[1, 2, 3])).unwrap(); + + // generate valid dates signatures + let dates_signatures = generate_expiration_date_signatures( + expiration_date, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + // generate coin indices signatures + let coin_indices_signatures = generate_coin_indices_signatures( + ¶ms, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + let mut wallet_blinded_signatures = Vec::new(); + for auth_keypair in authorities_keypairs { + let blind_signature = issue( + auth_keypair.secret_key(), + user_keypair.public_key(), + &req, + expiration_date, + t_type, + ); + wallet_blinded_signatures.push(blind_signature.unwrap()); + } + + let unblinded_wallet_shares: Vec = izip!( + wallet_blinded_signatures.iter(), + verification_keys_auth.iter() + ) + .enumerate() + .map(|(idx, (w, vk))| { + issue_verify(vk, user_keypair.secret_key(), w, &req_info, idx as u64 + 1).unwrap() + }) + .collect(); + + // Aggregate partial wallets + let mut aggr_wallet = aggregate_wallets( + &verification_key, + user_keypair.secret_key(), + &unblinded_wallet_shares, + &req_info, + ) + .unwrap(); + + // Let's try to spend some coins + let pay_info1 = PayInfo { + pay_info_bytes: [6u8; 72], + }; + let spend_vv = 1; + + let payment1 = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info1, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + assert!(payment1 + .spend_verify(&verification_key, &pay_info1, spend_date) + .is_ok()); + + let pay_info2 = PayInfo { + pay_info_bytes: [7u8; 72], + }; + let payment2 = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info2, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + assert!(payment2 + .spend_verify(&verification_key, &pay_info2, spend_date) + .is_ok()); + + let identify_result = identify(&payment1, &payment2, pay_info1, pay_info2); + assert_eq!(identify_result, IdentifyResult::NotADuplicatePayment); + } + + #[test] + fn two_payments_with_one_repeating_serial_number_but_different_pay_info() { + let total_coins = 32; + let params = Parameters::new(total_coins); + let grp = params.grp(); + // NOTE: Make sure that the date timestamp are calculated at 00:00:00!! + let spend_date = 1701907200; // Dec 07 2023 00:00:00 + let expiration_date = 1702166400; // Dec 10 2023 00:00:00 + let t_type = 1; + + let user_keypair = generate_keypair_user(); + + // GENERATE KEYS FOR OTHER USERS + let mut public_keys: Vec = Default::default(); + for _i in 0..50 { + let sk = grp.random_scalar(); + let sk_user = SecretKeyUser { sk }; + let pk_user = sk_user.public_key(); + public_keys.push(pk_user.clone()); + } + public_keys.push(user_keypair.public_key().clone()); + + let (req, req_info) = + withdrawal_request(user_keypair.secret_key(), expiration_date, t_type).unwrap(); + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&[1, 2, 3])).unwrap(); + + // generate valid dates signatures + let dates_signatures = generate_expiration_date_signatures( + expiration_date, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + // generate coin indices signatures + let coin_indices_signatures = generate_coin_indices_signatures( + ¶ms, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + let mut wallet_blinded_signatures = Vec::new(); + for auth_keypair in authorities_keypairs { + let blind_signature = issue( + auth_keypair.secret_key(), + user_keypair.public_key(), + &req, + expiration_date, + t_type, + ); + wallet_blinded_signatures.push(blind_signature.unwrap()); + } + + let unblinded_wallet_shares: Vec = izip!( + wallet_blinded_signatures.iter(), + verification_keys_auth.iter() + ) + .enumerate() + .map(|(idx, (w, vk))| { + issue_verify(vk, user_keypair.secret_key(), w, &req_info, idx as u64 + 1).unwrap() + }) + .collect(); + + // Aggregate partial wallets + let mut aggr_wallet = aggregate_wallets( + &verification_key, + user_keypair.secret_key(), + &unblinded_wallet_shares, + &req_info, + ) + .unwrap(); + + // Let's try to spend some coins + let pay_info1 = PayInfo { + pay_info_bytes: [6u8; 72], + }; + let spend_vv = 1; + + let payment1 = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info1, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + assert!(payment1 + .spend_verify(&verification_key, &pay_info1, spend_date) + .is_ok()); + + // let's reverse the spending counter in the wallet to create a double spending payment + aggr_wallet.tickets_spent -= 1; + + let pay_info2 = PayInfo { + pay_info_bytes: [7u8; 72], + }; + + let payment2 = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info2, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + assert!(payment2 + .spend_verify(&verification_key, &pay_info2, spend_date) + .is_ok()); + + let identify_result = identify(&payment1, &payment2, pay_info1, pay_info2); + assert_eq!( + identify_result, + IdentifyResult::DoubleSpendingPublicKeys(user_keypair.public_key()) + ); + } + + #[test] + fn two_payments_with_multiple_repeating_serial_numbers_but_different_pay_info() { + let total_coins = 32; + let params = Parameters::new(total_coins); + let grp = params.grp(); + // NOTE: Make sure that the date timestamp are calculated at 00:00:00!! + let spend_date = 1701907200; // Dec 07 2023 00:00:00 + let expiration_date = 1702166400; // Dec 10 2023 00:00:00 + let t_type = 1; + + let user_keypair = generate_keypair_user(); + + // GENERATE KEYS FOR OTHER USERS + let mut public_keys: Vec = Default::default(); + for _ in 0..50 { + let sk = grp.random_scalar(); + let sk_user = SecretKeyUser { sk }; + let pk_user = sk_user.public_key(); + public_keys.push(pk_user.clone()); + } + public_keys.push(user_keypair.public_key().clone()); + + let (req, req_info) = + withdrawal_request(user_keypair.secret_key(), expiration_date, t_type).unwrap(); + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&[1, 2, 3])).unwrap(); + + // generate valid dates signatures + let dates_signatures = generate_expiration_date_signatures( + expiration_date, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + // generate coin indices signatures + let coin_indices_signatures = generate_coin_indices_signatures( + ¶ms, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + let mut wallet_blinded_signatures = Vec::new(); + for auth_keypair in authorities_keypairs { + let blind_signature = issue( + auth_keypair.secret_key(), + user_keypair.public_key(), + &req, + expiration_date, + t_type, + ); + wallet_blinded_signatures.push(blind_signature.unwrap()); + } + + let unblinded_wallet_shares: Vec = izip!( + wallet_blinded_signatures.iter(), + verification_keys_auth.iter() + ) + .enumerate() + .map(|(idx, (w, vk))| { + issue_verify(vk, user_keypair.secret_key(), w, &req_info, idx as u64 + 1).unwrap() + }) + .collect(); + + // Aggregate partial wallets + let mut aggr_wallet = aggregate_wallets( + &verification_key, + user_keypair.secret_key(), + &unblinded_wallet_shares, + &req_info, + ) + .unwrap(); + + // Let's try to spend some coins + let pay_info1 = PayInfo { + pay_info_bytes: [6u8; 72], + }; + let spend_vv = 10; + + let payment1 = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info1, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + assert!(payment1 + .spend_verify(&verification_key, &pay_info1, spend_date) + .is_ok()); + + // let's reverse the spending counter in the wallet to create a double spending payment + aggr_wallet.tickets_spent -= 10; + + let pay_info2 = PayInfo { + pay_info_bytes: [7u8; 72], + }; + let payment2 = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info2, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + let identify_result = identify(&payment1, &payment2, pay_info1, pay_info2); + assert_eq!( + identify_result, + IdentifyResult::DoubleSpendingPublicKeys(user_keypair.public_key()) + ); + } +} diff --git a/common/nym_offline_compact_ecash/src/scheme/keygen.rs b/common/nym_offline_compact_ecash/src/scheme/keygen.rs new file mode 100644 index 0000000000..ade891b01f --- /dev/null +++ b/common/nym_offline_compact_ecash/src/scheme/keygen.rs @@ -0,0 +1,653 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::{CompactEcashError, Result}; +use crate::scheme::aggregation::aggregate_verification_keys; +use crate::scheme::SignerIndex; +use crate::traits::Bytable; +use crate::utils::{hash_to_scalar, Polynomial}; +use crate::utils::{ + try_deserialize_g1_projective, try_deserialize_g2_projective, try_deserialize_scalar, + try_deserialize_scalar_vec, +}; +use crate::{ecash_group_parameters, Base58}; +use bls12_381::{G1Projective, G2Projective, Scalar}; +use core::borrow::Borrow; +use core::iter::Sum; +use core::ops::{Add, Mul}; +use group::{Curve, GroupEncoding}; +use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; +use serde::{Deserialize, Serialize}; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +#[derive(Debug, PartialEq, Clone, Zeroize, ZeroizeOnDrop)] +pub struct SecretKeyAuth { + pub(crate) x: Scalar, + pub(crate) ys: Vec, +} + +impl PemStorableKey for SecretKeyAuth { + type Error = CompactEcashError; + + fn pem_type() -> &'static str { + "ECASH 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 SecretKeyAuth { + type Error = CompactEcashError; + + fn try_from(bytes: &[u8]) -> Result { + // There should be x and at least one y + if bytes.len() < 32 * 2 + 8 || (bytes.len() - 8) % 32 != 0 { + return Err(CompactEcashError::DeserializationInvalidLength { + actual: bytes.len(), + modulus_target: bytes.len() - 8, + target: 32 * 2 + 8, + modulus: 32, + object: "secret key".to_string(), + }); + } + + //SAFETY : slice to array conversion after a length check + #[allow(clippy::unwrap_used)] + let x_bytes: [u8; 32] = bytes[..32].try_into().unwrap(); + + #[allow(clippy::unwrap_used)] + let ys_len = u64::from_le_bytes(bytes[32..40].try_into().unwrap()); + let actual_ys_len = (bytes.len() - 40) / 32; + + if ys_len as usize != actual_ys_len { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "Secret_key ys".into(), + expected: ys_len as usize, + actual: actual_ys_len, + }); + } + + let x = try_deserialize_scalar(&x_bytes)?; + let ys = try_deserialize_scalar_vec(ys_len, &bytes[40..])?; + + Ok(SecretKeyAuth { x, ys }) + } +} + +impl SecretKeyAuth { + /// 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 } + } + + /// Extract the Scalar copy of the underlying secrets. + /// The caller of this function must exercise extreme care to not misuse the data and ensuring it gets zeroized + pub fn hazmat_to_raw(&self) -> (Scalar, Vec) { + (self.x, self.ys.clone()) + } + + pub fn size(&self) -> usize { + self.ys.len() + } + + pub fn verification_key(&self) -> VerificationKeyAuth { + let params = ecash_group_parameters(); + let g1 = params.gen1(); + let g2 = params.gen2(); + VerificationKeyAuth { + alpha: g2 * self.x, + beta_g1: self.ys.iter().map(|y| g1 * y).collect(), + beta_g2: self.ys.iter().map(|y| g2 * y).collect(), + } + } + + pub fn to_bytes(&self) -> Vec { + let ys_len = self.ys.len(); + let mut bytes = Vec::with_capacity(8 + (ys_len + 1) * 32); + bytes.extend_from_slice(&self.x.to_bytes()); + bytes.extend_from_slice(&ys_len.to_le_bytes()); + for y in self.ys.iter() { + bytes.extend_from_slice(&y.to_bytes()) + } + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + SecretKeyAuth::try_from(bytes) + } +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct VerificationKeyAuth { + pub(crate) alpha: G2Projective, + pub(crate) beta_g1: Vec, + pub(crate) beta_g2: Vec, +} + +impl PemStorableKey for VerificationKeyAuth { + type Error = CompactEcashError; + + fn pem_type() -> &'static str { + "ECASH 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 VerificationKeyAuth { + type Error = CompactEcashError; + + fn try_from(bytes: &[u8]) -> Result { + // There should be at least alpha, one betaG1 and one betaG2 and their length + if bytes.len() < 96 * 2 + 48 + 8 || (bytes.len() - 8 - 96) % (96 + 48) != 0 { + return Err(CompactEcashError::DeserializationInvalidLength { + actual: bytes.len(), + modulus_target: bytes.len() - 8 - 96, + target: 96 * 2 + 48 + 8, + modulus: 96 + 48, + object: "verification key".to_string(), + }); + } + + //SAFETY : slice to array conversion after a length check + #[allow(clippy::unwrap_used)] + let alpha_bytes: [u8; 96] = bytes[..96].try_into().unwrap(); + #[allow(clippy::unwrap_used)] + let betas_len = u64::from_le_bytes(bytes[96..104].try_into().unwrap()); + + let actual_betas_len = (bytes.len() - 104) / (96 + 48); + + if betas_len as usize != actual_betas_len { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "Verification_key betas".into(), + expected: betas_len as usize, + actual: actual_betas_len, + }); + } + + let alpha = try_deserialize_g2_projective(&alpha_bytes)?; + + let mut beta_g1 = Vec::with_capacity(betas_len as usize); + let mut beta_g1_end: u64 = 0; + for i in 0..betas_len { + let start = (104 + i * 48) as usize; + let end = start + 48; + //SAFETY : slice to array conversion after a length check + #[allow(clippy::unwrap_used)] + let beta_i_bytes = bytes[start..end].try_into().unwrap(); + let beta_i = try_deserialize_g1_projective(&beta_i_bytes)?; + + beta_g1_end = end as u64; + beta_g1.push(beta_i) + } + + let mut beta_g2 = Vec::with_capacity(betas_len as usize); + for i in 0..betas_len { + let start = (beta_g1_end + i * 96) as usize; + let end = start + 96; + //SAFETY : slice to array conversion after a length check + #[allow(clippy::unwrap_used)] + let beta_i_bytes = bytes[start..end].try_into().unwrap(); + let beta_i = try_deserialize_g2_projective(&beta_i_bytes)?; + + beta_g2.push(beta_i) + } + + Ok(VerificationKeyAuth { + alpha, + beta_g1, + beta_g2, + }) + } +} + +impl<'b> Add<&'b VerificationKeyAuth> for VerificationKeyAuth { + type Output = VerificationKeyAuth; + + #[inline] + fn add(self, rhs: &'b VerificationKeyAuth) -> VerificationKeyAuth { + // If you're trying to add two keys together that were created + // for different number of attributes, just panic as it's a + // nonsense operation. + assert_eq!( + self.beta_g1.len(), + rhs.beta_g1.len(), + "trying to add verification keys generated for different number of attributes [G1]" + ); + + assert_eq!( + self.beta_g2.len(), + rhs.beta_g2.len(), + "trying to add verification keys generated for different number of attributes [G2]" + ); + + assert_eq!( + self.beta_g1.len(), + self.beta_g2.len(), + "this key is incorrect - the number of elements G1 and G2 does not match" + ); + + assert_eq!( + rhs.beta_g1.len(), + rhs.beta_g2.len(), + "they key you want to add is incorrect - the number of elements G1 and G2 does not match" + ); + + VerificationKeyAuth { + alpha: self.alpha + rhs.alpha, + beta_g1: self + .beta_g1 + .iter() + .zip(rhs.beta_g1.iter()) + .map(|(self_beta_g1, rhs_beta_g1)| self_beta_g1 + rhs_beta_g1) + .collect(), + beta_g2: self + .beta_g2 + .iter() + .zip(rhs.beta_g2.iter()) + .map(|(self_beta_g2, rhs_beta_g2)| self_beta_g2 + rhs_beta_g2) + .collect(), + } + } +} + +impl<'a> Mul for &'a VerificationKeyAuth { + type Output = VerificationKeyAuth; + + #[inline] + fn mul(self, rhs: Scalar) -> Self::Output { + VerificationKeyAuth { + alpha: self.alpha * rhs, + beta_g1: self.beta_g1.iter().map(|b_i| b_i * rhs).collect(), + beta_g2: self.beta_g2.iter().map(|b_i| b_i * rhs).collect(), + } + } +} + +impl Sum for VerificationKeyAuth +where + T: Borrow, +{ + #[inline] + fn sum(iter: I) -> Self + where + I: Iterator, + { + let mut peekable = iter.peekable(); + let head_attributes = match peekable.peek() { + Some(head) => head.borrow().beta_g2.len(), + None => { + // TODO: this is a really weird edge case. You're trying to sum an EMPTY iterator + // of VerificationKey. So should it panic here or just return some nonsense value? + return VerificationKeyAuth::identity(0); + } + }; + + peekable.fold( + VerificationKeyAuth::identity(head_attributes), + |acc, item| acc + item.borrow(), + ) + } +} + +impl VerificationKeyAuth { + /// Create a (kinda) identity verification key using specified + /// number of 'beta' elements + pub(crate) fn identity(beta_size: usize) -> Self { + VerificationKeyAuth { + alpha: G2Projective::identity(), + beta_g1: vec![G1Projective::identity(); beta_size], + beta_g2: vec![G2Projective::identity(); beta_size], + } + } + + pub fn aggregate(sigs: &[Self], indices: Option<&[SignerIndex]>) -> Result { + aggregate_verification_keys(sigs, indices) + } + + pub fn alpha(&self) -> &G2Projective { + &self.alpha + } + + pub fn beta_g1(&self) -> &Vec { + &self.beta_g1 + } + + pub fn beta_g2(&self) -> &Vec { + &self.beta_g2 + } + + pub fn to_bytes(&self) -> Vec { + let beta_g1_len = self.beta_g1.len(); + let beta_g2_len = self.beta_g2.len(); + let mut bytes = Vec::with_capacity(96 + 8 + beta_g1_len * 48 + beta_g2_len * 96); + + bytes.extend_from_slice(&self.alpha.to_affine().to_compressed()); + + bytes.extend_from_slice(&beta_g1_len.to_le_bytes()); + + for beta_g1 in self.beta_g1.iter() { + bytes.extend_from_slice(&beta_g1.to_affine().to_compressed()) + } + + for beta_g2 in self.beta_g2.iter() { + bytes.extend_from_slice(&beta_g2.to_affine().to_compressed()) + } + + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + VerificationKeyAuth::try_from(bytes) + } +} + +impl Bytable for VerificationKeyAuth { + fn to_byte_vec(&self) -> Vec { + self.to_bytes().to_vec() + } + + fn try_from_byte_slice(slice: &[u8]) -> std::result::Result { + Self::from_bytes(slice) + } +} + +impl Base58 for VerificationKeyAuth {} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)] +pub struct SecretKeyUser { + pub(crate) sk: Scalar, +} + +impl SecretKeyUser { + pub fn public_key(&self) -> PublicKeyUser { + PublicKeyUser { + pk: ecash_group_parameters().gen1() * self.sk, + } + } + + pub fn to_bytes(&self) -> Vec { + self.sk.to_bytes().to_vec() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let sk = Scalar::try_from_byte_slice(bytes)?; + Ok(SecretKeyUser { sk }) + } +} + +impl Bytable for SecretKeyUser { + fn to_byte_vec(&self) -> Vec { + self.to_bytes().to_vec() + } + + fn try_from_byte_slice(slice: &[u8]) -> std::result::Result { + Self::from_bytes(slice) + } +} + +impl Base58 for SecretKeyUser {} + +#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +pub struct PublicKeyUser { + pub(crate) pk: G1Projective, +} + +impl PublicKeyUser { + pub fn to_base58_string(&self) -> String { + bs58::encode(&self.pk.to_bytes()).into_string() + } + + pub fn from_base58_string>(val: I) -> Result { + let bytes = bs58::decode(val).into_vec()?; + Self::from_bytes(&bytes) + } + + pub fn to_bytes(&self) -> Vec { + self.pk.to_affine().to_compressed().to_vec() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != 48 { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "PublicKeyUser".into(), + expected: 48, + actual: bytes.len(), + }); + } + //SAFETY : slice to array conversion after a length check + #[allow(clippy::unwrap_used)] + let pk_bytes: &[u8; 48] = bytes[..48].try_into().unwrap(); + let pk = try_deserialize_g1_projective(pk_bytes)?; + Ok(PublicKeyUser { pk }) + } +} + +impl Bytable for PublicKeyUser { + fn to_byte_vec(&self) -> Vec { + self.to_bytes().to_vec() + } + + fn try_from_byte_slice(slice: &[u8]) -> Result { + Self::from_bytes(slice) + } +} + +impl Base58 for PublicKeyUser {} + +#[derive(Debug, Zeroize, ZeroizeOnDrop)] +pub struct KeyPairAuth { + secret_key: SecretKeyAuth, + #[zeroize(skip)] + verification_key: VerificationKeyAuth, + /// Optional index value specifying polynomial point used during threshold key generation. + pub index: Option, +} + +impl From for KeyPairAuth { + fn from(secret_key: SecretKeyAuth) -> Self { + KeyPairAuth { + verification_key: secret_key.verification_key(), + secret_key, + index: None, + } + } +} + +impl PemStorableKeyPair for KeyPairAuth { + type PrivatePemKey = SecretKeyAuth; + type PublicPemKey = VerificationKeyAuth; + + 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 KeyPairAuth { + pub fn new( + sk: SecretKeyAuth, + vk: VerificationKeyAuth, + index: Option, + ) -> KeyPairAuth { + KeyPairAuth { + secret_key: sk, + verification_key: vk, + index, + } + } + + pub fn from_keys(secret_key: SecretKeyAuth, verification_key: VerificationKeyAuth) -> Self { + Self { + secret_key, + verification_key, + index: None, + } + } + + pub fn secret_key(&self) -> &SecretKeyAuth { + &self.secret_key + } + + pub fn verification_key(&self) -> VerificationKeyAuth { + self.verification_key.clone() + } + + pub fn verification_key_ref(&self) -> &VerificationKeyAuth { + &self.verification_key + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct KeyPairUser { + secret_key: SecretKeyUser, + public_key: PublicKeyUser, +} + +impl KeyPairUser { + pub fn secret_key(&self) -> &SecretKeyUser { + &self.secret_key + } + + pub fn public_key(&self) -> PublicKeyUser { + self.public_key.clone() + } + + pub fn to_bytes(&self) -> Vec { + [self.secret_key.to_bytes(), self.public_key.to_bytes()].concat() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != 32 + 48 { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "KeyPairUser".into(), + expected: 80, + actual: bytes.len(), + }); + } + let sk = SecretKeyUser::from_bytes(&bytes[..32])?; + let pk = PublicKeyUser::from_bytes(&bytes[32..32 + 48])?; + Ok(KeyPairUser { + secret_key: sk, + public_key: pk, + }) + } +} + +pub fn generate_keypair_user() -> KeyPairUser { + let params = ecash_group_parameters(); + let sk_user = SecretKeyUser { + sk: params.random_scalar(), + }; + let pk_user = PublicKeyUser { + pk: params.gen1() * sk_user.sk, + }; + + KeyPairUser { + secret_key: sk_user, + public_key: pk_user, + } +} + +pub fn generate_keypair_user_from_seed>(seed: M) -> KeyPairUser { + let params = ecash_group_parameters(); + let sk_user = SecretKeyUser { + sk: hash_to_scalar(seed), + }; + let pk_user = PublicKeyUser { + pk: params.gen1() * sk_user.sk, + }; + + KeyPairUser { + secret_key: sk_user, + public_key: pk_user, + } +} + +pub fn ttp_keygen(threshold: u64, num_authorities: u64) -> Result> { + let params = ecash_group_parameters(); + if threshold == 0 { + return Err(CompactEcashError::KeygenParameters); + } + + if threshold > num_authorities { + return Err(CompactEcashError::KeygenParameters); + } + + let attributes = params.gammas().len(); + + // generate polynomials + let v = Polynomial::new_random(params, threshold - 1); + let ws = (0..attributes + 1) + .map(|_| Polynomial::new_random(params, threshold - 1)) + .collect::>(); + + // TODO: potentially if we had some known authority identifier we could use that instead + // of the increasing (1,2,3,...) sequence + let polynomial_indices = (1..=num_authorities).collect::>(); + + // generate polynomial shares + let x = polynomial_indices + .iter() + .map(|&id| v.evaluate(&Scalar::from(id))); + let ys = polynomial_indices.iter().map(|&id| { + ws.iter() + .map(|w| w.evaluate(&Scalar::from(id))) + .collect::>() + }); + + // finally set the keys + let secret_keys = x.zip(ys).map(|(x, ys)| SecretKeyAuth { x, ys }); + + let keypairs = secret_keys + .zip(polynomial_indices.iter()) + .map(|(secret_key, index)| { + let verification_key = secret_key.verification_key(); + KeyPairAuth { + secret_key, + verification_key, + index: Some(*index), + } + }) + .collect(); + + Ok(keypairs) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_zeroize_on_drop() {} + + fn assert_zeroize() {} + + #[test] + fn secret_key_is_zeroized() { + assert_zeroize::(); + assert_zeroize_on_drop::(); + + assert_zeroize::(); + assert_zeroize_on_drop::(); + } +} diff --git a/common/nym_offline_compact_ecash/src/scheme/mod.rs b/common/nym_offline_compact_ecash/src/scheme/mod.rs new file mode 100644 index 0000000000..050a9e7eb6 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/scheme/mod.rs @@ -0,0 +1,999 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::common_types::{Signature, SignerIndex}; +use crate::error::{CompactEcashError, Result}; +use crate::helpers::{date_scalar, type_scalar}; +use crate::proofs::proof_spend::{SpendInstance, SpendProof, SpendWitness}; +use crate::scheme::coin_indices_signatures::CoinIndexSignature; +use crate::scheme::expiration_date_signatures::{find_index, ExpirationDateSignature}; +use crate::scheme::keygen::{SecretKeyUser, VerificationKeyAuth}; +use crate::scheme::setup::{GroupParameters, Parameters}; +use crate::traits::Bytable; +use crate::utils::{ + batch_verify_signatures, check_bilinear_pairing, hash_to_scalar, try_deserialize_scalar, +}; +use crate::{constants, ecash_group_parameters}; +use crate::{Base58, EncodedDate, EncodedTicketType}; +use bls12_381::{G1Projective, G2Prepared, G2Projective, Scalar}; +use group::Curve; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::borrow::Borrow; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +pub mod aggregation; +pub mod coin_indices_signatures; +pub mod expiration_date_signatures; +pub mod identify; +pub mod keygen; +pub mod setup; +pub mod withdrawal; + +/// The struct represents a partial wallet with essential components for a payment transaction. +/// +/// A `PartialWallet` includes a Pointcheval-Sanders signature (`sig`), +/// a scalar value (`v`) representing the wallet's secret, an optional +/// `SignerIndex` (`idx`) indicating the signer's index, and an expiration date (`expiration_date`). +/// +#[derive(Debug, Clone, PartialEq, Zeroize, ZeroizeOnDrop)] +pub struct PartialWallet { + #[zeroize(skip)] + sig: Signature, + v: Scalar, + idx: SignerIndex, + expiration_date: Scalar, + t_type: Scalar, +} + +impl PartialWallet { + pub fn signature(&self) -> &Signature { + &self.sig + } + + pub fn index(&self) -> SignerIndex { + self.idx + } + pub fn expiration_date(&self) -> Scalar { + self.expiration_date + } + pub fn t_type(&self) -> Scalar { + self.t_type + } + + /// Converts the `PartialWallet` to a fixed-size byte array. + /// + /// The resulting byte array has a length of 200 bytes and contains serialized + /// representations of the `Signature` (`sig`), scalar value (`v`), + /// expiration date (`expiration_date`), and `idx` fields of the `PartialWallet` struct. + /// + /// # Returns + /// + /// A fixed-size byte array (`[u8; 200]`) representing the serialized form of the `PartialWallet`. + /// + pub fn to_bytes(&self) -> [u8; 200] { + let mut bytes = [0u8; 200]; + bytes[0..96].copy_from_slice(&self.sig.to_bytes()); + bytes[96..128].copy_from_slice(&self.v.to_bytes()); + bytes[128..160].copy_from_slice(&self.expiration_date.to_bytes()); + bytes[160..192].copy_from_slice(&self.t_type.to_bytes()); + bytes[192..200].copy_from_slice(&self.idx.to_le_bytes()); + bytes + } + + /// Convert a byte slice into a `PartialWallet` instance. + /// + /// This function performs deserialization on the provided byte slice, which + /// represent a serialized `PartialWallet`. + /// + /// # Arguments + /// + /// * `bytes` - A reference to the byte slice to be deserialized. + /// + /// # Returns + /// + /// A `Result` containing the deserialized `PartialWallet` if successful, or a + /// `CompactEcashError` indicating the reason for failure. + pub fn from_bytes(bytes: &[u8]) -> Result { + const SIGNATURE_BYTES: usize = 96; + const V_BYTES: usize = 32; + const EXPIRATION_DATE_BYTES: usize = 32; + const T_TYPE_BYTES: usize = 32; + const IDX_BYTES: usize = 8; + const EXPECTED_LENGTH: usize = + SIGNATURE_BYTES + V_BYTES + EXPIRATION_DATE_BYTES + T_TYPE_BYTES + IDX_BYTES; + + if bytes.len() != EXPECTED_LENGTH { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "PartialWallet".into(), + expected: EXPECTED_LENGTH, + actual: bytes.len(), + }); + } + + let mut j = 0; + + let sig = Signature::try_from(&bytes[j..j + SIGNATURE_BYTES])?; + j += SIGNATURE_BYTES; + + //SAFETY: slice to array after length check + #[allow(clippy::unwrap_used)] + let v_bytes = bytes[j..j + V_BYTES].try_into().unwrap(); + let v = try_deserialize_scalar(v_bytes)?; + j += V_BYTES; + + //SAFETY: slice to array after length check + #[allow(clippy::unwrap_used)] + let expiration_date_bytes = bytes[j..j + EXPIRATION_DATE_BYTES].try_into().unwrap(); + let expiration_date = try_deserialize_scalar(expiration_date_bytes)?; + j += EXPIRATION_DATE_BYTES; + //SAFETY: slice to array after length check + #[allow(clippy::unwrap_used)] + let t_type_bytes = bytes[j..j + T_TYPE_BYTES].try_into().unwrap(); + let t_type = try_deserialize_scalar(t_type_bytes)?; + j += T_TYPE_BYTES; + + //SAFETY: slice to array after length check + #[allow(clippy::unwrap_used)] + let idx_bytes = bytes[j..].try_into().unwrap(); + let idx = u64::from_le_bytes(idx_bytes); + + Ok(PartialWallet { + sig, + v, + idx, + expiration_date, + t_type, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Zeroize, Serialize, Deserialize)] +pub struct Wallet { + /// The cryptographic materials required for producing spending proofs and payments. + signatures: WalletSignatures, + + /// Also known as `l` parameter in the paper + tickets_spent: u64, +} + +impl Wallet { + pub fn new(signatures: WalletSignatures, tickets_spent: u64) -> Self { + Wallet { + signatures, + tickets_spent, + } + } + + pub fn into_wallet_signatures(self) -> WalletSignatures { + self.into() + } + + pub fn to_bytes(&self) -> [u8; WalletSignatures::SERIALISED_SIZE + 8] { + let mut bytes = [0u8; WalletSignatures::SERIALISED_SIZE + 8]; + bytes[0..WalletSignatures::SERIALISED_SIZE].copy_from_slice(&self.signatures.to_bytes()); + bytes[WalletSignatures::SERIALISED_SIZE..] + .copy_from_slice(&self.tickets_spent.to_be_bytes()); + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != WalletSignatures::SERIALISED_SIZE + 8 { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "Wallet".into(), + expected: WalletSignatures::SERIALISED_SIZE + 8, + actual: bytes.len(), + }); + } + + //SAFETY : slice to array conversions after a length check + #[allow(clippy::unwrap_used)] + let tickets_bytes = bytes[WalletSignatures::SERIALISED_SIZE..] + .try_into() + .unwrap(); + + let signatures = WalletSignatures::from_bytes(&bytes[..WalletSignatures::SERIALISED_SIZE])?; + let tickets_spent = u64::from_be_bytes(tickets_bytes); + + Ok(Wallet { + signatures, + tickets_spent, + }) + } + + pub fn ensure_allowance( + params: &Parameters, + tickets_spent: u64, + spend_value: u64, + ) -> Result<()> { + if tickets_spent + spend_value > params.get_total_coins() { + Err(CompactEcashError::SpendExceedsAllowance { + spending: spend_value, + remaining: params.get_total_coins() - tickets_spent, + }) + } else { + Ok(()) + } + } + + pub fn check_remaining_allowance(&self, params: &Parameters, spend_value: u64) -> Result<()> { + Self::ensure_allowance(params, self.tickets_spent, spend_value) + } + + #[allow(clippy::too_many_arguments)] + pub fn spend( + &mut self, + params: &Parameters, + verification_key: &VerificationKeyAuth, + sk_user: &SecretKeyUser, + pay_info: &PayInfo, + spend_value: u64, + valid_dates_signatures: &[ExpirationDateSignature], + coin_indices_signatures: &[CoinIndexSignature], + spend_date_timestamp: EncodedDate, + ) -> Result { + self.check_remaining_allowance(params, spend_value)?; + + // produce payment + let payment = self.signatures.spend( + params, + verification_key, + sk_user, + pay_info, + self.tickets_spent, + spend_value, + valid_dates_signatures, + coin_indices_signatures, + spend_date_timestamp, + )?; + + // update the ticket counter + self.tickets_spent += spend_value; + Ok(payment) + } +} + +impl From for WalletSignatures { + fn from(value: Wallet) -> Self { + value.signatures + } +} + +/// The struct represents a wallet with essential components for a payment transaction. +/// +/// A `Wallet` includes a Pointcheval-Sanders signature (`sig`), +/// a scalar value (`v`) representing the wallet's secret, an optional +/// an expiration date (`expiration_date`) +/// and an u64 ('l') indicating the total number of spent coins. +/// +#[derive(Debug, Clone, PartialEq, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] +pub struct WalletSignatures { + #[zeroize(skip)] + sig: Signature, + v: Scalar, + expiration_date_timestamp: EncodedDate, + t_type: EncodedTicketType, +} + +impl WalletSignatures { + pub fn with_tickets_spent(self, tickets_spent: u64) -> Wallet { + Wallet { + signatures: self, + tickets_spent, + } + } + + pub fn new_wallet(self) -> Wallet { + self.with_tickets_spent(0) + } + + pub fn encoded_expiration_date(&self) -> Scalar { + date_scalar(self.expiration_date_timestamp) + } +} + +/// Computes the hash of payment information concatenated with a numeric value. +/// +/// This function takes a `PayInfo` structure and a numeric value `k`, and +/// concatenates the serialized `payinfo` field of `PayInfo` with the little-endian +/// byte representation of `k`. The resulting byte sequence is then hashed to produce +/// a scalar value using the `hash_to_scalar` function. +/// +/// # Arguments +/// +/// * `pay_info` - A reference to the `PayInfo` structure containing payment information. +/// * `k` - A numeric value used in the hash computation. +/// +/// # Returns +/// +/// A `Scalar` value representing the hash of the concatenated byte sequence. +/// +pub fn compute_pay_info_hash(pay_info: &PayInfo, k: u64) -> Scalar { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&pay_info.pay_info_bytes); + bytes.extend_from_slice(&k.to_le_bytes()); + hash_to_scalar(bytes) +} + +impl WalletSignatures { + // signature size (96) + secret size (32) + expiration size (4) + t_type (1) + pub const SERIALISED_SIZE: usize = 133; + + pub fn signature(&self) -> &Signature { + &self.sig + } + + /// Converts the `WalletSignatures` to a fixed-size byte array. + /// + /// The resulting byte array has a length of 168 bytes and contains serialized + /// representations of the `Signature` (`sig`), scalar value (`v`), and + /// expiration date (`expiration_date`) fields of the `WalletSignatures` struct. + /// + /// # Returns + /// + /// A fixed-size byte array (`[u8; 136]`) representing the serialized form of the `Wallet`. + /// + pub fn to_bytes(&self) -> [u8; Self::SERIALISED_SIZE] { + let mut bytes = [0u8; Self::SERIALISED_SIZE]; + bytes[0..96].copy_from_slice(&self.sig.to_bytes()); + bytes[96..128].copy_from_slice(&self.v.to_bytes()); + bytes[128..132].copy_from_slice(&self.expiration_date_timestamp.to_be_bytes()); + bytes[132] = self.t_type; + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != Self::SERIALISED_SIZE { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "WalletSignatures".into(), + expected: Self::SERIALISED_SIZE, + actual: bytes.len(), + }); + } + //SAFETY : slice to array conversions after a length check + #[allow(clippy::unwrap_used)] + let sig_bytes: &[u8; 96] = &bytes[..96].try_into().unwrap(); + + #[allow(clippy::unwrap_used)] + let v_bytes: &[u8; 32] = &bytes[96..128].try_into().unwrap(); + + #[allow(clippy::unwrap_used)] + let expiration_date_bytes = bytes[128..132].try_into().unwrap(); + + let sig = Signature::try_from(sig_bytes.as_slice())?; + let v = Scalar::from_bytes(v_bytes).unwrap(); + let expiration_date_timestamp = EncodedDate::from_be_bytes(expiration_date_bytes); + let t_type = bytes[132]; + + Ok(WalletSignatures { + sig, + v, + expiration_date_timestamp, + t_type, + }) + } + + /// Performs a spending operation with the given parameters, updating the wallet and generating a payment. + /// + /// # Arguments + /// + /// * `verification_key` - The global verification key. + /// * `sk_user` - The secret key of the user who wants to spend from their wallet. + /// * `pay_info` - Unique information related to the payment. + /// * `current_tickets_spent` - The total number of tickets already spent in the associated wallet. + /// * `spend_value` - The amount to spend from the wallet. + /// * `valid_dates_signatures` - A list of **SORTED** signatures on valid dates during which we can spend from the wallet. + /// * `coin_indices_signatures` - A list of **SORTED** signatures on coin indices. + /// * `spend_date` - The date on which the spending occurs, expressed as unix timestamp. + /// + /// # Returns + /// + /// A tuple containing the generated payment and a reference to the updated wallet, or an error. + #[allow(clippy::too_many_arguments)] + pub fn spend( + &self, + params: &Parameters, + verification_key: &VerificationKeyAuth, + sk_user: &SecretKeyUser, + pay_info: &PayInfo, + current_tickets_spent: u64, + spend_value: u64, + valid_dates_signatures: &[BE], + coin_indices_signatures: &[BI], + spend_date_timestamp: EncodedDate, + ) -> Result + where + BI: Borrow, + BE: Borrow, + { + // Extract group parameters + let grp_params = params.grp(); + + if verification_key.beta_g2.is_empty() { + return Err(CompactEcashError::VerificationKeyTooShort); + } + + if valid_dates_signatures.len() != constants::CRED_VALIDITY_PERIOD_DAYS as usize { + return Err(CompactEcashError::InsufficientNumberOfExpirationSignatures); + } + + if coin_indices_signatures.len() != params.get_total_coins() as usize { + return Err(CompactEcashError::InsufficientNumberOfIndexSignatures); + } + + Wallet::ensure_allowance(params, current_tickets_spent, spend_value)?; + + // Wallet attributes needed for spending + let attributes = [&sk_user.sk, &self.v, &self.encoded_expiration_date()]; + + // Randomize wallet signature + let (signature_prime, sign_blinding_factor) = self.signature().blind_and_randomise(); + + // compute kappa (i.e., blinded attributes for show) to prove possession of the wallet signature + let kappa = compute_kappa( + grp_params, + verification_key, + &attributes, + sign_blinding_factor, + ); + + // Randomise the expiration date signature for the date when we want to perform the spending, and compute kappa_e to prove possession of + // the expiration signature + let date_signature_index = + find_index(spend_date_timestamp, self.expiration_date_timestamp)?; + + //SAFETY : find_index eiter returns a valid index or an error. The unwrap is therefore fine + #[allow(clippy::unwrap_used)] + let date_signature = valid_dates_signatures + .get(date_signature_index) + .unwrap() + .borrow(); + let (date_signature_prime, date_sign_blinding_factor) = + date_signature.blind_and_randomise(); + // compute kappa_e to prove possession of the expiration signature + //SAFETY: we checked that verification beta_g2 isn't empty + #[allow(clippy::unwrap_used)] + let kappa_e: G2Projective = grp_params.gen2() * date_sign_blinding_factor + + verification_key.alpha + + verification_key.beta_g2.first().unwrap() * self.encoded_expiration_date(); + + // pick random openings o_c and compute commitments C to v (wallet secret) + let o_c = grp_params.random_scalar(); + //SAFETY: grp_params is static with length 3 + #[allow(clippy::unwrap_used)] + let cc = grp_params.gen1() * o_c + grp_params.gamma_idx(1).unwrap() * self.v; + + let mut aa: Vec = Default::default(); + let mut ss: Vec = Default::default(); + let mut tt: Vec = Default::default(); + let mut rr: Vec = Default::default(); + let mut o_a: Vec = Default::default(); + let mut o_mu: Vec = Default::default(); + let mut mu: Vec = Default::default(); + let r_k_vec: Vec = Default::default(); + let mut kappa_k_vec: Vec = Default::default(); + let mut lk_vec: Vec = Default::default(); + + let mut coin_indices_signatures_prime: Vec = Default::default(); + for k in 0..spend_value { + let lk = current_tickets_spent + k; + lk_vec.push(Scalar::from(lk)); + + // compute hashes R_k = H(payinfo, k) + let rr_k = compute_pay_info_hash(pay_info, k); + rr.push(rr_k); + + let o_a_k = grp_params.random_scalar(); + o_a.push(o_a_k); + //SAFETY: grp_params is static with length 3 + #[allow(clippy::unwrap_used)] + let aa_k = + grp_params.gen1() * o_a_k + grp_params.gamma_idx(1).unwrap() * Scalar::from(lk); + aa.push(aa_k); + + // compute the serial numbers + let ss_k = pseudorandom_f_delta_v(grp_params, &self.v, lk)?; + ss.push(ss_k); + // compute the identification tags + let tt_k = grp_params.gen1() * sk_user.sk + + pseudorandom_f_g_v(grp_params, &self.v, lk)? * rr_k; + tt.push(tt_k); + + // compute values mu, o_mu, lambda, o_lambda + let maybe_mu_k: Option = (self.v + Scalar::from(lk) + Scalar::from(1)) + .invert() + .into(); + let mu_k = maybe_mu_k.ok_or(CompactEcashError::UnluckiestError)?; + mu.push(mu_k); + + let o_mu_k = ((o_a_k + o_c) * mu_k).neg(); + o_mu.push(o_mu_k); + + // Randomize the coin index signatures and compute kappa_k to prove possession of each coin's signature + // This involves iterating over the signatures corresponding to the coins we want to spend in this payment. + //SAFETY : Earlier `ensure_allowance` ensures we don't do out of of bound here + #[allow(clippy::unwrap_used)] + let coin_sign = coin_indices_signatures.get(lk as usize).unwrap().borrow(); + let (coin_sign_prime, coin_sign_blinding_factor) = coin_sign.blind_and_randomise(); + coin_indices_signatures_prime.push(coin_sign_prime); + //SAFETY: we checked that verification beta_g2 isn't empty + #[allow(clippy::unwrap_used)] + let kappa_k: G2Projective = grp_params.gen2() * coin_sign_blinding_factor + + verification_key.alpha + + verification_key.beta_g2.first().unwrap() * Scalar::from(lk); + kappa_k_vec.push(kappa_k); + } + + // construct the zkp proof + let spend_instance = SpendInstance { + kappa, + cc, + aa: aa.clone(), + ss: ss.clone(), + tt: tt.clone(), + kappa_k: kappa_k_vec.clone(), + kappa_e, + }; + let spend_witness = SpendWitness { + attributes: &attributes, + r: sign_blinding_factor, + o_c, + lk: lk_vec, + o_a, + mu, + o_mu, + r_k: r_k_vec, + r_e: date_sign_blinding_factor, + }; + + let zk_proof = SpendProof::construct( + &spend_instance, + &spend_witness, + verification_key, + &rr, + pay_info, + spend_value, + ); + + // output pay + let pay = Payment { + kappa, + kappa_e, + sig: signature_prime, + sig_exp: date_signature_prime, + kappa_k: kappa_k_vec.clone(), + omega: coin_indices_signatures_prime, + ss: ss.clone(), + tt: tt.clone(), + aa: aa.clone(), + spend_value, + cc, + t_type: self.t_type, + zk_proof, + }; + + Ok(pay) + } +} + +fn pseudorandom_f_delta_v(params: &GroupParameters, v: &Scalar, l: u64) -> Result { + let maybe_pow: Option = (v + Scalar::from(l) + Scalar::from(1)).invert().into(); + Ok(params.delta() * maybe_pow.ok_or(CompactEcashError::UnluckiestError)?) +} + +fn pseudorandom_f_g_v(params: &GroupParameters, v: &Scalar, l: u64) -> Result { + let maybe_pow: Option = (v + Scalar::from(l) + Scalar::from(1)).invert().into(); + Ok(params.gen1() * maybe_pow.ok_or(CompactEcashError::UnluckiestError)?) +} + +/// Computes the value of kappa (blinded private attributes for show) for proving possession of the wallet signature. +/// +/// This function calculates the value of kappa, which is used to prove possession of the wallet signature in the zero-knowledge proof. +/// +/// # Arguments +/// +/// * `params` - A reference to the group parameters required for the computation. +/// * `verification_key` - The global verification key of the signing authorities. +/// * `attributes` - A slice of private attributes associated with the wallet. +/// * `blinding_factor` - The blinding factor used to randomise the wallet's signature. +/// +/// # Returns +/// +/// A `G2Projective` element representing the computed value of kappa. +/// +fn compute_kappa( + params: &GroupParameters, + verification_key: &VerificationKeyAuth, + attributes: &[&Scalar], + blinding_factor: Scalar, +) -> G2Projective { + params.gen2() * blinding_factor + + verification_key.alpha + + attributes + .iter() + .zip(verification_key.beta_g2.iter()) + .map(|(&priv_attr, beta_i)| beta_i * priv_attr) + .sum::() +} + +/// Represents the unique payment information associated with the payment. +/// The bytes representing the payment information encode the public key of the +/// provider with whom you are spending the payment, timestamp and a unique random 32 bytes. +/// +/// # Fields +/// +/// * `payinfo_bytes` - An array of bytes representing the payment information. +/// +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub struct PayInfo { + pub pay_info_bytes: [u8; 72], +} + +impl Serialize for PayInfo { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + self.pay_info_bytes.to_vec().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for PayInfo { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let pay_info_bytes = >::deserialize(deserializer)?; + Ok(PayInfo { + pay_info_bytes: pay_info_bytes + .try_into() + .map_err(|_| serde::de::Error::custom("invalid pay info bytes"))?, + }) + } +} + +impl Bytable for PayInfo { + fn to_byte_vec(&self) -> Vec { + self.pay_info_bytes.to_vec() + } + + fn try_from_byte_slice(slice: &[u8]) -> std::result::Result { + if slice.len() != 72 { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "PayInfo".into(), + expected: 72, + actual: slice.len(), + }); + } + //safety : we checked that slices length is exactly 72, hence this unwrap won't fail + #[allow(clippy::unwrap_used)] + Ok(Self { + pay_info_bytes: slice.try_into().unwrap(), + }) + } +} + +impl Base58 for PayInfo {} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Payment { + pub kappa: G2Projective, + pub kappa_e: G2Projective, + pub sig: Signature, + pub sig_exp: ExpirationDateSignature, + pub kappa_k: Vec, + pub omega: Vec, + pub ss: Vec, + pub tt: Vec, + pub aa: Vec, + pub spend_value: u64, + pub cc: G1Projective, + pub t_type: EncodedTicketType, + pub zk_proof: SpendProof, +} + +impl Payment { + /// Checks the validity of the payment signature. + /// + /// This function performs two checks to ensure the payment signature is valid: + /// - Verifies that the element `h` of the payment signature does not equal the identity. + /// - Performs a bilinear pairing check involving the elements of the signature and the payment (`h`, `kappa`, and `s`). + /// + /// # Arguments + /// + /// * `params` - A reference to the system parameters required for the checks. + /// + /// # Returns + /// + /// A `Result` indicating success if the signature is valid or an error if any check fails. + /// + /// # Errors + /// + /// An error is returned if: + /// - The element `h` of the payment signature equals the identity. + /// - The bilinear pairing check for `kappa` fails. + /// + pub fn check_signature_validity(&self, verification_key: &VerificationKeyAuth) -> Result<()> { + let params = ecash_group_parameters(); + if bool::from(self.sig.h.is_identity()) { + return Err(CompactEcashError::SpendSignaturesValidity); + } + + let kappa_type = self.kappa + verification_key.beta_g2[3] * type_scalar(self.t_type); + if !check_bilinear_pairing( + &self.sig.h.to_affine(), + &G2Prepared::from(kappa_type.to_affine()), + &self.sig.s.to_affine(), + params.prepared_miller_g2(), + ) { + return Err(CompactEcashError::SpendSignaturesValidity); + } + Ok(()) + } + + /// Checks the validity of the expiration signature encoded in the payment given a spending date. + /// If the spending date is within the allowed range before the expiration date, the check is successful. + /// + /// This function performs two checks to ensure the payment expiration signature is valid: + /// - Verifies that the element `h` of the expiration signature does not equal the identity. + /// - Performs a bilinear pairing check involving the elements of the expiration signature and the payment (`h`, `kappa_e`, and `s`). + /// + /// # Arguments + /// + /// * `verification_key` - The global verification key of the signing authorities. + /// * `spend_date` - The date associated with the payment. + /// + /// # Returns + /// + /// A `Result` indicating success if the expiration signature is valid or an error if any check fails. + /// + /// # Errors + /// + /// An error is returned if: + /// - The element `h` of the payment expiration signature equals the identity. + /// - The bilinear pairing check for `kappa_e` fails. + /// + pub fn check_exp_signature_validity( + &self, + verification_key: &VerificationKeyAuth, + spend_date: Scalar, + ) -> Result<()> { + let grp_params = ecash_group_parameters(); + // Check if the element h of the payment expiration signature equals the identity. + if bool::from(self.sig_exp.h.is_identity()) { + return Err(CompactEcashError::ExpirationDateSignatureValidity); + } + + if verification_key.beta_g2.len() < 3 { + return Err(CompactEcashError::VerificationKeyTooShort); + } + + // Calculate m1 and m2 values. + let m1: Scalar = spend_date; + let m2: Scalar = constants::TYPE_EXP; + + // Perform a bilinear pairing check for kappa_e + //SAFETY: we checked the size of beta_G2 earlier + let combined_kappa_e = + self.kappa_e + verification_key.beta_g2[1] * m1 + verification_key.beta_g2[2] * m2; + + if !check_bilinear_pairing( + &self.sig_exp.h.to_affine(), + &G2Prepared::from(combined_kappa_e.to_affine()), + &self.sig_exp.s.to_affine(), + grp_params.prepared_miller_g2(), + ) { + return Err(CompactEcashError::ExpirationDateSignatureValidity); + } + + Ok(()) + } + + /// Checks that all serial numbers in the payment are unique. + /// + /// This function verifies that each serial number in the payment's serial number array (`ss`) is unique. + /// + /// # Returns + /// + /// A `Result` indicating success if all serial numbers are unique or an error if any serial number is duplicated. + /// + /// # Errors + /// + /// An error is returned if not all serial numbers in the payment are unique. + /// + pub fn no_duplicate_serial_numbers(&self) -> Result<()> { + let mut seen_serial_numbers = Vec::new(); + + for serial_number in &self.ss { + if seen_serial_numbers.contains(serial_number) { + return Err(CompactEcashError::SpendDuplicateSerialNumber); + } + seen_serial_numbers.push(*serial_number); + } + + Ok(()) + } + + // /// Checks the validity of the coin index signature at a specific index. + // /// + // /// This function performs two checks to ensure the coin index signature at a given index (`k`) is valid: + // /// - Verifies that the element `h` of the coin index signature does not equal the identity. + // /// - Calculates a combined element for the bilinear pairing check involving `kappa_k`, and verifies the pairing with the coin index signature elements (`h`, `kappa_k`, and `s`). + // /// + // /// # Arguments + // /// + // /// * `verification_key` - The global verification key of the signing authorities. + // /// * `k` - The index at which to check the coin index signature. + // /// + // /// # Returns + // /// + // /// A `Result` indicating success if the coin index signature is valid or an error if any check fails. + // /// + // /// # Errors + // /// + // /// An error is returned if: + // /// - The element `h` of the coin index signature at the specified index equals the identity. + // /// - The bilinear pairing check for `kappa_k` at the specified index fails. + // /// - The specified index is out of bounds for the coin index signatures array (`omega`). + // /// + // pub fn check_coin_index_signature( + // &self, + // verification_key: &VerificationKeyAuth, + // k: u64, + // ) -> Result<()> { + // if let Some(coin_idx_sign) = self.omega.get(k as usize) { + // if bool::from(coin_idx_sign.h.is_identity()) { + // return Err(CompactEcashError::SpendSignaturesVerification); + // } + // if verification_key.beta_g2.len() < 3 { + // return Err(CompactEcashError::VerificationKeyTooShort); + // } + // //SAFETY: we checked the size of beta_G2 earlier + // #[allow(clippy::unwrap_used)] + // let combined_kappa_k = self.kappa_k[k as usize].to_affine() + // + verification_key.beta_g2.get(1).unwrap() * constants::TYPE_IDX + // + verification_key.beta_g2.get(2).unwrap() * constants::TYPE_IDX; + // + // if !check_bilinear_pairing( + // &coin_idx_sign.h.to_affine(), + // &G2Prepared::from(combined_kappa_k.to_affine()), + // &coin_idx_sign.s.to_affine(), + // ecash_group_parameters().prepared_miller_g2(), + // ) { + // return Err(CompactEcashError::SpendSignaturesVerification); + // } + // } else { + // return Err(CompactEcashError::SpendSignaturesVerification); + // } + // Ok(()) + // } + + /// Checks the validity of all coin index signatures available. + pub fn batch_check_coin_index_signatures( + &self, + verification_key: &VerificationKeyAuth, + ) -> Result<()> { + if verification_key.beta_g2.len() < 3 { + return Err(CompactEcashError::VerificationKeyTooShort); + } + + if self.omega.len() != self.kappa_k.len() { + return Err(CompactEcashError::SpendSignaturesVerification); + } + + let partially_signed = verification_key.beta_g2[1] * constants::TYPE_IDX + + verification_key.beta_g2[2] * constants::TYPE_IDX; + + let mut pairing_terms = Vec::with_capacity(self.omega.len()); + for (sig, kappa_k) in self.omega.iter().zip(self.kappa_k.iter()) { + pairing_terms.push((sig, partially_signed + kappa_k)) + } + + if !batch_verify_signatures(pairing_terms.iter()) { + return Err(CompactEcashError::SpendSignaturesVerification); + } + Ok(()) + } + + /// Checks the validity of the attached zk proof of spending. + pub fn verify_spend_proof( + &self, + verification_key: &VerificationKeyAuth, + pay_info: &PayInfo, + ) -> Result<()> { + // Compute pay_info hash for each coin + let mut rr = Vec::with_capacity(self.spend_value as usize); + for k in 0..self.spend_value { + // Compute hashes R_k = H(payinfo, k) + let rr_k = compute_pay_info_hash(pay_info, k); + rr.push(rr_k); + } + + // verify the zk proof + let instance = SpendInstance { + kappa: self.kappa, + cc: self.cc, + aa: self.aa.clone(), + ss: self.ss.clone(), + tt: self.tt.clone(), + kappa_k: self.kappa_k.clone(), + kappa_e: self.kappa_e, + }; + + // verify the zk-proof + if !self + .zk_proof + .verify(&instance, verification_key, &rr, pay_info, self.spend_value) + { + return Err(CompactEcashError::SpendZKProofVerification); + } + + Ok(()) + } + + /// Verifies the validity of a spend transaction, including signature checks, + /// expiration date signature checks, serial number uniqueness, coin index signature checks, + /// and zero-knowledge proof verification. + /// + /// # Arguments + /// + /// * `params` - The cryptographic parameters. + /// * `verification_key` - The verification key used for validation. + /// * `pay_info` - The pay information associated with the transaction. + /// * `spend_date` - The date at which the spending transaction occurs. + /// + /// # Returns + /// + /// Returns `Ok(true)` if the spend transaction is valid; otherwise, returns an error. + pub fn spend_verify( + &self, + verification_key: &VerificationKeyAuth, + pay_info: &PayInfo, + spend_date: EncodedDate, + ) -> Result<()> { + // check if all serial numbers are different + self.no_duplicate_serial_numbers()?; + // verify the zk proof + self.verify_spend_proof(verification_key, pay_info)?; + // Verify whether the payment signature and kappa are correct + self.check_signature_validity(verification_key)?; + // Verify whether the expiration date signature and kappa_e are correct + self.check_exp_signature_validity(verification_key, date_scalar(spend_date))?; + // Verify whether the coin indices signatures and kappa_k are correct + self.batch_check_coin_index_signatures(verification_key)?; + + Ok(()) + } + + pub fn encoded_serial_number(&self) -> Vec { + SerialNumberRef { inner: &self.ss }.to_bytes() + } + + pub fn serial_number_bs58(&self) -> String { + SerialNumberRef { inner: &self.ss }.to_bs58() + } + + // pub fn has_serial_number(&self, serial_number_bs58: &str) -> Result { + // let serial_number = SerialNumberRef::try_from_bs58(serial_number_bs58)?; + // let ret = self.ss.eq(&serial_number.inner); + // Ok(ret) + // } +} + +pub struct SerialNumberRef<'a> { + pub(crate) inner: &'a [G1Projective], +} + +impl<'a> SerialNumberRef<'a> { + pub fn to_bytes(&self) -> Vec { + let ss_len = self.inner.len(); + let mut bytes: Vec = Vec::with_capacity(ss_len * 48); + for s in self.inner { + bytes.extend_from_slice(&s.to_affine().to_compressed()); + } + bytes + } + + pub fn to_bs58(&self) -> String { + bs58::encode(self.to_bytes()).into_string() + } +} diff --git a/common/nym_offline_compact_ecash/src/scheme/setup.rs b/common/nym_offline_compact_ecash/src/scheme/setup.rs new file mode 100644 index 0000000000..dd469fb009 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/scheme/setup.rs @@ -0,0 +1,104 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash_group_parameters; +use crate::utils::hash_g1; +use bls12_381::{G1Affine, G1Projective, G2Affine, G2Prepared, Scalar}; +use ff::Field; +use group::GroupEncoding; +use rand::thread_rng; + +#[derive(Debug)] +pub struct GroupParameters { + /// Generator of the G1 group + g1: G1Affine, + /// Generator of the G2 group + g2: G2Affine, + /// Additional generators of the G1 group + gammas: Vec, + // Additional generator of the G1 group + delta: G1Projective, + /// Precomputed G2 generator used for the miller loop + _g2_prepared_miller: G2Prepared, +} + +impl GroupParameters { + pub fn new(attributes: usize) -> GroupParameters { + assert!(attributes > 0); + let gammas = (1..=attributes) + .map(|i| hash_g1(format!("gamma{}", i))) + .collect(); + + let delta = hash_g1("delta"); + + GroupParameters { + g1: G1Affine::generator(), + g2: G2Affine::generator(), + gammas, + delta, + _g2_prepared_miller: G2Prepared::from(G2Affine::generator()), + } + } + + pub(crate) fn gen1(&self) -> &G1Affine { + &self.g1 + } + + pub(crate) fn gen2(&self) -> &G2Affine { + &self.g2 + } + + pub(crate) fn gammas(&self) -> &Vec { + &self.gammas + } + + pub(crate) fn gammas_to_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(self.gammas.len() * 48); + for g in &self.gammas { + bytes.extend_from_slice(g.to_bytes().as_ref()); + } + bytes + } + + pub(crate) fn gamma_idx(&self, i: usize) -> Option<&G1Projective> { + self.gammas.get(i) + } + + pub(crate) fn delta(&self) -> &G1Projective { + &self.delta + } + + pub fn random_scalar(&self) -> Scalar { + // lazily-initialized thread-local random number generator, seeded by the system + let mut rng = thread_rng(); + Scalar::random(&mut rng) + } + + pub fn n_random_scalars(&self, n: usize) -> Vec { + (0..n).map(|_| self.random_scalar()).collect() + } + + pub(crate) fn prepared_miller_g2(&self) -> &G2Prepared { + &self._g2_prepared_miller + } +} + +#[derive(Debug)] +pub struct Parameters { + /// Number of coins of fixed denomination in the credential wallet; L in construction + total_coins: u64, +} + +impl Parameters { + pub fn new(total_coins: u64) -> Parameters { + assert!(total_coins > 0); + Parameters { total_coins } + } + pub fn grp(&self) -> &GroupParameters { + ecash_group_parameters() + } + + pub fn get_total_coins(&self) -> u64 { + self.total_coins + } +} diff --git a/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs b/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs new file mode 100644 index 0000000000..082f969508 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs @@ -0,0 +1,521 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::common_types::{BlindedSignature, Signature, SignerIndex}; +use crate::error::{CompactEcashError, Result}; +use crate::helpers::{date_scalar, type_scalar}; +use crate::proofs::proof_withdrawal::{ + WithdrawalReqInstance, WithdrawalReqProof, WithdrawalReqWitness, +}; +use crate::scheme::keygen::{PublicKeyUser, SecretKeyAuth, SecretKeyUser, VerificationKeyAuth}; +use crate::scheme::setup::GroupParameters; +use crate::scheme::PartialWallet; +use crate::utils::{check_bilinear_pairing, hash_g1}; +use crate::{constants, ecash_group_parameters, Attribute, EncodedDate, EncodedTicketType}; +use bls12_381::{multi_miller_loop, G1Projective, G2Prepared, G2Projective, Scalar}; +use group::{Curve, Group, GroupEncoding}; +use serde::{Deserialize, Serialize}; +use std::ops::Neg; + +/// Represents a withdrawal request generate by the client who wants to obtain a zk-nym credential. +/// +/// This struct encapsulates the necessary components for a withdrawal request, including the joined commitment hash, the joined commitment, +/// individual Pedersen commitments for private attributes, and a zero-knowledge proof for the withdrawal request. +/// +/// # Fields +/// +/// * `joined_commitment_hash` - The joined commitment hash represented as a G1Projective element. +/// * `joined_commitment` - The joined commitment represented as a G1Projective element. +/// * `private_attributes_commitments` - A vector of individual Pedersen commitments for private attributes represented as G1Projective elements. +/// * `zk_proof` - The zero-knowledge proof for the withdrawal request. +/// +/// # Derives +/// +/// The struct derives `Debug` and `PartialEq` to provide debug output and basic comparison functionality. +/// +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct WithdrawalRequest { + joined_commitment_hash: G1Projective, + joined_commitment: G1Projective, + private_attributes_commitments: Vec, + zk_proof: WithdrawalReqProof, +} + +impl WithdrawalRequest { + pub fn get_private_attributes_commitments(&self) -> &[G1Projective] { + &self.private_attributes_commitments + } +} + +/// Represents information associated with a withdrawal request. +/// +/// This structure holds the commitment hash, commitment opening, private attributes openings, +/// the wallet secret (scalar), and the expiration date related to a withdrawal request. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RequestInfo { + joined_commitment_hash: G1Projective, + joined_commitment_opening: Scalar, + private_attributes_openings: Vec, + wallet_secret: Scalar, + expiration_date: Scalar, + t_type: Scalar, +} + +impl RequestInfo { + pub fn get_joined_commitment_hash(&self) -> &G1Projective { + &self.joined_commitment_hash + } + pub fn get_joined_commitment_opening(&self) -> &Scalar { + &self.joined_commitment_opening + } + pub fn get_private_attributes_openings(&self) -> &[Scalar] { + &self.private_attributes_openings + } + pub fn get_v(&self) -> &Scalar { + &self.wallet_secret + } + pub fn get_expiration_date(&self) -> &Scalar { + &self.expiration_date + } + pub fn get_t_type(&self) -> &Scalar { + &self.t_type + } +} + +/// Computes Pedersen commitments for private attributes. +/// +/// Given a set of private attributes and the commitment hash for all attributes, +/// this function generates random blinding factors (`openings`) and computes corresponding +/// Pedersen commitments for each private attribute. +/// Pedersen commitments have the hiding and binding properties, providing a secure way +/// to represent private values in a commitment scheme. +/// +/// # Arguments +/// +/// * `params` - Group parameters for the cryptographic group. +/// * `joined_commitment_hash` - The commitment hash to be used in the Pedersen commitments. +/// * `private_attributes` - A slice of private attributes to be committed. +/// +/// # Returns +/// +/// A tuple containing vectors of blinding factors (`openings`) and corresponding +/// Pedersen commitments for each private attribute. +fn compute_private_attribute_commitments( + params: &GroupParameters, + joined_commitment_hash: &G1Projective, + private_attributes: &[Scalar], +) -> (Vec, Vec) { + let (openings, commitments): (Vec, Vec) = private_attributes + .iter() + .map(|m_j| { + let o_j = params.random_scalar(); + (o_j, params.gen1() * o_j + joined_commitment_hash * m_j) + }) + .unzip(); + + (openings, commitments) +} + +/// Generates a withdrawal request for the given user to request a zk-nym credential wallet. +/// +/// # Arguments +/// +/// * `sk_user` - A reference to the user's secret key. +/// * `expiration_date` - The expiration date for the withdrawal request. +/// * `t_type` - The type of the ticket book +/// +/// # Returns +/// +/// A tuple containing the generated `WithdrawalRequest` and `RequestInfo`, or an error if the operation fails. +/// +/// # Details +/// +/// The function starts by generating a random, unique wallet secret `v` and computing the joined commitment for all attributes, +/// including public (expiration date) and private ones (user secret key and wallet secret). +/// It then calculates the commitment hash (`joined_commitment_hash`) and computes Pedersen commitments for private attributes. +/// A zero-knowledge proof of knowledge is constructed to prove possession of specific attributes. +/// +/// The resulting `WithdrawalRequest` includes the commitment hash, joined commitment, commitments for private +/// attributes, and the constructed zero-knowledge proof. +/// +/// The associated `RequestInfo` includes information such as commitment hash, commitment opening, +/// openings for private attributes, `v`, and the expiration date. +pub fn withdrawal_request( + sk_user: &SecretKeyUser, + expiration_date: EncodedDate, + t_type: EncodedTicketType, +) -> Result<(WithdrawalRequest, RequestInfo)> { + let params = ecash_group_parameters(); + // Generate random and unique wallet secret + let v = params.random_scalar(); + let joined_commitment_opening = params.random_scalar(); + + let gamma = params.gammas(); + let expiration_date = date_scalar(expiration_date); + let t_type = type_scalar(t_type); + + // Compute joined commitment for all attributes (public and private) + let joined_commitment = + params.gen1() * joined_commitment_opening + gamma[0] * sk_user.sk + gamma[1] * v; + + // Compute commitment hash h + let joined_commitment_hash = + hash_g1((joined_commitment + gamma[2] * expiration_date + gamma[3] * t_type).to_bytes()); + + // Compute Pedersen commitments for private attributes (wallet secret and user's secret) + let private_attributes = vec![sk_user.sk, v]; + let (private_attributes_openings, private_attributes_commitments) = + compute_private_attribute_commitments(params, &joined_commitment_hash, &private_attributes); + + // construct a NIZK proof of knowledge proving possession of m1, m2, o, o1, o2 + let instance = WithdrawalReqInstance { + joined_commitment, + joined_commitment_hash, + private_attributes_commitments: private_attributes_commitments.clone(), + pk_user: PublicKeyUser { + pk: params.gen1() * sk_user.sk, + }, + }; + + let witness = WithdrawalReqWitness { + private_attributes, + joined_commitment_opening, + private_attributes_openings: private_attributes_openings.clone(), + }; + let zk_proof = WithdrawalReqProof::construct(&instance, &witness); + + // Create and return WithdrawalRequest and RequestInfo + Ok(( + WithdrawalRequest { + joined_commitment_hash, + joined_commitment, + private_attributes_commitments, + zk_proof, + }, + RequestInfo { + joined_commitment_hash, + joined_commitment_opening, + private_attributes_openings: private_attributes_openings.clone(), + wallet_secret: v, + expiration_date, + t_type, + }, + )) +} + +/// Verifies the integrity of a withdrawal request, including the joined commitment hash +/// and the zero-knowledge proof of knowledge. +/// +/// # Arguments +/// +/// * `req` - The withdrawal request to be verified. +/// * `pk_user` - Public key of the user associated with the withdrawal request. +/// * `expiration_date` - Expiration date for the ticket book. +/// * `t_type` - The type of the ticket book +/// +/// # Returns +/// +/// Returns `Ok(true)` if the verification is successful, otherwise returns an error +/// with a specific message indicating the verification failure. +pub fn request_verify( + req: &WithdrawalRequest, + pk_user: PublicKeyUser, + expiration_date: EncodedDate, + t_type: EncodedTicketType, +) -> Result<()> { + let params = ecash_group_parameters(); + + let gamma = params.gammas(); + let expiration_date = date_scalar(expiration_date); + let t_type = type_scalar(t_type); + + let expected_commitment_hash = hash_g1( + (req.joined_commitment + gamma[2] * expiration_date + gamma[3] * t_type).to_bytes(), + ); + if req.joined_commitment_hash != expected_commitment_hash { + return Err(CompactEcashError::WithdrawalRequestVerification); + } + // Verify zk proof + let instance = WithdrawalReqInstance { + joined_commitment: req.joined_commitment, + joined_commitment_hash: req.joined_commitment_hash, + private_attributes_commitments: req.private_attributes_commitments.clone(), + pk_user, + }; + if !req.zk_proof.verify(&instance) { + return Err(CompactEcashError::WithdrawalRequestVerification); + } + Ok(()) +} + +/// Signs an expiration date using a joined commitment hash and a secret key. +/// +/// Given a joined commitment hash (`joined_commitment_hash`), an expiration date (`expiration_date`), +/// and a secret key for authentication (`sk_auth`), this function computes the signature of the +/// expiration date by multiplying the commitment hash with the blinding factor derived from the secret key +/// and the expiration date. +/// +/// # Arguments +/// +/// * `joined_commitment_hash` - The G1Projective point representing the joined commitment hash. +/// * `expiration_date` - The expiration date timestamp to be signed. +/// * `sk_auth` - The secret key of the signing authority. Assumes key is long enough. +/// +/// # Returns +/// +/// A `Result` containing the resulting G1Projective point if successful, or an error if the +/// authentication secret key index is out of bounds. +fn sign_expiration_date( + joined_commitment_hash: &G1Projective, + expiration_date: EncodedDate, + sk_auth: &SecretKeyAuth, +) -> G1Projective { + joined_commitment_hash * (sk_auth.ys[2] * date_scalar(expiration_date)) +} + +/// Signs a transaction type using a joined commitment hash and a secret key. +/// +/// Given a joined commitment hash (`joined_commitment_hash`), a ticket type (`t_type`), +/// and a secret key for authentication (`sk_auth`), this function computes the signature of the +/// ticket type. +/// +/// # Arguments +/// +/// * `joined_commitment_hash` - The G1Projective point representing the joined commitment hash. +/// * `t_type` - The ticket type identifier to be signed. +/// * `sk_auth` - The secret key of the signing authority. +/// +/// # Returns +/// +/// The resulting G1Projective point representing the signed ticket type. +fn sign_t_type( + joined_commitment_hash: &G1Projective, + t_type: EncodedTicketType, + sk_auth: &SecretKeyAuth, +) -> G1Projective { + joined_commitment_hash * (sk_auth.ys[3] * type_scalar(t_type)) +} + +/// Issues a blinded signature for a withdrawal request, after verifying its integrity. +/// +/// This function first verifies the withdrawal request using the provided group parameters, +/// user's public key, and expiration date. If the verification is successful, +/// the function proceeds to blind sign the private attributes and sign the expiration date, +/// combining both signatures into a final signature. +/// +/// # Arguments +/// +/// * `sk_auth` - Secret key of the signing authority. +/// * `pk_user` - Public key of the user associated with the withdrawal request. +/// * `withdrawal_req` - The withdrawal request to be signed. +/// * `expiration_date` - Expiration date for the withdrawal request. +/// +/// # Returns +/// +/// Returns a `BlindedSignature` if the issuance process is successful, otherwise returns an error +/// with a specific message indicating the failure. +pub fn issue( + sk_auth: &SecretKeyAuth, + pk_user: PublicKeyUser, + withdrawal_req: &WithdrawalRequest, + expiration_date: EncodedDate, + t_type: EncodedTicketType, +) -> Result { + // Verify the withdrawal request + request_verify(withdrawal_req, pk_user, expiration_date, t_type)?; + // Verify `sk_auth` is long enough + if sk_auth.ys.len() < constants::ATTRIBUTES_LEN { + return Err(CompactEcashError::KeyTooShort); + } + // Blind sign the private attributes + let blind_signatures: G1Projective = withdrawal_req + .private_attributes_commitments + .iter() + .zip(sk_auth.ys.iter().take(2)) + .map(|(pc, yi)| pc * yi) + .sum(); + // Sign the expiration date + //SAFETY: key length was verified before + let expiration_date_sign = sign_expiration_date( + &withdrawal_req.joined_commitment_hash, + expiration_date, + sk_auth, + ); + // Sign the type + let t_type_sign = sign_t_type(&withdrawal_req.joined_commitment_hash, t_type, sk_auth); + // Combine both signatures + let signature = blind_signatures + + withdrawal_req.joined_commitment_hash * sk_auth.x + + expiration_date_sign + + t_type_sign; + + Ok(BlindedSignature { + h: withdrawal_req.joined_commitment_hash, + c: signature, + }) +} + +/// Verifies the integrity and correctness of a blinded signature +/// and returns an unblinded partial zk-nym wallet. +/// +/// This function first verifies the integrity of the received blinded signature by checking +/// if the joined commitment hash matches the one provided in the `req_info`. If the verification +/// is successful, it proceeds to unblind the blinded signature and verify its correctness. +/// +/// # Arguments +/// +/// * `vk_auth` - Verification key of the signing authority. +/// * `sk_user` - Secret key of the user. +/// * `blind_signature` - Blinded signature received from the authority. +/// * `req_info` - Information associated with the request, including the joined commitment hash, +/// private attributes openings, v, and expiration date. +/// +/// # Returns +/// +/// Returns a `PartialWallet` if the verification process is successful, otherwise returns an error +/// with a specific message indicating the failure. +pub fn issue_verify( + vk_auth: &VerificationKeyAuth, + sk_user: &SecretKeyUser, + blind_signature: &BlindedSignature, + req_info: &RequestInfo, + signer_index: SignerIndex, +) -> Result { + let params = ecash_group_parameters(); + // Verify the integrity of the response from the authority + if req_info.joined_commitment_hash != blind_signature.h { + return Err(CompactEcashError::IssuanceVerification); + } + + // Unblind the blinded signature on the partial signature + let blinding_removers = vk_auth + .beta_g1 + .iter() + .zip(&req_info.private_attributes_openings) + .map(|(beta, opening)| beta * opening) + .sum::(); + let unblinded_c = blind_signature.c - blinding_removers; + + let attr = [ + sk_user.sk, + req_info.wallet_secret, + req_info.expiration_date, + req_info.t_type, + ]; + + let signed_attributes = attr + .iter() + .zip(vk_auth.beta_g2.iter()) + .map(|(attr, beta_i)| beta_i * attr) + .sum::(); + + // Verify the signature correctness on the wallet share + if !check_bilinear_pairing( + &blind_signature.h.to_affine(), + &G2Prepared::from((vk_auth.alpha + signed_attributes).to_affine()), + &unblinded_c.to_affine(), + params.prepared_miller_g2(), + ) { + return Err(CompactEcashError::IssuanceVerification); + } + + Ok(PartialWallet { + sig: Signature { + h: blind_signature.h, + s: unblinded_c, + }, + v: req_info.wallet_secret, + idx: signer_index, + expiration_date: req_info.expiration_date, + t_type: req_info.t_type, + }) +} + +/// Verifies a partial blind signature using the provided parameters and validator's verification key. +/// +/// # Arguments +/// +/// * `blind_sign_request` - A reference to the blind signature request signed by the client. +/// * `public_attributes` - A reference to the public attributes included in the client's request. +/// * `blind_sig` - A reference to the issued partial blinded signature to be verified. +/// * `partial_verification_key` - A reference to the validator's partial verification key. +/// +/// # Returns +/// +/// A boolean indicating whether the partial blind signature is valid (`true`) or not (`false`). +/// +/// # Remarks +/// +/// This function verifies the correctness and validity of a partial blind signature using +/// the provided cryptographic parameters, blind signature request, blinded signature, +/// and partial verification key. +/// It calculates pairings based on the provided values and checks whether the partial blind signature +/// is consistent with the verification key and commitments in the blind signature request. +/// The function returns `true` if the partial blind signature is valid, and `false` otherwise. +pub fn verify_partial_blind_signature( + private_attribute_commitments: &[G1Projective], + public_attributes: &[&Attribute], + blind_sig: &BlindedSignature, + partial_verification_key: &VerificationKeyAuth, +) -> bool { + let params = ecash_group_parameters(); + let num_private_attributes = private_attribute_commitments.len(); + if num_private_attributes + public_attributes.len() > partial_verification_key.beta_g2.len() { + return false; + } + + // TODO: we're losing some memory here due to extra allocation, + // but worst-case scenario (given SANE amount of attributes), it's just few kb at most + let c_neg = blind_sig.c.to_affine().neg(); + let g2_prep = params.prepared_miller_g2(); + + let mut terms = vec![ + // (c^{-1}, g2) + (c_neg, g2_prep.clone()), + // (s, alpha) + ( + blind_sig.h.to_affine(), + G2Prepared::from(partial_verification_key.alpha.to_affine()), + ), + ]; + + // for each private attribute, add (cm_i, beta_i) to the miller terms + for (private_attr_commit, beta_g2) in private_attribute_commitments + .iter() + .zip(&partial_verification_key.beta_g2) + { + // (cm_i, beta_i) + terms.push(( + private_attr_commit.to_affine(), + G2Prepared::from(beta_g2.to_affine()), + )) + } + + // for each public attribute, add (s^pub_j, beta_{priv + j}) to the miller terms + for (&pub_attr, beta_g2) in public_attributes.iter().zip( + partial_verification_key + .beta_g2 + .iter() + .skip(num_private_attributes), + ) { + // (s^pub_j, beta_j) + terms.push(( + (blind_sig.h * pub_attr).to_affine(), + G2Prepared::from(beta_g2.to_affine()), + )) + } + + // get the references to all the terms to get the arguments the miller loop expects + #[allow(clippy::map_identity)] + let terms_refs = terms.iter().map(|(g1, g2)| (g1, g2)).collect::>(); + + // since checking whether e(a, b) == e(c, d) + // is equivalent to checking e(a, b) • e(c, d)^{-1} == id + // and thus to e(a, b) • e(c^{-1}, d) == id + // + // compute e(c^{-1}, g2) • e(s, alpha) • e(cm_0, beta_0) • e(cm_i, beta_i) • (s^pub_0, beta_{i+1}) (s^pub_j, beta_{i + j}) + multi_miller_loop(&terms_refs) + .final_exponentiation() + .is_identity() + .into() +} diff --git a/common/nym_offline_compact_ecash/src/tests/e2e.rs b/common/nym_offline_compact_ecash/src/tests/e2e.rs new file mode 100644 index 0000000000..6bed36fb30 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/tests/e2e.rs @@ -0,0 +1,136 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(test)] +mod tests { + use crate::error::Result; + use crate::scheme::aggregation::{aggregate_verification_keys, aggregate_wallets}; + use crate::scheme::keygen::{ + generate_keypair_user, ttp_keygen, SecretKeyAuth, VerificationKeyAuth, + }; + use crate::scheme::withdrawal::{issue, issue_verify, withdrawal_request, WithdrawalRequest}; + use crate::scheme::{PartialWallet, PayInfo, Payment, Wallet}; + use crate::setup::Parameters; + use crate::tests::helpers::{ + generate_coin_indices_signatures, generate_expiration_date_signatures, + }; + use itertools::izip; + + #[test] + fn main() -> Result<()> { + let total_coins = 32; + let params = Parameters::new(total_coins); + // NOTE: Make sure that the date timestamp are calculated at 00:00:00!! + let spend_date = 1701907200; // Dec 07 2023 00:00:00 + let expiration_date = 1702166400; // Dec 10 2023 00:00:00 + let t_type = 1; + let user_keypair = generate_keypair_user(); + + // generate authorities keys + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&[1, 2, 3]))?; + + // generate valid dates signatures + let dates_signatures = generate_expiration_date_signatures( + expiration_date, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + )?; + + // generate coin indices signatures + let coin_indices_signatures = generate_coin_indices_signatures( + ¶ms, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + )?; + + // request a wallet + let (req, req_info) = + withdrawal_request(user_keypair.secret_key(), expiration_date, t_type).unwrap(); + let req_bytes = req.to_bytes(); + let req2 = WithdrawalRequest::try_from(req_bytes.as_slice()).unwrap(); + assert_eq!(req, req2); + + // issue partial wallets + let mut wallet_blinded_signatures = Vec::new(); + for auth_keypair in authorities_keypairs { + let blind_signature = issue( + auth_keypair.secret_key(), + user_keypair.public_key(), + &req, + expiration_date, + t_type, + ); + wallet_blinded_signatures.push(blind_signature.unwrap()); + } + + let unblinded_wallet_shares: Vec = izip!( + wallet_blinded_signatures.iter(), + verification_keys_auth.iter() + ) + .enumerate() + .map(|(idx, (w, vk))| { + issue_verify(vk, user_keypair.secret_key(), w, &req_info, idx as u64 + 1).unwrap() + }) + .collect(); + + let partial_wallet = unblinded_wallet_shares.first().unwrap().clone(); + let partial_wallet_bytes = partial_wallet.to_bytes(); + let partial_wallet2 = PartialWallet::try_from(&partial_wallet_bytes[..]).unwrap(); + assert_eq!(partial_wallet, partial_wallet2); + + // Aggregate partial wallets + let mut aggr_wallet = aggregate_wallets( + &verification_key, + user_keypair.secret_key(), + &unblinded_wallet_shares, + &req_info, + )?; + + let wallet_bytes = aggr_wallet.to_bytes(); + let wallet = Wallet::from_bytes(&wallet_bytes).unwrap(); + assert_eq!(aggr_wallet, wallet); + + // Let's try to spend some coins + let pay_info = PayInfo { + pay_info_bytes: [6u8; 72], + }; + let spend_vv = 1; + + let payment = aggr_wallet.spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + )?; + + assert!(payment + .spend_verify(&verification_key, &pay_info, spend_date) + .is_ok()); + + let payment_bytes = payment.to_bytes(); + let payment2 = Payment::try_from(&payment_bytes[..]).unwrap(); + assert_eq!(payment, payment2); + + Ok(()) + } +} diff --git a/common/nym_offline_compact_ecash/src/tests/helpers.rs b/common/nym_offline_compact_ecash/src/tests/helpers.rs new file mode 100644 index 0000000000..5533dc0f48 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/tests/helpers.rs @@ -0,0 +1,182 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::common_types::SignerIndex; +use crate::error::Result; +use crate::scheme::coin_indices_signatures::{ + aggregate_indices_signatures, sign_coin_indices, CoinIndexSignature, CoinIndexSignatureShare, +}; +use crate::scheme::expiration_date_signatures::{ + aggregate_expiration_signatures, sign_expiration_date, ExpirationDateSignature, + ExpirationDateSignatureShare, +}; +use crate::scheme::keygen::{KeyPairAuth, SecretKeyAuth}; +use crate::scheme::Payment; +use crate::setup::Parameters; +use crate::{ + aggregate_verification_keys, aggregate_wallets, constants, generate_keypair_user, issue, + issue_verify, withdrawal_request, EncodedDate, EncodedTicketType, PartialWallet, PayInfo, + VerificationKeyAuth, +}; +use itertools::izip; + +pub fn generate_expiration_date_signatures( + expiration_date: EncodedDate, + secret_keys_authorities: &[&SecretKeyAuth], + verification_keys_auth: &[VerificationKeyAuth], + verification_key: &VerificationKeyAuth, + indices: &[u64], +) -> Result> { + let mut edt_partial_signatures: Vec> = + Vec::with_capacity(constants::CRED_VALIDITY_PERIOD_DAYS as usize); + for sk_auth in secret_keys_authorities.iter() { + //Test helpers + #[allow(clippy::unwrap_used)] + let sign = sign_expiration_date(sk_auth, expiration_date).unwrap(); + edt_partial_signatures.push(sign); + } + let combined_data: Vec<_> = indices + .iter() + .zip( + verification_keys_auth + .iter() + .zip(edt_partial_signatures.iter()), + ) + .map(|(i, (vk, sigs))| ExpirationDateSignatureShare { + index: *i, + key: vk.clone(), + signatures: sigs.clone(), + }) + .collect(); + + aggregate_expiration_signatures(verification_key, expiration_date, &combined_data) +} + +pub fn generate_coin_indices_signatures( + params: &Parameters, + secret_keys_authorities: &[&SecretKeyAuth], + verification_keys_auth: &[VerificationKeyAuth], + verification_key: &VerificationKeyAuth, + indices: &[u64], +) -> Result> { + // create the partial signatures from each authority + //Test helpers + #[allow(clippy::unwrap_used)] + let partial_signatures: Vec> = secret_keys_authorities + .iter() + .map(|sk_auth| sign_coin_indices(params, verification_key, sk_auth).unwrap()) + .collect(); + + let combined_data: Vec<_> = indices + .iter() + .zip(verification_keys_auth.iter().zip(partial_signatures.iter())) + .map(|(i, (vk, sigs))| CoinIndexSignatureShare { + index: *i, + key: vk.clone(), + signatures: sigs.clone(), + }) + .collect(); + + aggregate_indices_signatures(params, verification_key, &combined_data) +} + +pub fn payment_from_keys_and_expiration_date( + ecash_keypairs: &Vec, + indices: &[SignerIndex], + expiration_date: EncodedDate, + t_type: EncodedTicketType, +) -> Result<(Payment, PayInfo)> { + let total_coins = 32; + let params = Parameters::new(total_coins); + let spend_date = expiration_date - 29 * constants::SECONDS_PER_DAY; + let user_keypair = generate_keypair_user(); + + let secret_keys_authorities: Vec<&SecretKeyAuth> = ecash_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + let verification_keys_auth: Vec = ecash_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + + // aggregate verification keys + let verification_key = aggregate_verification_keys(&verification_keys_auth, Some(indices))?; + + // generate valid dates signatures + let dates_signatures = generate_expiration_date_signatures( + expiration_date, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + indices, + )?; + + // generate coin indices signatures + let coin_indices_signatures = generate_coin_indices_signatures( + ¶ms, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + indices, + )?; + //SAFETY : method intended for test only + #[allow(clippy::unwrap_used)] + // request a wallet + let (req, req_info) = + withdrawal_request(user_keypair.secret_key(), expiration_date, t_type).unwrap(); + + // generate blinded signatures + let mut wallet_blinded_signatures = Vec::new(); + + for keypair in ecash_keypairs { + let blinded_signature = issue( + keypair.secret_key(), + user_keypair.public_key(), + &req, + expiration_date, + t_type, + )?; + wallet_blinded_signatures.push(blinded_signature) + } + + // Unblind + //SAFETY : method intended for test only + #[allow(clippy::unwrap_used)] + let unblinded_wallet_shares: Vec = izip!( + wallet_blinded_signatures.iter(), + verification_keys_auth.iter() + ) + .enumerate() + .map(|(idx, (w, vk))| { + issue_verify(vk, user_keypair.secret_key(), w, &req_info, idx as u64 + 1).unwrap() + }) + .collect(); + + // Aggregate partial wallets + let mut aggr_wallet = aggregate_wallets( + &verification_key, + user_keypair.secret_key(), + &unblinded_wallet_shares, + &req_info, + )?; + + // Let's try to spend some coins + let pay_info = PayInfo { + pay_info_bytes: [6u8; 72], + }; + let spend_vv = 1; + + let payment = aggr_wallet.spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + )?; + + Ok((payment, pay_info)) +} diff --git a/common/nym_offline_compact_ecash/src/tests/mod.rs b/common/nym_offline_compact_ecash/src/tests/mod.rs new file mode 100644 index 0000000000..6c3d2a3b0d --- /dev/null +++ b/common/nym_offline_compact_ecash/src/tests/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +mod e2e; +pub mod helpers; diff --git a/common/nym_offline_compact_ecash/src/traits.rs b/common/nym_offline_compact_ecash/src/traits.rs new file mode 100644 index 0000000000..a9a22144b9 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/traits.rs @@ -0,0 +1,140 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::common_types::{BlindedSignature, Signature}; +use crate::proofs::proof_spend::{SpendInstance, SpendProof}; +use crate::proofs::proof_withdrawal::{WithdrawalReqInstance, WithdrawalReqProof}; +use crate::scheme::withdrawal::RequestInfo; +use crate::scheme::{Payment, WalletSignatures}; +use crate::{Attribute, CompactEcashError, PartialWallet, WithdrawalRequest}; +use bls12_381::{G1Affine, G1Projective}; +use group::GroupEncoding; + +#[macro_export] +macro_rules! impl_byteable_bs58 { + ($typ:ident) => { + impl $crate::traits::Bytable for $typ { + fn to_byte_vec(&self) -> Vec { + self.to_bytes().to_vec() + } + + fn try_from_byte_slice(slice: &[u8]) -> $crate::error::Result { + Self::from_bytes(slice) + } + } + + impl $crate::traits::Base58 for $typ {} + + impl TryFrom<&[u8]> for $typ { + type Error = CompactEcashError; + + fn try_from(bytes: &[u8]) -> $crate::error::Result { + Self::from_bytes(bytes) + } + } + }; +} + +macro_rules! impl_complex_binary_bytable { + ($typ:ident) => { + impl $typ { + pub fn to_bytes(&self) -> Vec { + use bincode::Options; + + // all of our manually derived types correctly serialise into bincode + #[allow(clippy::unwrap_used)] + crate::binary_serialiser().serialize(self).unwrap() + } + + pub fn from_bytes(bytes: &[u8]) -> crate::error::Result { + use bincode::Options; + crate::binary_serialiser() + .deserialize(bytes) + .map_err(|source| CompactEcashError::BinaryDeserialisationFailure { + type_name: std::any::type_name::<$typ>().to_string(), + source, + }) + } + } + + impl_byteable_bs58!($typ); + }; +} + +pub trait Bytable +where + Self: Sized, +{ + fn to_byte_vec(&self) -> Vec; + + fn try_from_byte_slice(slice: &[u8]) -> Result; +} + +pub trait Base58 +where + Self: Bytable, +{ + fn try_from_bs58>(x: S) -> Result { + Self::try_from_byte_slice(&bs58::decode(x.as_ref()).into_vec()?) + } + fn to_bs58(&self) -> String { + bs58::encode(self.to_byte_vec()).into_string() + } +} + +impl Bytable for G1Projective { + fn to_byte_vec(&self) -> Vec { + self.to_bytes().as_ref().to_vec() + } + + fn try_from_byte_slice(slice: &[u8]) -> Result { + let bytes = slice + .try_into() + .map_err(|_| CompactEcashError::G1ProjectiveDeserializationFailure)?; + + let maybe_g1 = G1Affine::from_compressed(&bytes); + if maybe_g1.is_none().into() { + Err(CompactEcashError::G1ProjectiveDeserializationFailure) + } else { + // safety: this unwrap is fine as we've just checked the element is not none + #[allow(clippy::unwrap_used)] + Ok(maybe_g1.unwrap().into()) + } + } +} + +impl Base58 for G1Projective {} + +impl Bytable for Attribute { + fn to_byte_vec(&self) -> Vec { + self.to_bytes().to_vec() + } + + fn try_from_byte_slice(slice: &[u8]) -> Result { + let maybe_attribute = Attribute::from_bytes( + slice + .try_into() + .map_err(|_| CompactEcashError::ScalarDeserializationFailure)?, + ); + if maybe_attribute.is_none().into() { + Err(CompactEcashError::ScalarDeserializationFailure) + } else { + // safety: this unwrap is fine as we've just checked the element is not none + #[allow(clippy::unwrap_used)] + Ok(maybe_attribute.unwrap()) + } + } +} + +impl_byteable_bs58!(Signature); +impl_byteable_bs58!(BlindedSignature); +impl_byteable_bs58!(WalletSignatures); +impl_byteable_bs58!(PartialWallet); + +impl_complex_binary_bytable!(SpendProof); +impl_complex_binary_bytable!(SpendInstance); +impl_complex_binary_bytable!(WithdrawalReqProof); +impl_complex_binary_bytable!(WithdrawalReqInstance); +impl_complex_binary_bytable!(Payment); +impl_complex_binary_bytable!(WithdrawalRequest); +impl_complex_binary_bytable!(RequestInfo); diff --git a/common/nym_offline_compact_ecash/src/utils.rs b/common/nym_offline_compact_ecash/src/utils.rs new file mode 100644 index 0000000000..57a648fff2 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/utils.rs @@ -0,0 +1,404 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::common_types::SignerIndex; +use crate::error::{CompactEcashError, Result}; +use crate::scheme::setup::GroupParameters; +use crate::{ecash_group_parameters, Signature, VerificationKeyAuth}; +use bls12_381::hash_to_curve::{ExpandMsgXmd, HashToCurve, HashToField}; +use bls12_381::{ + multi_miller_loop, G1Affine, G1Projective, G2Affine, G2Prepared, G2Projective, Scalar, +}; +use core::iter::Sum; +use core::ops::Mul; +use ff::Field; +use group::{Curve, Group}; +use itertools::Itertools; +use std::borrow::Borrow; +use std::ops::Neg; + +pub struct Polynomial { + coefficients: Vec, +} + +impl Polynomial { + // for polynomial of degree n, we generate n+1 values + // (for example for degree 1, like y = x + 2, we need [2,1]) + pub fn new_random(params: &GroupParameters, degree: u64) -> Self { + Polynomial { + coefficients: params.n_random_scalars((degree + 1) as usize), + } + } + + /// Evaluates the polynomial at point x. + pub fn evaluate(&self, x: &Scalar) -> Scalar { + if self.coefficients.is_empty() { + 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().unwrap_u8() == 1 { + // we checked that coefficients are not empty so unwrap here is fine + #[allow(clippy::unwrap_used)] + *self.coefficients.first().unwrap() + } else { + self.coefficients + .iter() + .enumerate() + // coefficient[n] * x ^ n + .map(|(i, coefficient)| coefficient * x.pow(&[i as u64, 0, 0, 0])) + .sum() + } + } +} + +#[inline] +pub fn generate_lagrangian_coefficients_at_origin(points: &[u64]) -> Vec { + let x = Scalar::zero(); + + points + .iter() + .enumerate() + .map(|(i, point_i)| { + let mut numerator = Scalar::one(); + let mut denominator = Scalar::one(); + let xi = Scalar::from(*point_i); + + for (j, point_j) in points.iter().enumerate() { + if j != i { + let xj = Scalar::from(*point_j); + + // numerator = (x - xs[0]) * ... * (x - xs[j]), j != i + numerator *= x - xj; + + // denominator = (xs[i] - x[0]) * ... * (xs[i] - x[j]), j != i + denominator *= xi - xj; + } + } + // numerator / denominator + //SAFETY: denominator start as one, and (xi-xj) is guaranteed to be non zero, as we force i != j + numerator * denominator.invert().unwrap() + }) + .collect() +} + +/// Performs a Lagrange interpolation at the origin for a polynomial defined by `points` and `values`. +/// It can be used for Scalars, G1 and G2 points. +pub(crate) fn perform_lagrangian_interpolation_at_origin( + points: &[SignerIndex], + values: &[T], +) -> Result +where + T: Sum, + for<'a> &'a T: Mul, +{ + if points.is_empty() || values.is_empty() { + return Err(CompactEcashError::InterpolationSetSize); + } + + if points.len() != values.len() { + return Err(CompactEcashError::InterpolationSetSize); + } + + let coefficients = generate_lagrangian_coefficients_at_origin(points); + + Ok(coefficients + .into_iter() + .zip(values.iter()) + .map(|(coeff, val)| val * coeff) + .sum()) +} + +//domain name following https://www.rfc-editor.org/rfc/rfc9380.html#name-domain-separation-requireme recommendation +const G1_HASH_DOMAIN: &[u8] = b"NYMECASH-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_RO_"; +const SCALAR_HASH_DOMAIN: &[u8] = b"NYMECASH-V01-CS02-with-expander-SHA256"; + +pub fn hash_g1>(msg: M) -> G1Projective { + >>::hash_to_curve(msg, G1_HASH_DOMAIN) +} + +pub fn hash_to_scalar>(msg: M) -> Scalar { + let mut output = vec![Scalar::zero()]; + + Scalar::hash_to_field::>( + msg.as_ref(), + SCALAR_HASH_DOMAIN, + &mut output, + ); + output[0] +} + +pub fn try_deserialize_scalar_vec(expected_len: u64, bytes: &[u8]) -> Result> { + if bytes.len() != expected_len as usize * 32 { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "Scalar vector".into(), + expected: expected_len as usize * 32, + actual: bytes.len(), + }); + } + + let mut out = Vec::with_capacity(expected_len as usize); + for i in 0..expected_len as usize { + //SAFETY : casting 32 len slice into 32 len array + #[allow(clippy::unwrap_used)] + let s_bytes = bytes[i * 32..(i + 1) * 32].try_into().unwrap(); + let s = match Scalar::from_bytes(&s_bytes).into() { + None => return Err(CompactEcashError::ScalarDeserializationFailure), + Some(scalar) => scalar, + }; + out.push(s) + } + + Ok(out) +} + +pub fn try_deserialize_scalar(bytes: &[u8; 32]) -> Result { + Into::>::into(Scalar::from_bytes(bytes)) + .ok_or(CompactEcashError::ScalarDeserializationFailure) +} + +pub fn try_deserialize_g1_projective(bytes: &[u8; 48]) -> Result { + Into::>::into(G1Affine::from_compressed(bytes)) + .ok_or(CompactEcashError::G1ProjectiveDeserializationFailure) + .map(G1Projective::from) +} + +pub fn try_deserialize_g2_projective(bytes: &[u8; 96]) -> Result { + Into::>::into(G2Affine::from_compressed(bytes)) + .ok_or(CompactEcashError::G2ProjectiveDeserializationFailure) + .map(G2Projective::from) +} + +/// Checks whether e(P, Q) * e(-R, S) == id +pub fn check_bilinear_pairing(p: &G1Affine, q: &G2Prepared, r: &G1Affine, s: &G2Prepared) -> bool { + // checking e(P, Q) * e(-R, S) == id + // is equivalent to checking e(P, Q) == e(R, S) + // but requires only a single final exponentiation rather than two of them + // and therefore, as seen via benchmarks.rs, is almost 50% faster + // (1.47ms vs 2.45ms, tested on R9 5900X) + + let multi_miller = multi_miller_loop(&[(p, q), (&r.neg(), s)]); + multi_miller.final_exponentiation().is_identity().into() +} + +// compute e(h1, X1) * e(s1, g2^-1) * ... == id +// pub fn batch_verify_signatures(iter: Vec<(&Signature, G2Projective)>) -> bool { +pub fn batch_verify_signatures(iter: impl Iterator) -> bool +where + T: Borrow<(S, G2)>, + S: Borrow, + G2: Borrow, +{ + let mut miller_terms_owned = Vec::new(); + for t in iter { + let (sig, q) = t.borrow(); + let sig = sig.borrow(); + miller_terms_owned.push(( + sig.h.to_affine(), + G2Prepared::from(q.borrow().to_affine()), + sig.s.to_affine(), + )); + } + + let params = ecash_group_parameters(); + let g2_prep_neg = G2Prepared::from(params.gen2().neg()); + + let mut miller_terms = Vec::with_capacity(miller_terms_owned.len() * 2); + for (h, q, s) in &miller_terms_owned { + miller_terms.push((h, q)); + miller_terms.push((s, &g2_prep_neg)); + } + + multi_miller_loop(&miller_terms) + .final_exponentiation() + .is_identity() + .into() +} + +pub fn check_vk_pairing( + params: &GroupParameters, + dkg_values: &[G2Projective], + vk: &VerificationKeyAuth, +) -> 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; + } + + // safety: we made an explicit check for if the length of the slice is 0, thus unwrap here is fine + #[allow(clippy::unwrap_used)] + if &vk.alpha != *dkg_values.first().as_ref().unwrap() { + return false; + } + let dkg_betas = &dkg_values[1..]; + if dkg_betas + .iter() + .zip(vk.beta_g2.iter()) + .any(|(dkg_beta, vk_beta)| dkg_beta != vk_beta) + { + return false; + } + + let mut owned_miller_terms = vec![]; + for (i, (g1, g2)) in vk.beta_g1.iter().zip(vk.beta_g2.iter()).enumerate() { + if i % 2 == 0 { + owned_miller_terms.push((g1.to_affine(), G2Prepared::from(g2.to_affine()))); + } else { + // negate every other g1 element + owned_miller_terms.push((g1.neg().to_affine(), G2Prepared::from(g2.to_affine()))); + } + } + + // if our key has odd length, make sure to include the generators to correctly compute the final exponentiation + if owned_miller_terms.len() % 2 == 1 { + let neg_g1 = params.gen1().neg(); + let g2_prep = params.prepared_miller_g2(); + owned_miller_terms.push((neg_g1, g2_prep.to_owned())) + } + + let mut miller_terms = Vec::new(); + for ((p, s), (r, q)) in owned_miller_terms.iter().tuples::<(_, _)>() { + miller_terms.push((p, q)); + miller_terms.push((r, s)); + } + + // check if e(g1^x, g2^y) * e(g1^-y, g2^x) * ... == id + // in case of odd-length key check: + // check if e(g1^x, g2^y) * e(g1^-y, g2^x) * ... * e(g1^-z, g2) * e(g1^1, g2^z) == id + // (this is more than 2x as fast as checking each pairing individually for key of size 5) + multi_miller_loop(&miller_terms) + .final_exponentiation() + .is_identity() + .into() +} + +#[cfg(test)] +mod tests { + use rand::RngCore; + + use super::*; + + #[test] + fn polynomial_evaluation() { + // y = 42 (it should be 42 regardless of x) + let poly = Polynomial { + coefficients: vec![Scalar::from(42)], + }; + + assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(1))); + assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(0))); + assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(10))); + + // y = x + 10, at x = 2 (exp: 12) + let poly = Polynomial { + coefficients: vec![Scalar::from(10), Scalar::from(1)], + }; + + assert_eq!(Scalar::from(12), poly.evaluate(&Scalar::from(2))); + + // y = x^4 - 5x^2 + 2x - 3, at x = 3 (exp: 39) + let poly = Polynomial { + coefficients: vec![ + (-Scalar::from(3)), + Scalar::from(2), + (-Scalar::from(5)), + Scalar::zero(), + Scalar::from(1), + ], + }; + + assert_eq!(Scalar::from(39), poly.evaluate(&Scalar::from(3))); + + // empty polynomial + let poly = Polynomial { + coefficients: vec![], + }; + + // should always be 0 + assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(1))); + assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(0))); + assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(10))); + } + + #[test] + fn performing_lagrangian_scalar_interpolation_at_origin() { + // x^2 + 3 + // x, f(x): + // 1, 4, + // 2, 7, + // 3, 12, + let points = vec![1, 2, 3]; + let values = vec![Scalar::from(4), Scalar::from(7), Scalar::from(12)]; + + assert_eq!( + Scalar::from(3), + perform_lagrangian_interpolation_at_origin(&points, &values).unwrap() + ); + + // x^3 + 3x^2 - 5x + 11 + // x, f(x): + // 1, 10 + // 2, 21 + // 3, 50 + // 4, 103 + let points = vec![1, 2, 3, 4]; + let values = vec![ + Scalar::from(10), + Scalar::from(21), + Scalar::from(50), + Scalar::from(103), + ]; + + assert_eq!( + Scalar::from(11), + perform_lagrangian_interpolation_at_origin(&points, &values).unwrap() + ); + + // more points than it is required + // x^2 + x + 10 + // x, f(x) + // 1, 12 + // 2, 16 + // 3, 22 + // 4, 30 + // 5, 40 + let points = vec![1, 2, 3, 4, 5]; + let values = vec![ + Scalar::from(12), + Scalar::from(16), + Scalar::from(22), + Scalar::from(30), + Scalar::from(40), + ]; + + assert_eq!( + Scalar::from(10), + perform_lagrangian_interpolation_at_origin(&points, &values).unwrap() + ); + } + + #[test] + fn hash_g1_sanity_check() { + let mut rng = rand::thread_rng(); + let mut msg1 = [0u8; 1024]; + rng.fill_bytes(&mut msg1); + let mut msg2 = [0u8; 1024]; + rng.fill_bytes(&mut msg2); + + assert_eq!(hash_g1(msg1), hash_g1(msg1)); + assert_eq!(hash_g1(msg2), hash_g1(msg2)); + assert_ne!(hash_g1(msg1), hash_g1(msg2)); + } + + #[test] + fn hash_scalar_sanity_check() { + let mut rng = rand::thread_rng(); + let mut msg1 = [0u8; 1024]; + rng.fill_bytes(&mut msg1); + let mut msg2 = [0u8; 1024]; + rng.fill_bytes(&mut msg2); + + assert_eq!(hash_to_scalar(msg1), hash_to_scalar(msg1)); + assert_eq!(hash_to_scalar(msg2), hash_to_scalar(msg2)); + assert_ne!(hash_to_scalar(msg1), hash_to_scalar(msg2)); + } +} diff --git a/common/nymcoconut/benches/benchmarks.rs b/common/nymcoconut/benches/benchmarks.rs index e550791969..42fb36dc82 100644 --- a/common/nymcoconut/benches/benchmarks.rs +++ b/common/nymcoconut/benches/benchmarks.rs @@ -33,6 +33,40 @@ fn multi_miller_pairing_affine(g11: &G1Affine, g21: &G2Affine, g12: &G1Affine, g )) } +#[allow(unused)] +fn bench_pairings(c: &mut Criterion) { + let mut rng = rand::thread_rng(); + + let g1 = G1Affine::generator(); + let g2 = G2Affine::generator(); + let r = Scalar::random(&mut rng); + let s = Scalar::random(&mut rng); + + let g11 = (g1 * r).to_affine(); + let g21 = (g2 * s).to_affine(); + let g21_prep = G2Prepared::from(g21); + + let g12 = (g1 * s).to_affine(); + let g22 = (g2 * r).to_affine(); + let g22_prep = G2Prepared::from(g22); + + c.bench_function("double pairing", |b| { + b.iter(|| double_pairing(&g11, &g21, &g12, &g22)) + }); + + c.bench_function("multi miller in affine", |b| { + b.iter(|| multi_miller_pairing_affine(&g11, &g21, &g12, &g22)) + }); + + c.bench_function("multi miller with prepared g2", |b| { + b.iter(|| multi_miller_pairing_with_prepared(&g11, &g21_prep, &g12, &g22_prep)) + }); + + c.bench_function("multi miller with semi-prepared g2", |b| { + b.iter(|| multi_miller_pairing_with_semi_prepared(&g11, &g21, &g12, &g22_prep)) + }); +} + #[allow(unused)] fn multi_miller_pairing_with_prepared( g11: &G1Affine, @@ -125,43 +159,9 @@ impl BenchCase { } } -#[allow(unused)] -fn bench_pairings(c: &mut Criterion) { - let mut rng = rand::thread_rng(); - - let g1 = G1Affine::generator(); - let g2 = G2Affine::generator(); - let r = Scalar::random(&mut rng); - let s = Scalar::random(&mut rng); - - let g11 = (g1 * r).to_affine(); - let g21 = (g2 * s).to_affine(); - let g21_prep = G2Prepared::from(g21); - - let g12 = (g1 * s).to_affine(); - let g22 = (g2 * r).to_affine(); - let g22_prep = G2Prepared::from(g22); - - c.bench_function("double pairing", |b| { - b.iter(|| double_pairing(&g11, &g21, &g12, &g22)) - }); - - c.bench_function("multi miller in affine", |b| { - b.iter(|| multi_miller_pairing_affine(&g11, &g21, &g12, &g22)) - }); - - c.bench_function("multi miller with prepared g2", |b| { - b.iter(|| multi_miller_pairing_with_prepared(&g11, &g21_prep, &g12, &g22_prep)) - }); - - c.bench_function("multi miller with semi-prepared g2", |b| { - b.iter(|| multi_miller_pairing_with_semi_prepared(&g11, &g21, &g12, &g22_prep)) - }); -} - fn bench_coconut(c: &mut Criterion) { let mut group = c.benchmark_group("benchmark-coconut"); - group.measurement_time(Duration::from_secs(100)); + group.measurement_time(Duration::from_secs(1000)); let case = BenchCase { num_authorities: 100, threshold_p: 0.7, @@ -184,7 +184,7 @@ fn bench_coconut(c: &mut Criterion) { // Let's benchmark the operations the client has to perform // to ask for a credential group.bench_function( - &format!( + format!( "[Client] prepare_blind_sign_{}_authorities_{}_attributes_{}_threshold", case.num_authorities, case.num_attrs(), @@ -205,7 +205,7 @@ fn bench_coconut(c: &mut Criterion) { let keypair = coconut_keypairs.choose(&mut rng).unwrap(); group.bench_function( - &format!( + format!( "[Validator] compute_single_blind_sign_for_credential_with_{}_attributes", case.num_attrs(), ), @@ -247,7 +247,7 @@ fn bench_coconut(c: &mut Criterion) { let partial_verification_key = verification_keys.get(rand_idx).unwrap(); group.bench_function( - &format!( + format!( "verify_partial_blind_signature_{}_private_attributes_{}_public_attributes", case.num_private_attrs, case.num_public_attrs ), @@ -284,7 +284,7 @@ fn bench_coconut(c: &mut Criterion) { // CLIENT BENCHMARK: aggregate all partial credentials group.bench_function( - &format!( + format!( "[Client] unblind_and_aggregate_partial_credentials_{}_authorities_{}_attributes_{}_threshold", case.num_authorities, case.num_attrs(), @@ -317,7 +317,7 @@ fn bench_coconut(c: &mut Criterion) { // CLIENT BENCHMARK group.bench_function( - &format!( + format!( "[Client] randomize_and_prove_credential_{}_authorities_{}_attributes_{}_threshold", case.num_authorities, case.num_attrs(), @@ -343,7 +343,7 @@ fn bench_coconut(c: &mut Criterion) { // VERIFICATION BENCHMARK group.bench_function( - &format!( + format!( "[Verifier] verify_credentials_{}_authorities_{}_attributes_{}_threshold", case.num_authorities, case.num_attrs(), diff --git a/common/nymcoconut/src/lib.rs b/common/nymcoconut/src/lib.rs index 3eed2bf88c..dbcbda5b39 100644 --- a/common/nymcoconut/src/lib.rs +++ b/common/nymcoconut/src/lib.rs @@ -47,7 +47,7 @@ mod proofs; mod scheme; pub mod tests; mod traits; -mod utils; +pub mod utils; pub type Attribute = bls12_381::Scalar; pub type PrivateAttribute = Attribute; diff --git a/common/nymcoconut/src/utils.rs b/common/nymcoconut/src/utils.rs index 819156cb23..d12707408c 100644 --- a/common/nymcoconut/src/utils.rs +++ b/common/nymcoconut/src/utils.rs @@ -122,7 +122,7 @@ const G1_HASH_DOMAIN: &[u8] = b"QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_R // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#appendix-K.1 const SCALAR_HASH_DOMAIN: &[u8] = b"QUUX-V01-CS02-with-expander"; -pub(crate) fn hash_g1>(msg: M) -> G1Projective { +pub fn hash_g1>(msg: M) -> G1Projective { >>::hash_to_curve(msg, G1_HASH_DOMAIN) } @@ -137,7 +137,7 @@ pub fn hash_to_scalar>(msg: M) -> Scalar { output[0] } -pub(crate) fn try_deserialize_scalar_vec( +pub fn try_deserialize_scalar_vec( expected_len: u64, bytes: &[u8], err: CoconutError, @@ -161,23 +161,17 @@ pub(crate) fn try_deserialize_scalar_vec( Ok(out) } -pub(crate) fn try_deserialize_scalar(bytes: &[u8; 32], err: CoconutError) -> Result { +pub fn try_deserialize_scalar(bytes: &[u8; 32], err: CoconutError) -> Result { Into::>::into(Scalar::from_bytes(bytes)).ok_or(err) } -pub(crate) fn try_deserialize_g1_projective( - bytes: &[u8; 48], - err: CoconutError, -) -> Result { +pub fn try_deserialize_g1_projective(bytes: &[u8; 48], err: CoconutError) -> Result { Into::>::into(G1Affine::from_compressed(bytes)) .ok_or(err) .map(G1Projective::from) } -pub(crate) fn try_deserialize_g2_projective( - bytes: &[u8; 96], - err: CoconutError, -) -> Result { +pub fn try_deserialize_g2_projective(bytes: &[u8; 96], err: CoconutError) -> Result { Into::>::into(G2Affine::from_compressed(bytes)) .ok_or(err) .map(G2Projective::from) diff --git a/common/pemstore/src/lib.rs b/common/pemstore/src/lib.rs index a89bc628c5..bd36158229 100644 --- a/common/pemstore/src/lib.rs +++ b/common/pemstore/src/lib.rs @@ -9,7 +9,7 @@ use std::path::{Path, PathBuf}; pub mod traits; -#[derive(Debug)] +#[derive(Debug, Default)] pub struct KeyPairPath { pub private_key_path: PathBuf, pub public_key_path: PathBuf, diff --git a/common/serde-helpers/Cargo.toml b/common/serde-helpers/Cargo.toml new file mode 100644 index 0000000000..1cb5c76fb7 --- /dev/null +++ b/common/serde-helpers/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "nym-serde-helpers" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +serde = { workspace = true } +bs58 = { workspace = true, optional = true } +base64 = { workspace = true, optional = true } + + +[features] +bs58 = ["dep:bs58"] +base64 = ["dep:base64"] diff --git a/common/serde-helpers/src/lib.rs b/common/serde-helpers/src/lib.rs new file mode 100644 index 0000000000..f90a65a2f4 --- /dev/null +++ b/common/serde-helpers/src/lib.rs @@ -0,0 +1,33 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(feature = "base64")] +pub mod base64 { + use base64::{engine::general_purpose::STANDARD, Engine as _}; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(bytes: &[u8], serializer: S) -> Result { + serializer.serialize_str(&STANDARD.encode(bytes)) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { + let s = ::deserialize(deserializer)?; + STANDARD.decode(s).map_err(serde::de::Error::custom) + } +} + +#[cfg(feature = "bs58")] +pub mod bs58 { + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(bytes: &[u8], serializer: S) -> Result { + serializer.serialize_str(&::bs58::encode(bytes).into_string()) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { + let s = String::deserialize(deserializer)?; + ::bs58::decode(&s) + .into_vec() + .map_err(serde::de::Error::custom) + } +} diff --git a/common/socks5-client-core/Cargo.toml b/common/socks5-client-core/Cargo.toml index 290016a332..ce8421f3d7 100644 --- a/common/socks5-client-core/Cargo.toml +++ b/common/socks5-client-core/Cargo.toml @@ -22,7 +22,7 @@ tokio = { workspace = true, features = ["rt-multi-thread", "net", "signal"] } url = { workspace = true } nym-bandwidth-controller = { path = "../../common/bandwidth-controller" } -nym-client-core = { path = "../client-core", features = ["fs-surb-storage", "fs-gateways-storage"] } +nym-client-core = { path = "../client-core", features = ["fs-credentials-storage", "fs-surb-storage", "fs-gateways-storage"] } nym-config = { path = "../config" } nym-contracts-common = { path = "../cosmwasm-smart-contracts/contracts-common" } nym-credential-storage = { path = "../credential-storage" } diff --git a/common/task/src/manager.rs b/common/task/src/manager.rs index ab03870dc0..8a4c21a311 100644 --- a/common/task/src/manager.rs +++ b/common/task/src/manager.rs @@ -3,7 +3,6 @@ use futures::{future::pending, FutureExt, SinkExt, StreamExt}; use log::{log, Level}; -use std::future::Future; use std::sync::atomic::{AtomicBool, Ordering}; use std::{error::Error, time::Duration}; use tokio::sync::{ @@ -368,17 +367,6 @@ impl TaskClient { self.named(name) } - pub async fn run_future(&mut self, fut: Fut) -> Option - where - Fut: Future, - { - tokio::select! { - biased; - _ = self.recv() => None, - res = fut => Some(res) - } - } - // Create a dummy that will never report that we should shutdown. pub fn dummy() -> TaskClient { let (_notify_tx, notify_rx) = watch::channel(()); diff --git a/common/types/src/error.rs b/common/types/src/error.rs index 4546731ff4..2a4595a495 100644 --- a/common/types/src/error.rs +++ b/common/types/src/error.rs @@ -1,3 +1,4 @@ +use nym_mixnet_contract_common::ContractsCommonError; use nym_validator_client::error::TendermintRpcError; use nym_validator_client::nym_api::error::NymAPIError; use nym_validator_client::{nyxd::error::NyxdError, ValidatorClientError}; @@ -8,6 +9,9 @@ use thiserror::Error; // TODO: ask @MS why this even exists #[derive(Error, Debug)] pub enum TypesError { + #[error(transparent)] + ContractsCommon(#[from] ContractsCommonError), + #[error("{source}")] NyxdError { #[from] diff --git a/common/types/src/pending_events.rs b/common/types/src/pending_events.rs index f5789890bf..d36e0b978c 100644 --- a/common/types/src/pending_events.rs +++ b/common/types/src/pending_events.rs @@ -84,6 +84,7 @@ impl PendingEpochEventData { mix_id, amount, proxy, + .. } => Ok(PendingEpochEventData::Delegate { owner: owner.into_string(), mix_id, @@ -94,6 +95,7 @@ impl PendingEpochEventData { owner, mix_id, proxy, + .. } => Ok(PendingEpochEventData::Undelegate { owner: owner.into_string(), mix_id, diff --git a/common/types/src/transaction.rs b/common/types/src/transaction.rs index a7e4176d7a..9179a0170b 100644 --- a/common/types/src/transaction.rs +++ b/common/types/src/transaction.rs @@ -65,7 +65,7 @@ impl TransactionDetails { #[derive(Deserialize, Serialize, Debug)] pub struct TransactionExecuteResult { pub logs_json: String, - pub data_json: String, + pub msg_responses_json: String, pub transaction_hash: String, pub gas_info: GasInfo, pub fee: Option, @@ -79,7 +79,7 @@ impl TransactionExecuteResult { Ok(TransactionExecuteResult { gas_info: value.gas_info.into(), transaction_hash: value.transaction_hash.to_string(), - data_json: ::serde_json::to_string_pretty(&value.data)?, + msg_responses_json: ::serde_json::to_string_pretty(&value.msg_responses)?, logs_json: ::serde_json::to_string_pretty(&value.logs)?, fee, }) diff --git a/common/wasm/client-core/src/lib.rs b/common/wasm/client-core/src/lib.rs index e3f85e41aa..c985e92587 100644 --- a/common/wasm/client-core/src/lib.rs +++ b/common/wasm/client-core/src/lib.rs @@ -18,7 +18,9 @@ pub use nym_client_core::*; pub use nym_client_core::{ client::key_manager::ClientKeys, error::ClientCoreError, init::types::InitialisationResult, }; -pub use nym_gateway_client::{error::GatewayClientError, GatewayClient, GatewayConfig}; +pub use nym_gateway_client::{ + error::GatewayClientError, GatewayClient, GatewayClientConfig, GatewayConfig, +}; pub use nym_sphinx::{ addressing::{clients::Recipient, nodes::NodeIdentity}, params::PacketType, diff --git a/common/wireguard-types/src/error.rs b/common/wireguard-types/src/error.rs index 3584a056a6..0bd4a8d3f6 100644 --- a/common/wireguard-types/src/error.rs +++ b/common/wireguard-types/src/error.rs @@ -32,7 +32,4 @@ pub enum Error { #[source] source: hmac::digest::MacError, }, - - #[error("peers can't be modified anymore")] - PeerModifyStopped, } diff --git a/common/wireguard-types/src/lib.rs b/common/wireguard-types/src/lib.rs index 3c2cc66951..18a01cd9e7 100644 --- a/common/wireguard-types/src/lib.rs +++ b/common/wireguard-types/src/lib.rs @@ -9,9 +9,7 @@ pub mod registration; pub use config::Config; pub use error::Error; pub use public_key::PeerPublicKey; -pub use registration::{ - ClientMac, ClientMessage, GatewayClient, GatewayClientRegistry, InitMessage, Nonce, -}; +pub use registration::{ClientMac, ClientMessage, GatewayClient, InitMessage, Nonce}; #[cfg(feature = "verify")] pub use registration::HmacSha256; diff --git a/common/wireguard-types/src/registration.rs b/common/wireguard-types/src/registration.rs index ae5a758d90..9d2d182f6d 100644 --- a/common/wireguard-types/src/registration.rs +++ b/common/wireguard-types/src/registration.rs @@ -17,7 +17,6 @@ use nym_crypto::asymmetric::encryption::PrivateKey; #[cfg(feature = "verify")] use sha2::Sha256; -pub type GatewayClientRegistry = DashMap; pub type PendingRegistrations = DashMap; pub type PrivateIPs = DashMap; diff --git a/common/wireguard/Cargo.toml b/common/wireguard/Cargo.toml index 51a0b115a1..f90708a68d 100644 --- a/common/wireguard/Cargo.toml +++ b/common/wireguard/Cargo.toml @@ -12,6 +12,7 @@ license.workspace = true [dependencies] base64 = { workspace = true } +bincode = { workspace = true } chrono = { workspace = true } dashmap = { workspace = true } defguard_wireguard_rs = { workspace = true } @@ -22,6 +23,7 @@ x25519-dalek = { workspace = true } ip_network = { workspace = true } log.workspace = true nym-crypto = { path = "../crypto", features = ["asymmetric"] } +nym-gateway-storage = { path = "../gateway-storage" } nym-network-defaults = { path = "../network-defaults" } nym-task = { path = "../task" } nym-wireguard-types = { path = "../wireguard-types" } diff --git a/common/wireguard/src/error.rs b/common/wireguard/src/error.rs index da7452614f..bd5fa06399 100644 --- a/common/wireguard/src/error.rs +++ b/common/wireguard/src/error.rs @@ -8,4 +8,7 @@ pub enum Error { #[error("{0}")] Defguard(#[from] defguard_wireguard_rs::error::WireguardInterfaceError), + + #[error("{0}")] + GatewayStorage(#[from] nym_gateway_storage::error::StorageError), } diff --git a/common/wireguard/src/lib.rs b/common/wireguard/src/lib.rs index 6244320e54..fa1c22b7cb 100644 --- a/common/wireguard/src/lib.rs +++ b/common/wireguard/src/lib.rs @@ -6,10 +6,11 @@ // #![warn(clippy::expect_used)] // #![warn(clippy::unwrap_used)] -use dashmap::DashMap; -use defguard_wireguard_rs::{host::Peer, key::Key, net::IpAddrMask, WGApi}; +use defguard_wireguard_rs::WGApi; +#[cfg(target_os = "linux")] +use defguard_wireguard_rs::{host::Peer, key::Key, net::IpAddrMask}; use nym_crypto::asymmetric::encryption::KeyPair; -use nym_wireguard_types::{Config, Error, GatewayClient, GatewayClientRegistry, PeerPublicKey}; +use nym_wireguard_types::Config; use peer_controller::PeerControlRequest; use std::sync::Arc; use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; @@ -42,7 +43,6 @@ impl Drop for WgApiWrapper { pub struct WireguardGatewayData { config: Config, keypair: Arc, - client_registry: Arc, peer_tx: UnboundedSender, } @@ -56,7 +56,6 @@ impl WireguardGatewayData { WireguardGatewayData { config, keypair, - client_registry: Arc::new(DashMap::default()), peer_tx, }, peer_rx, @@ -71,28 +70,8 @@ impl WireguardGatewayData { &self.keypair } - pub fn client_registry(&self) -> &Arc { - &self.client_registry - } - - pub fn add_peer(&self, client: &GatewayClient) -> Result<(), Error> { - let mut peer = Peer::new(Key::new(client.pub_key.to_bytes())); - peer.allowed_ips - .push(IpAddrMask::new(client.private_ip, 32)); - let msg = PeerControlRequest::AddPeer(peer); - self.peer_tx.send(msg).map_err(|_| Error::PeerModifyStopped) - } - - pub fn remove_peer(&self, client: &GatewayClient) -> Result<(), Error> { - let key = Key::new(client.pub_key().to_bytes()); - let msg = PeerControlRequest::RemovePeer(key); - self.peer_tx.send(msg).map_err(|_| Error::PeerModifyStopped) - } - - pub fn query_bandwidth(&self, peer_public_key: PeerPublicKey) -> Result<(), Error> { - let key = Key::new(peer_public_key.to_bytes()); - let msg = PeerControlRequest::QueryBandwidth(key); - self.peer_tx.send(msg).map_err(|_| Error::PeerModifyStopped) + pub fn peer_tx(&self) -> &UnboundedSender { + &self.peer_tx } } @@ -103,7 +82,8 @@ pub struct WireguardData { /// Start wireguard device #[cfg(target_os = "linux")] -pub async fn start_wireguard( +pub async fn start_wireguard( + storage: St, task_client: nym_task::TaskClient, wireguard_data: WireguardData, control_tx: UnboundedSender, @@ -114,11 +94,15 @@ pub async fn start_wireguard( use peer_controller::PeerController; let mut peers = vec![]; - for peer_client in wireguard_data.inner.client_registry().iter() { - let mut peer = Peer::new(Key::new(peer_client.pub_key.to_bytes())); - let peer_ip_mask = IpAddrMask::new(peer_client.private_ip, 32); - peer.set_allowed_ips(vec![peer_ip_mask]); - peers.push(peer); + let mut suspended_peers = vec![]; + for storage_peer in storage.get_all_wireguard_peers().await? { + let suspended = storage_peer.suspended; + let peer = Peer::try_from(storage_peer)?; + if suspended { + suspended_peers.push(peer); + } else { + peers.push(peer); + } } let ifname = String::from(WG_TUN_NAME); @@ -147,8 +131,10 @@ pub async fn start_wireguard( let wg_api = std::sync::Arc::new(WgApiWrapper::new(wg_api)); let mut controller = PeerController::new( + storage, wg_api.clone(), interface_config.peers, + suspended_peers, wireguard_data.peer_rx, control_tx, ); diff --git a/common/wireguard/src/peer_controller.rs b/common/wireguard/src/peer_controller.rs index fa11aa8a71..ad7628c8b8 100644 --- a/common/wireguard/src/peer_controller.rs +++ b/common/wireguard/src/peer_controller.rs @@ -3,6 +3,7 @@ use chrono::{Timelike, Utc}; use defguard_wireguard_rs::{host::Peer, key::Key, WireguardInterfaceApi}; +use nym_gateway_storage::Storage; use nym_wireguard_types::registration::{RemainingBandwidthData, BANDWIDTH_CAP_PER_DAY}; use std::time::SystemTime; use std::{collections::HashMap, sync::Arc, time::Duration}; @@ -20,6 +21,7 @@ const DEFAULT_PEER_TIMEOUT_CHECK: Duration = Duration::from_secs(60); // 1 minut pub enum PeerControlRequest { AddPeer(Peer), RemovePeer(Key), + QueryPeer(Key), QueryBandwidth(Key), } @@ -30,12 +32,17 @@ pub enum PeerControlResponse { RemovePeer { success: bool, }, + QueryPeer { + success: bool, + peer: Option, + }, QueryBandwidth { bandwidth_data: Option, }, } -pub struct PeerController { +pub struct PeerController { + storage: St, request_rx: mpsc::UnboundedReceiver, response_tx: mpsc::UnboundedSender, wg_api: Arc, @@ -45,36 +52,55 @@ pub struct PeerController { last_seen_bandwidth: HashMap, } -impl PeerController { +impl PeerController { pub fn new( + storage: St, wg_api: Arc, peers: Vec, + suspended_peers: Vec, request_rx: mpsc::UnboundedReceiver, response_tx: mpsc::UnboundedSender, ) -> Self { let timeout_check_interval = tokio_stream::wrappers::IntervalStream::new( tokio::time::interval(DEFAULT_PEER_TIMEOUT_CHECK), ); - let active_peers = peers + let active_peers: HashMap = peers .into_iter() .map(|peer| (peer.public_key.clone(), peer)) .collect(); + let suspended_peers: HashMap = suspended_peers + .into_iter() + .map(|peer| (peer.public_key.clone(), peer)) + .collect(); + let last_seen_bandwidth = active_peers + .iter() + .map(|(k, p)| (k.clone(), p.rx_bytes + p.tx_bytes)) + .chain(suspended_peers.keys().map(|k| (k.clone(), 0))) + .collect(); PeerController { + storage, wg_api, request_rx, response_tx, timeout_check_interval, active_peers, - suspended_peers: HashMap::new(), - last_seen_bandwidth: HashMap::new(), + suspended_peers, + last_seen_bandwidth, } } - fn check_stale_peer(&self, peer: &Peer, current_timestamp: SystemTime) -> Result { + async fn check_stale_peer( + &self, + peer: &Peer, + current_timestamp: SystemTime, + ) -> Result { if let Some(timestamp) = peer.last_handshake { if let Ok(duration_since_handshake) = current_timestamp.duration_since(timestamp) { if duration_since_handshake > DEFAULT_PEER_TIMEOUT { + self.storage + .remove_wireguard_peer(&peer.public_key.to_string()) + .await?; self.wg_api.inner.remove_peer(&peer.public_key)?; return Ok(true); } @@ -84,7 +110,7 @@ impl PeerController { Ok(false) } - fn check_suspend_peer(&mut self, peer: &Peer) -> Result<(), Error> { + async fn check_suspend_peer(&mut self, peer: &Peer) -> Result<(), Error> { let prev_peer = self .active_peers .get(&peer.public_key) @@ -92,17 +118,21 @@ impl PeerController { let data_usage = (peer.rx_bytes + peer.tx_bytes).saturating_sub(prev_peer.rx_bytes + prev_peer.tx_bytes); if data_usage > BANDWIDTH_CAP_PER_DAY { + self.storage.insert_wireguard_peer(peer, true).await?; self.wg_api.inner.remove_peer(&peer.public_key)?; - let (moved_key, moved_peer) = self - .active_peers + self.active_peers .remove_entry(&peer.public_key) .ok_or(Error::PeerMismatch)?; - self.suspended_peers.insert(moved_key, moved_peer); + self.suspended_peers + .insert(peer.public_key.clone(), peer.clone()); + } else { + // Update peer stored data + self.storage.insert_wireguard_peer(peer, false).await?; } Ok(()) } - fn check_peers(&mut self) -> Result<(), Error> { + async fn check_peers(&mut self) -> Result<(), Error> { // Add 10 seconds to cover edge cases. At worst, we give ten free seconds worth of bandwidth // by resetting the bandwidth twice let reset = Utc::now().num_seconds_from_midnight() as u64 @@ -121,11 +151,21 @@ impl PeerController { .collect(); if reset { self.active_peers = host.peers; + for peer in self.active_peers.values() { + self.storage.insert_wireguard_peer(peer, false).await?; + } } else { + let peers = self + .storage + .get_all_wireguard_peers() + .await? + .into_iter() + .map(Peer::try_from) + .collect::, _>>()?; let current_timestamp = SystemTime::now(); - for peer in host.peers.values() { - if !self.check_stale_peer(peer, current_timestamp)? { - self.check_suspend_peer(peer)?; + for peer in peers { + if !self.check_stale_peer(&peer, current_timestamp).await? { + self.check_suspend_peer(&peer).await?; } } } @@ -137,7 +177,7 @@ impl PeerController { loop { tokio::select! { _ = self.timeout_check_interval.next() => { - if let Err(e) = self.check_peers() { + if let Err(e) = self.check_peers().await { log::error!("Error while periodically checking peers: {:?}", e); } } @@ -148,16 +188,27 @@ impl PeerController { msg = self.request_rx.recv() => { match msg { Some(PeerControlRequest::AddPeer(peer)) => { + if let Err(e) = self.storage.insert_wireguard_peer(&peer, false).await { + log::error!("Could not insert peer into storage: {:?}", e); + self.response_tx.send(PeerControlResponse::AddPeer { success: false }).ok(); + continue; + } let success = if let Err(e) = self.wg_api.inner.configure_peer(&peer) { log::error!("Could not configure peer: {:?}", e); false } else { + self.last_seen_bandwidth.insert(peer.public_key.clone(), peer.rx_bytes + peer.tx_bytes); self.active_peers.insert(peer.public_key.clone(), peer); true }; self.response_tx.send(PeerControlResponse::AddPeer { success }).ok(); } Some(PeerControlRequest::RemovePeer(peer_pubkey)) => { + if let Err(e) = self.storage.remove_wireguard_peer(&peer_pubkey.to_string()).await { + log::error!("Could not remove peer from storage: {:?}", e); + self.response_tx.send(PeerControlResponse::RemovePeer { success: false }).ok(); + continue; + } let success = if let Err(e) = self.wg_api.inner.remove_peer(&peer_pubkey) { log::error!("Could not remove peer: {:?}", e); false @@ -168,6 +219,25 @@ impl PeerController { }; self.response_tx.send(PeerControlResponse::RemovePeer { success }).ok(); } + Some(PeerControlRequest::QueryPeer(peer_pubkey)) => { + let (success, peer) = match self.storage.get_wireguard_peer(&peer_pubkey.to_string()).await { + Err(e) => { + log::error!("Could not query peer storage {e}"); + (false, None) + }, + Ok(None) => (true, None), + Ok(Some(storage_peer)) => { + match Peer::try_from(storage_peer) { + Ok(peer) => (true, Some(peer)), + Err(e) => { + log::error!("Could not parse storage peer {e}"); + (false, None) + } + } + }, + }; + self.response_tx.send(PeerControlResponse::QueryPeer { success, peer }).ok(); + } Some(PeerControlRequest::QueryBandwidth(peer_pubkey)) => { let msg = if self.suspended_peers.contains_key(&peer_pubkey) { PeerControlResponse::QueryBandwidth { bandwidth_data: Some(RemainingBandwidthData{ available_bandwidth: 0, suspended: true }) } diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index c1440b1c6f..f2e9d29ad2 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -242,6 +242,21 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const_panic" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6051f239ecec86fde3410901ab7860d458d160371533842974fc61f96d15879b" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cosmwasm-crypto" version = "1.4.3" @@ -438,9 +453,9 @@ dependencies = [ [[package]] name = "cw-multi-test" -version = "0.16.4" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a18afd2e201221c6d72a57f0886ef2a22151bbc9e6db7af276fde8a91081042" +checksum = "127c7bb95853b8e828bdab97065c81cb5ddc20f7339180b61b2300565aaa99d1" dependencies = [ "anyhow", "cosmwasm-std", @@ -744,7 +759,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" dependencies = [ "curve25519-dalek 3.2.0", - "hashbrown", + "hashbrown 0.12.3", "hex", "rand_core 0.6.4", "serde", @@ -797,6 +812,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "ff" version = "0.12.1" @@ -891,6 +912,12 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hex" version = "0.4.3" @@ -931,6 +958,16 @@ dependencies = [ "serde", ] +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown 0.14.5", +] + [[package]] name = "inout" version = "0.1.3" @@ -987,6 +1024,33 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28" +[[package]] +name = "konst" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a0ba6de5f7af397afff922f22c149ff605c766cd3269cf6c1cd5e466dbe3b9" +dependencies = [ + "const_panic", + "konst_kernel", + "konst_proc_macros", + "typewit", +] + +[[package]] +name = "konst_kernel" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0a455a1719220fd6adf756088e1c69a85bf14b6a9e24537a5cc04f503edb2b" +dependencies = [ + "typewit", +] + +[[package]] +name = "konst_proc_macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e28ab1dc35e09d60c2b8c90d12a9a8d9666c876c10a3739a3196db0103b6043" + [[package]] name = "libc" version = "0.2.153" @@ -1151,6 +1215,47 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-ecash" +version = "0.1.0" +dependencies = [ + "bs58 0.4.0", + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-controllers", + "cw-storage-plus", + "cw-utils", + "cw2", + "cw3", + "cw4", + "nym-contracts-common", + "nym-crypto", + "nym-ecash-contract-common", + "nym-multisig-contract-common", + "nym-network-defaults", + "rand_chacha", + "schemars", + "semver", + "serde", + "sylvia", + "thiserror", +] + +[[package]] +name = "nym-ecash-contract-common" +version = "0.1.0" +dependencies = [ + "bs58 0.5.1", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-utils", + "cw2", + "nym-multisig-contract-common", + "thiserror", +] + [[package]] name = "nym-group-contract-common" version = "0.1.0" @@ -1171,6 +1276,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", + "cw-controllers", "cw-storage-plus", "cw2", "nym-contracts-common", @@ -1190,6 +1296,7 @@ dependencies = [ "bs58 0.4.0", "cosmwasm-schema", "cosmwasm-std", + "cw-controllers", "cw2", "humantime-serde", "log", @@ -1217,6 +1324,10 @@ dependencies = [ "thiserror", ] +[[package]] +name = "nym-network-defaults" +version = "0.1.0" + [[package]] name = "nym-pemstore" version = "0.3.0" @@ -1333,6 +1444,40 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.81" @@ -1561,6 +1706,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-cw-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75d32da6b8ed758b7d850b6c3c08f1d7df51a4df3cb201296e63e34a78e99d4" +dependencies = [ + "serde", +] + [[package]] name = "serde-json-wasm" version = "0.5.0" @@ -1724,6 +1878,39 @@ dependencies = [ "zeroize", ] +[[package]] +name = "sylvia" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f33388920659b494dab887f3bb40ebb071c602750597575034bea7c63ab12800" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test", + "derivative", + "konst", + "schemars", + "serde", + "serde-cw-value", + "serde-json-wasm", + "sylvia-derive", +] + +[[package]] +name = "sylvia-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8031f53dbfda341acd7bd321e10d0d684b673324145026e23705da4b6d5c4919" +dependencies = [ + "convert_case", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "syn" version = "1.0.109" @@ -1814,18 +2001,56 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "toml_datetime" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "typewit" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fb9ae6a3cafaf0a5d14c2302ca525f9ae8e07a0f0e6949de88d882c37a6e24" +dependencies = [ + "typewit_proc_macros", +] + +[[package]] +name = "typewit_proc_macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + [[package]] name = "vergen" version = "8.3.1" @@ -1853,6 +2078,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "x25519-dalek" version = "2.0.1" diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 175bbcc767..a7c49c9099 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -3,7 +3,8 @@ resolver = "2" members = [ "coconut-bandwidth", "coconut-dkg", - "coconut-test", + "coconut-test", + "ecash", "mixnet", "mixnet-vesting-integration-tests", "multisig/cw3-flex-multisig", @@ -38,7 +39,7 @@ cosmwasm-schema = "=1.4.3" cosmwasm-std = "=1.4.3" cosmwasm-storage = "=1.4.3" cw-controllers = "=1.1.0" -cw-multi-test = "=0.16.4" +cw-multi-test = "=0.16.5" cw-storage-plus = "=1.2.0" cw-utils = "=1.0.1" cw2 = "=1.1.2" @@ -48,5 +49,7 @@ cw4 = "=1.1.2" cw20 = "=1.1.2" semver = "1.0.21" serde = "1.0.196" +sylvia = "0.8.0" +schemars = "0.8.16" thiserror = "1.0.48" diff --git a/contracts/Makefile b/contracts/Makefile index 519bab061f..e9ff461be0 100644 --- a/contracts/Makefile +++ b/contracts/Makefile @@ -1,4 +1,4 @@ -schema: coconut-bandwidth-schema coconut-dkg-schema mixnet-schema vesting-schema multisig-schema group-schema +schema: coconut-bandwidth-schema coconut-dkg-schema mixnet-schema vesting-schema multisig-schema group-schema ecash-schema coconut-bandwidth-schema: $(MAKE) -C coconut-bandwidth generate-schema @@ -12,6 +12,9 @@ mixnet-schema: vesting-schema: $(MAKE) -C vesting generate-schema +ecash-schema: + $(MAKE) -C ecash generate-schema + multisig-schema: $(MAKE) -C multisig/cw3-flex-multisig generate-schema diff --git a/contracts/coconut-dkg/schema/nym-coconut-dkg.json b/contracts/coconut-dkg/schema/nym-coconut-dkg.json index 43f3a69975..b33212190b 100644 --- a/contracts/coconut-dkg/schema/nym-coconut-dkg.json +++ b/contracts/coconut-dkg/schema/nym-coconut-dkg.json @@ -374,6 +374,29 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "get_epoch_threshold" + ], + "properties": { + "get_epoch_threshold": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -1843,6 +1866,13 @@ } } }, + "get_epoch_threshold": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "uint64", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, "get_registered_dealer": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "RegisteredDealerDetails", diff --git a/contracts/coconut-dkg/schema/raw/query.json b/contracts/coconut-dkg/schema/raw/query.json index 72e1e095cd..386632565c 100644 --- a/contracts/coconut-dkg/schema/raw/query.json +++ b/contracts/coconut-dkg/schema/raw/query.json @@ -41,6 +41,29 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "get_epoch_threshold" + ], + "properties": { + "get_epoch_threshold": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ diff --git a/contracts/coconut-dkg/schema/raw/response_to_get_epoch_threshold.json b/contracts/coconut-dkg/schema/raw/response_to_get_epoch_threshold.json new file mode 100644 index 0000000000..7b729a7b96 --- /dev/null +++ b/contracts/coconut-dkg/schema/raw/response_to_get_epoch_threshold.json @@ -0,0 +1,7 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "uint64", + "type": "integer", + "format": "uint64", + "minimum": 0.0 +} diff --git a/contracts/coconut-dkg/src/contract.rs b/contracts/coconut-dkg/src/contract.rs index a1d84337af..acc1917b8d 100644 --- a/contracts/coconut-dkg/src/contract.rs +++ b/contracts/coconut-dkg/src/contract.rs @@ -13,8 +13,9 @@ use crate::dealings::queries::{ use crate::dealings::transactions::{try_commit_dealings_chunk, try_submit_dealings_metadata}; use crate::epoch_state::queries::{ query_can_advance_state, query_current_epoch, query_current_epoch_threshold, + query_epoch_threshold, }; -use crate::epoch_state::storage::CURRENT_EPOCH; +use crate::epoch_state::storage::{CURRENT_EPOCH, EPOCH_THRESHOLDS, THRESHOLD}; use crate::epoch_state::transactions::{ try_advance_epoch_state, try_initiate_dkg, try_trigger_reset, try_trigger_resharing, }; @@ -134,6 +135,9 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result { to_binary(&query_current_epoch_threshold(deps.storage)?)? } + QueryMsg::GetEpochThreshold { epoch_id } => { + to_binary(&query_epoch_threshold(deps.storage, epoch_id)?)? + } QueryMsg::GetRegisteredDealer { dealer_address, epoch_id, @@ -217,6 +221,13 @@ pub fn migrate(deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result // SPDX-License-Identifier: Apache-2.0 -use crate::epoch_state::storage::{CURRENT_EPOCH, THRESHOLD}; +use crate::epoch_state::storage::{CURRENT_EPOCH, EPOCH_THRESHOLDS, THRESHOLD}; use crate::epoch_state::utils::check_state_completion; use crate::error::ContractError; use cosmwasm_std::{Env, Storage}; -use nym_coconut_dkg_common::types::{Epoch, EpochState, StateAdvanceResponse}; +use nym_coconut_dkg_common::types::{Epoch, EpochId, EpochState, StateAdvanceResponse}; pub(crate) fn query_can_advance_state( storage: &dyn Storage, @@ -45,6 +45,13 @@ pub(crate) fn query_current_epoch_threshold( Ok(THRESHOLD.may_load(storage)?) } +pub(crate) fn query_epoch_threshold( + storage: &dyn Storage, + epoch_id: EpochId, +) -> Result, ContractError> { + Ok(EPOCH_THRESHOLDS.may_load(storage, epoch_id)?) +} + #[cfg(test)] pub(crate) mod test { use super::*; diff --git a/contracts/coconut-dkg/src/epoch_state/storage.rs b/contracts/coconut-dkg/src/epoch_state/storage.rs index 56c80fac2d..0ab1d9ab9c 100644 --- a/contracts/coconut-dkg/src/epoch_state/storage.rs +++ b/contracts/coconut-dkg/src/epoch_state/storage.rs @@ -1,8 +1,11 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cw_storage_plus::Item; -use nym_coconut_dkg_common::types::Epoch; +use cw_storage_plus::{Item, Map}; +use nym_coconut_dkg_common::types::{Epoch, EpochId}; pub(crate) const CURRENT_EPOCH: Item<'_, Epoch> = Item::new("current_epoch"); + pub const THRESHOLD: Item = Item::new("threshold"); + +pub const EPOCH_THRESHOLDS: Map = Map::new("epoch_thresholds"); diff --git a/contracts/coconut-dkg/src/epoch_state/transactions/advance_epoch_state.rs b/contracts/coconut-dkg/src/epoch_state/transactions/advance_epoch_state.rs index 396a7d1408..7ca350120a 100644 --- a/contracts/coconut-dkg/src/epoch_state/transactions/advance_epoch_state.rs +++ b/contracts/coconut-dkg/src/epoch_state/transactions/advance_epoch_state.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::epoch_state::storage::{CURRENT_EPOCH, THRESHOLD}; +use crate::epoch_state::storage::{CURRENT_EPOCH, EPOCH_THRESHOLDS, THRESHOLD}; use crate::epoch_state::transactions::reset_dkg_state; use crate::epoch_state::utils::check_state_completion; use crate::error::ContractError; @@ -61,7 +61,10 @@ pub fn try_advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_schema::write_api; +use nym_ecash_contract_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg, + } +} diff --git a/contracts/ecash/src/constants.rs b/contracts/ecash/src/constants.rs new file mode 100644 index 0000000000..ce5ff212fe --- /dev/null +++ b/contracts/ecash/src/constants.rs @@ -0,0 +1,5 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub const BLACKLIST_PROPOSAL_REPLY_ID: u64 = 7759; +pub const REDEMPTION_PROPOSAL_REPLY_ID: u64 = 2137; diff --git a/contracts/ecash/src/contract/helpers.rs b/contracts/ecash/src/contract/helpers.rs new file mode 100644 index 0000000000..bab1a5472f --- /dev/null +++ b/contracts/ecash/src/contract/helpers.rs @@ -0,0 +1,95 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::contract::NymEcashContract; +use crate::helpers::{create_batch_redemption_proposal, create_blacklist_proposal, ProposalId}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{to_binary, Addr, Deps, Storage, SubMsg}; +use cw3::ProposalResponse; +use nym_ecash_contract_common::EcashContractError; +use nym_multisig_contract_common::msg::QueryMsg as MultisigQueryMsg; +use nym_network_defaults::TICKETBOOK_SIZE; +use sylvia::types::ExecCtx; + +#[cw_serde] +pub(crate) struct Invariants { + pub(crate) ticket_book_size: u64, +} + +impl NymEcashContract<'_> { + pub(crate) fn get_ticketbook_size( + &self, + storage: &dyn Storage, + ) -> Result { + let invariants = self.expected_invariants.load(storage)?; + if invariants.ticket_book_size != TICKETBOOK_SIZE { + return Err(EcashContractError::TicketBookSizeChanged { + at_init: invariants.ticket_book_size, + current: TICKETBOOK_SIZE, + }); + } + Ok(TICKETBOOK_SIZE) + } + + fn must_get_multisig_addr(&self, deps: Deps) -> Result { + // SAFETY: multisig admin MUST always be set on initialisation, + // if the call fails, we're in some weird UB land + #[allow(clippy::expect_used)] + Ok(self + .multisig + .get(deps)? + .expect("multisig admin must always be set on initialisation")) + } + + pub(crate) fn create_redemption_proposal( + &self, + ctx: ExecCtx, + commitment_bs58: String, + number_of_tickets: u16, + ) -> Result { + let multisig_addr = self.must_get_multisig_addr(ctx.deps.as_ref())?; + + create_batch_redemption_proposal( + commitment_bs58, + ctx.info.sender.into_string(), + number_of_tickets, + ctx.env.contract.address.into_string(), + multisig_addr.into_string(), + ) + .map_err(Into::into) + } + + // temporarily dead + #[allow(dead_code)] + pub(crate) fn create_blacklist_proposal( + &self, + ctx: ExecCtx, + public_key: String, + ) -> Result { + let multisig_addr = self.must_get_multisig_addr(ctx.deps.as_ref())?; + + create_blacklist_proposal( + public_key, + ctx.env.contract.address.into_string(), + multisig_addr.into_string(), + ) + .map_err(Into::into) + } + + pub(crate) fn query_multisig_proposal( + &self, + deps: Deps, + proposal_id: ProposalId, + ) -> Result { + let msg = MultisigQueryMsg::Proposal { proposal_id }; + let multisig_addr = self.must_get_multisig_addr(deps)?; + + let proposal_response: ProposalResponse = deps.querier.query( + &cosmwasm_std::QueryRequest::Wasm(cosmwasm_std::WasmQuery::Smart { + contract_addr: multisig_addr.to_string(), + msg: to_binary(&msg)?, + }), + )?; + Ok(proposal_response) + } +} diff --git a/contracts/ecash/src/contract/mod.rs b/contracts/ecash/src/contract/mod.rs new file mode 100644 index 0000000000..a201ade356 --- /dev/null +++ b/contracts/ecash/src/contract/mod.rs @@ -0,0 +1,452 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::{BLACKLIST_PROPOSAL_REPLY_ID, REDEMPTION_PROPOSAL_REPLY_ID}; +use crate::contract::helpers::Invariants; +use crate::deposit::DepositStorage; +use crate::helpers::{ + BlacklistKey, Config, MultisigReply, BLACKLIST_PAGE_DEFAULT_LIMIT, BLACKLIST_PAGE_MAX_LIMIT, + CONTRACT_NAME, CONTRACT_VERSION, DEPOSITS_PAGE_DEFAULT_LIMIT, DEPOSITS_PAGE_MAX_LIMIT, +}; +use cosmwasm_std::{ + coin, BankMsg, Coin, Decimal, Event, Order, Reply, Response, StdResult, Uint128, +}; +use cw4::Cw4Contract; +use cw_controllers::Admin; +use cw_storage_plus::{Bound, Item, Map}; +use nym_contracts_common::set_build_information; +use nym_ecash_contract_common::blacklist::{ + BlacklistedAccount, BlacklistedAccountResponse, Blacklisting, PagedBlacklistedAccountResponse, +}; +use nym_ecash_contract_common::counters::PoolCounters; +use nym_ecash_contract_common::deposit::{DepositData, DepositResponse, PagedDepositsResponse}; +use nym_ecash_contract_common::events::{ + DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ID, PROPOSAL_ID_ATTRIBUTE_NAME, +}; +use nym_ecash_contract_common::EcashContractError; +use nym_network_defaults::TICKETBOOK_SIZE; +use sylvia::types::{ExecCtx, InstantiateCtx, MigrateCtx, QueryCtx, ReplyCtx}; +use sylvia::{contract, entry_points}; + +mod helpers; + +#[cfg(test)] +mod test; + +pub struct NymEcashContract<'a> { + pub(crate) contract_admin: Admin<'a>, + pub(crate) multisig: Admin<'a>, + pub(crate) config: Item<'a, Config>, + pub(crate) pool_counters: Item<'a, PoolCounters>, + pub(crate) expected_invariants: Item<'a, Invariants>, + + pub(crate) blacklist: Map<'a, BlacklistKey, Blacklisting>, + + pub(crate) deposits: DepositStorage<'a>, +} + +#[entry_points] +#[contract] +#[error(EcashContractError)] +impl NymEcashContract<'_> { + #[allow(clippy::new_without_default)] + pub const fn new() -> Self { + Self { + contract_admin: Admin::new("contract_admin"), + multisig: Admin::new("multisig"), + config: Item::new("config"), + pool_counters: Item::new("pool_counters"), + expected_invariants: Item::new("expected_invariants"), + blacklist: Map::new("blacklist"), + deposits: DepositStorage::new(), + } + } + + #[msg(instantiate)] + pub fn instantiate( + &self, + mut ctx: InstantiateCtx, + holding_account: String, + multisig_addr: String, + group_addr: String, + deposit_amount: Coin, + ) -> Result { + let multisig_addr = ctx.deps.api.addr_validate(&multisig_addr)?; + let holding_account = ctx.deps.api.addr_validate(&holding_account)?; + let group_addr = Cw4Contract(ctx.deps.api.addr_validate(&group_addr).map_err(|_| { + EcashContractError::InvalidGroup { + addr: group_addr.clone(), + } + })?); + + // by default the sender becomes the admin + self.contract_admin + .set(ctx.deps.branch(), Some(ctx.info.sender))?; + self.multisig + .set(ctx.deps.branch(), Some(multisig_addr.clone()))?; + + self.expected_invariants.save( + ctx.deps.storage, + &Invariants { + ticket_book_size: TICKETBOOK_SIZE, + }, + )?; + + self.pool_counters.save( + ctx.deps.storage, + &PoolCounters { + total_deposited: coin(0, &deposit_amount.denom), + total_redeemed: coin(0, &deposit_amount.denom), + }, + )?; + + self.config.save( + ctx.deps.storage, + &Config { + group_addr, + holding_account, + redemption_gateway_share: Decimal::percent(5), + deposit_amount, + }, + )?; + + cw2::set_contract_version(ctx.deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + set_build_information!(ctx.deps.storage)?; + + Ok(Response::default()) + } + + /*================== + ======QUERIES======= + ==================*/ + #[msg(query)] + pub fn get_blacklist_paged( + &self, + ctx: QueryCtx, + limit: Option, + start_after: Option, + ) -> StdResult { + let limit = limit + .unwrap_or(BLACKLIST_PAGE_DEFAULT_LIMIT) + .min(BLACKLIST_PAGE_MAX_LIMIT) as usize; + + let start = start_after.as_deref().map(Bound::exclusive); + + let nodes = self + .blacklist + .range(ctx.deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(Into::into)) + .collect::>>()?; + + let start_next_after = nodes + .last() + .map(|account: &BlacklistedAccount| account.public_key().to_string()); + + Ok(PagedBlacklistedAccountResponse::new( + nodes, + limit, + start_next_after, + )) + } + + #[msg(query)] + pub fn get_blacklisted_account( + &self, + ctx: QueryCtx, + public_key: String, + ) -> StdResult { + let account = self.blacklist.may_load(ctx.deps.storage, public_key)?; + Ok(BlacklistedAccountResponse::new(account)) + } + + #[msg(query)] + pub fn get_required_deposit_amount(&self, ctx: QueryCtx) -> StdResult { + let deposit_amount = self.config.load(ctx.deps.storage)?.deposit_amount; + + Ok(deposit_amount) + } + + #[msg(query)] + pub fn get_deposit( + &self, + ctx: QueryCtx, + deposit_id: u32, + ) -> Result { + Ok(DepositResponse { + id: deposit_id, + deposit: self.deposits.try_load_by_id(ctx.deps.storage, deposit_id)?, + }) + } + + #[msg(query)] + pub fn get_deposits_paged( + &self, + ctx: QueryCtx, + limit: Option, + start_after: Option, + ) -> StdResult { + let limit = limit + .unwrap_or(DEPOSITS_PAGE_DEFAULT_LIMIT) + .min(DEPOSITS_PAGE_MAX_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let deposits = self + .deposits + .range(ctx.deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(Into::into)) + .collect::>>()?; + + let start_next_after = deposits.last().map(|deposit| deposit.id); + + Ok(PagedDepositsResponse { + deposits, + start_next_after, + }) + } + + /*===================== + ======EXECUTIONS======= + =====================*/ + + #[msg(exec)] + pub fn deposit_ticket_book_funds( + &self, + ctx: ExecCtx, + identity_key: String, + ) -> Result { + let required_deposit = self.config.load(ctx.deps.storage)?.deposit_amount; + + let submitted = cw_utils::must_pay(&ctx.info, &required_deposit.denom)?; + + if submitted != required_deposit.amount { + let mut funds = ctx.info.funds; + return Err(EcashContractError::WrongAmount { + // SAFETY: the call to `must_pay` ensured a single coin has been sent + #[allow(clippy::unwrap_used)] + received: funds.pop().unwrap(), + amount: required_deposit, + }); + } + + self.pool_counters + .update(ctx.deps.storage, |mut counters| -> StdResult<_> { + counters.total_deposited.amount += submitted; + Ok(counters) + })?; + + let deposit_id = self.deposits.save_deposit(ctx.deps.storage, identity_key)?; + + Ok(Response::new() + .add_event( + Event::new(DEPOSITED_FUNDS_EVENT_TYPE) + .add_attribute(DEPOSIT_ID, deposit_id.to_string()), + ) + .set_data(deposit_id.to_be_bytes())) + } + + #[msg(exec)] + pub fn request_redemption( + &self, + ctx: ExecCtx, + commitment_bs58: String, + number_of_tickets: u16, + ) -> Result { + // basic validation of commitment to make sure it's a valid sha256 digest + let Ok(digest) = bs58::decode(&commitment_bs58).into_vec() else { + return Err(EcashContractError::MalformedRedemptionCommitment); + }; + if digest.len() != 32 { + return Err(EcashContractError::MalformedRedemptionCommitment); + } + + let msg = self.create_redemption_proposal(ctx, commitment_bs58, number_of_tickets)?; + Ok(Response::new().add_submessage(msg)) + } + + #[msg(exec)] + pub fn redeem_tickets( + &self, + ctx: ExecCtx, + n: u16, + gw: String, + ) -> Result { + // preserve the gateway argument so that upon scraping the chain and going through transactions, + // one could see which gateway attempted to redeem it. + // in the long run it will be needed to determine work factor. + let _ = gw; + + // only a mutlisig proposal can do that + self.multisig + .assert_admin(ctx.deps.as_ref(), &ctx.info.sender)?; + + let config = self.config.load(ctx.deps.storage)?; + + // TODO: we need unit tests for this + let deposit_amount = config.deposit_amount.amount; + let ticketbook_size = Uint128::new(self.get_ticketbook_size(ctx.deps.storage)? as u128); + let tickets = Uint128::new(n as u128); + + // how many tickets from a ticketbook you redeemed + let book_ratio = Decimal::from_ratio(tickets, ticketbook_size); + + // return = ticketbook_price * (tickets / ticketbook_size) + let return_amount = book_ratio * deposit_amount; + + self.pool_counters + .update(ctx.deps.storage, |mut counters| -> StdResult<_> { + counters.total_redeemed.amount += return_amount; + Ok(counters) + })?; + + Ok(Response::new().add_message(BankMsg::Send { + to_address: config.holding_account.to_string(), + amount: vec![Coin { + denom: config.deposit_amount.denom, + amount: return_amount, + }], + })) + } + + #[msg(exec)] + pub fn update_admin( + &self, + ctx: ExecCtx, + admin: String, + ) -> Result { + let new_admin = ctx.deps.api.addr_validate(&admin)?; + + // note: the below performs validation to ensure the sender IS the current admin + Ok(self + .contract_admin + .execute_update_admin(ctx.deps, ctx.info, Some(new_admin))?) + } + + #[msg(exec)] + pub fn update_deposit_value( + &self, + ctx: ExecCtx, + new_deposit: Coin, + ) -> Result { + // only current admin can do that + self.contract_admin + .assert_admin(ctx.deps.as_ref(), &ctx.info.sender)?; + + let deposit_str = new_deposit.to_string(); + self.config + .update(ctx.deps.storage, |mut cfg| -> StdResult<_> { + cfg.deposit_amount = new_deposit; + Ok(cfg) + })?; + Ok(Response::new().add_attribute("updated_deposit", deposit_str)) + } + + #[msg(exec)] + pub fn propose_to_blacklist( + &self, + ctx: ExecCtx, + public_key: String, + ) -> Result { + let _ = ctx; + let _ = public_key; + Err(EcashContractError::UnimplementedBlacklisting) + // let cfg = self.config.load(ctx.deps.storage)?; + // cfg.group_addr + // .is_voting_member(&ctx.deps.querier, &ctx.info.sender, ctx.env.block.height)? + // .ok_or(EcashContractError::Unauthorized)?; + // + // if let Some(blacklisted) = self + // .blacklist + // .may_load(ctx.deps.storage, public_key.clone())? + // { + // // return existing proposal id + // Ok(Response::new().add_attribute( + // PROPOSAL_ID_ATTRIBUTE_NAME, + // blacklisted.proposal_id.to_string(), + // )) + // } else { + // let msg = self.create_blacklist_proposal(ctx, public_key)?; + // Ok(Response::new().add_submessage(msg)) + // } + } + + #[msg(exec)] + pub fn add_to_blacklist( + &self, + ctx: ExecCtx, + public_key: String, + ) -> Result { + let _ = ctx; + let _ = public_key; + Err(EcashContractError::UnimplementedBlacklisting) + // //Only by multisig contract, actually add public key to blacklist + // self.multisig + // .assert_admin(ctx.deps.as_ref(), &ctx.info.sender)?; + // + // let mut blacklisting = self.blacklist.load(ctx.deps.storage, public_key.clone())?; + // blacklisting.finalized_at_height = Some(ctx.env.block.height); + // self.blacklist + // .save(ctx.deps.storage, public_key.clone(), &blacklisting)?; + // + // Ok(Response::new()) + } + + /*===================== + =========REPLY========= + =====================*/ + #[msg(reply)] + pub fn reply(&self, ctx: ReplyCtx, msg: Reply) -> Result { + match msg.id { + n if n == BLACKLIST_PROPOSAL_REPLY_ID => self.handle_blacklist_proposal_reply(ctx, msg), + n if n == REDEMPTION_PROPOSAL_REPLY_ID => { + self.handle_redemption_proposal_reply(ctx, msg) + } + other => Err(EcashContractError::InvalidReplyId { id: other }), + } + } + + fn handle_blacklist_proposal_reply( + &self, + ctx: ReplyCtx, + msg: Reply, + ) -> Result { + let proposal_id = msg.multisig_proposal_id()?; + + let proposal = self.query_multisig_proposal(ctx.deps.as_ref(), proposal_id)?; + let public_key = proposal.description; + self.blacklist.save( + ctx.deps.storage, + public_key, + &Blacklisting::new(proposal_id), + )?; + + // TODO: that `BLACKLIST_PROPOSAL_ID` might be redundant since it should be available from cw3 event + Ok(Response::new().add_attribute(PROPOSAL_ID_ATTRIBUTE_NAME, proposal_id.to_string())) + } + + fn handle_redemption_proposal_reply( + &self, + _ctx: ReplyCtx, + msg: Reply, + ) -> Result { + let proposal_id = msg.multisig_proposal_id()?; + + // emit the proposal_id in the response data for easy client access and to make sure it can't be tampered with + // (since it's included as part of block hash) + + Ok(Response::new().set_data(proposal_id.to_be_bytes())) + } + + /*===================== + =======MIGRATION======= + =====================*/ + #[msg(migrate)] + pub fn migrate(&self, ctx: MigrateCtx) -> Result { + set_build_information!(ctx.deps.storage)?; + cw2::ensure_from_older_version(ctx.deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Response::new()) + } +} diff --git a/contracts/ecash/src/contract/test.rs b/contracts/ecash/src/contract/test.rs new file mode 100644 index 0000000000..0a90c0b542 --- /dev/null +++ b/contracts/ecash/src/contract/test.rs @@ -0,0 +1,89 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::contract::NymEcashContract; +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier}; +use cosmwasm_std::{coin, Addr, Empty, Env, MemoryStorage, OwnedDeps}; +use sylvia::types::{InstantiateCtx, QueryCtx}; + +pub const TEST_DENOM: &str = "unym"; + +#[allow(dead_code)] +pub struct TestSetup { + pub contract: NymEcashContract<'static>, + pub deps: OwnedDeps>, + pub env: Env, + + pub holding_account: Addr, + pub multisig_contract: Addr, + pub group_contract: Addr, +} + +impl TestSetup { + pub fn init() -> TestSetup { + let mut deps = mock_dependencies(); + let env = mock_env(); + let admin = mock_info("admin", &[]); + let init_ctx = InstantiateCtx::from((deps.as_mut(), env.clone(), admin)); + + let multisig_contract = "multisig"; + let group_contract = "group"; + let holding = "holding"; + + let contract = NymEcashContract::new(); + contract + .instantiate( + init_ctx, + holding.to_string(), + multisig_contract.to_string(), + group_contract.to_string(), + coin(75000000, TEST_DENOM), + ) + .unwrap(); + + TestSetup { + contract, + deps, + env, + holding_account: Addr::unchecked(holding), + multisig_contract: Addr::unchecked(multisig_contract), + group_contract: Addr::unchecked(group_contract), + } + } + + pub fn query_ctx(&self) -> QueryCtx { + QueryCtx::from((self.deps.as_ref(), self.env.clone())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nym_ecash_contract_common::deposit::Deposit; + use sylvia::anyhow; + + #[test] + fn deposit_queries() -> anyhow::Result<()> { + let mut test = TestSetup::init(); + + // no deposit + let res = test.contract.get_deposit(test.query_ctx(), 42)?; + assert!(res.deposit.is_none()); + + let deps = test.deps.as_mut(); + let deposit_id = test.contract.deposits.save_deposit( + deps.storage, + "GLdR2NRVZBiCoCbv4fNqt9wUJZAnNjGXHkx3TjVAUzrK".to_string(), + )?; + + // deposit exists + let res = test.contract.get_deposit(test.query_ctx(), deposit_id)?; + let expected = Deposit { + bs58_encoded_ed25519_pubkey: "GLdR2NRVZBiCoCbv4fNqt9wUJZAnNjGXHkx3TjVAUzrK".to_string(), + }; + + assert_eq!(expected, res.deposit.unwrap()); + + Ok(()) + } +} diff --git a/contracts/ecash/src/deposit.rs b/contracts/ecash/src/deposit.rs new file mode 100644 index 0000000000..310b4a0a26 --- /dev/null +++ b/contracts/ecash/src/deposit.rs @@ -0,0 +1,230 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{Order, StdResult, Storage}; +use cw_storage_plus::{Bound, Item, Key, Path, Prefix, PrimaryKey}; +use nym_ecash_contract_common::deposit::DepositId; +use nym_ecash_contract_common::{deposit::Deposit, EcashContractError}; + +pub(crate) struct DepositStorage<'a> { + pub(crate) deposit_id_counter: Item<'a, DepositId>, + pub(crate) deposits: StoredDeposits, +} + +impl<'a> DepositStorage<'a> { + pub const fn new() -> Self { + DepositStorage { + deposit_id_counter: Item::new("deposit_ids"), + deposits: StoredDeposits, + } + } + + fn next_id(&self, store: &mut dyn Storage) -> Result { + let id: DepositId = self.deposit_id_counter.may_load(store)?.unwrap_or_default(); + let next_id = id + 1; + self.deposit_id_counter.save(store, &next_id)?; + Ok(id) + } + + pub fn save_deposit( + &self, + storage: &mut dyn Storage, + bs58_encoded_ed25519: String, + ) -> Result { + let id = self.next_id(storage)?; + + // ed25519 key MUST be represented with valid bs58 representation + // and after decoding it's always exactly 32 bytes which takes less + // space than its string representation (~44 bytes) + let bytes = Deposit::new(bs58_encoded_ed25519).to_bytes()?; + + let storage_key = StoredDeposits::storage_key(id); + storage.set(&storage_key, &bytes); + Ok(id) + } + + pub fn try_load_by_id( + &self, + storage: &dyn Storage, + id: DepositId, + ) -> Result, EcashContractError> { + let storage_key = StoredDeposits::storage_key(id); + + let Some(deposit_bytes) = storage.get(&storage_key) else { + return Ok(None); + }; + + Ok(Some(Deposit::try_from_bytes(&deposit_bytes)?)) + } + pub fn range( + &'a self, + store: &'a dyn Storage, + min: Option>, + max: Option>, + order: Order, + ) -> impl Iterator> + 'a { + self.deposits.no_prefix().range(store, min, max, order) + } +} + +// a helper structure for storing deposits to bypass json serialisation and use more efficient and compact representation +pub(crate) struct StoredDeposits; + +impl StoredDeposits { + const NAMESPACE: &'static [u8] = b"deposit"; + + fn deserialize_deposit_record(kv: cosmwasm_std::Record) -> StdResult<(DepositId, Deposit)> { + let (k, deposit_bytes) = kv; + let id = ::from_vec(k)?; + + Ok((id, Deposit::try_from_bytes(&deposit_bytes)?)) + } + + fn no_prefix(&self) -> Prefix { + cw_storage_plus::Prefix::with_deserialization_functions( + Self::NAMESPACE, + &[], + &[], + // explicitly panic to make sure we're never attempting to call an unexpected deserializer on our data + |_, _, kv| Self::deserialize_deposit_record(kv), + |_, _, _| panic!("attempted to call custom de_fn_v"), + ) + } + + fn storage_key(deposit_id: u32) -> Path> { + let key = deposit_id; + Path::new( + Self::NAMESPACE, + &key.key().iter().map(Key::as_ref).collect::>(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::support::tests::test_rng; + use cosmwasm_std::testing::mock_dependencies; + use nym_crypto::asymmetric::ed25519; + + #[test] + fn iterating_over_deposits() { + let mut deps = mock_dependencies(); + let mut rng = test_rng(); + + let storage = DepositStorage::new(); + + let count = 10; + let mut expected = Vec::new(); + for _ in 0..count { + let ed25519_keypair = ed25519::KeyPair::new(&mut rng); + + let bs58_encoded_ed25519 = ed25519_keypair.public_key().to_base58_string(); + expected.push(Deposit { + bs58_encoded_ed25519_pubkey: bs58_encoded_ed25519.clone(), + }); + + storage + .save_deposit(deps.as_mut().storage, bs58_encoded_ed25519) + .unwrap(); + } + + // just first entry + let res = storage + .range( + deps.as_ref().storage, + None, + Some(Bound::inclusive(1u32)), + Order::Ascending, + ) + .collect::>(); + let (id, deposit) = res[0].as_ref().unwrap(); + assert_eq!(0, *id); + assert_eq!(deposit, &expected[0]); + + // first three entries + let res = storage + .range( + deps.as_ref().storage, + None, + Some(Bound::exclusive(4u32)), + Order::Ascending, + ) + .collect::>(); + for i in 0..3 { + let (id, deposit) = res[i].as_ref().unwrap(); + assert_eq!(i as u32, *id); + assert_eq!(deposit, &expected[i]); + } + + // two entries in the middle + let res = storage + .range( + deps.as_ref().storage, + Some(Bound::inclusive(5u32)), + Some(Bound::inclusive(6u32)), + Order::Ascending, + ) + .collect::>(); + let (id1, deposit1) = res[0].as_ref().unwrap(); + let (id2, deposit2) = res[1].as_ref().unwrap(); + assert_eq!(5, *id1); + assert_eq!(deposit1, &expected[5]); + assert_eq!(6, *id2); + assert_eq!(deposit2, &expected[6]); + + // last 2 entries + let res = storage + .range( + deps.as_ref().storage, + Some(Bound::inclusive(8u32)), + Some(Bound::inclusive(9u32)), + Order::Ascending, + ) + .collect::>(); + let (id1, deposit1) = res[0].as_ref().unwrap(); + let (id2, deposit2) = res[1].as_ref().unwrap(); + assert_eq!(8, *id1); + assert_eq!(deposit1, &expected[8]); + assert_eq!(9, *id2); + assert_eq!(deposit2, &expected[9]); + + // last 2 entries but with iterator going beyond + let res = storage + .range( + deps.as_ref().storage, + Some(Bound::inclusive(8u32)), + Some(Bound::inclusive(42u32)), + Order::Ascending, + ) + .collect::>(); + let (id1, deposit1) = res[0].as_ref().unwrap(); + let (id2, deposit2) = res[1].as_ref().unwrap(); + assert_eq!(8, *id1); + assert_eq!(deposit1, &expected[8]); + assert_eq!(9, *id2); + assert_eq!(deposit2, &expected[9]); + + // outside the saved range + let res = storage + .range( + deps.as_ref().storage, + Some(Bound::inclusive(42u32)), + Some(Bound::inclusive(666u32)), + Order::Ascending, + ) + .collect::>(); + assert!(res.is_empty()); + + // all entries + let res = storage + .range(deps.as_ref().storage, None, None, Order::Ascending) + .collect::>(); + assert_eq!(res.len(), count as usize); + for (i, val) in res.into_iter().enumerate() { + let (id, deposit) = val.unwrap(); + assert_eq!(id, i as u32); + assert_eq!(&deposit, &expected[i]) + } + } +} diff --git a/contracts/ecash/src/helpers.rs b/contracts/ecash/src/helpers.rs new file mode 100644 index 0000000000..650dc82f5c --- /dev/null +++ b/contracts/ecash/src/helpers.rs @@ -0,0 +1,119 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::{BLACKLIST_PROPOSAL_REPLY_ID, REDEMPTION_PROPOSAL_REPLY_ID}; +use cosmwasm_std::{ + to_binary, Addr, Coin, CosmosMsg, Decimal, Reply, StdError, StdResult, SubMsg, SubMsgResult, + WasmMsg, +}; +use cw4::Cw4Contract; +use nym_contracts_common::events::try_find_attribute; +use nym_ecash_contract_common::events::{PROPOSAL_ID_ATTRIBUTE_NAME, WASM_EVENT_NAME}; +use nym_ecash_contract_common::redeem_credential::BATCH_REDEMPTION_PROPOSAL_TITLE; +use nym_ecash_contract_common::{msg::ExecuteMsg, EcashContractError}; +use nym_multisig_contract_common::msg::ExecuteMsg as MultisigExecuteMsg; +use serde::{Deserialize, Serialize}; + +// version info for migration info +pub(crate) const CONTRACT_NAME: &str = "crate:nym-ecash"; +pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Config { + pub group_addr: Cw4Contract, + pub holding_account: Addr, + + pub redemption_gateway_share: Decimal, + pub deposit_amount: Coin, +} + +//type aliases for easier reasoning +pub(crate) type BlacklistKey = String; +pub(crate) type ProposalId = u64; + +// paged retrieval limits for all blacklist queries and transactions +pub(crate) const BLACKLIST_PAGE_MAX_LIMIT: u32 = 75; +pub(crate) const BLACKLIST_PAGE_DEFAULT_LIMIT: u32 = 50; + +// paged retrieval limits for all deposit queries and transactions +pub(crate) const DEPOSITS_PAGE_MAX_LIMIT: u32 = 100; +pub(crate) const DEPOSITS_PAGE_DEFAULT_LIMIT: u32 = 50; + +pub(crate) fn create_batch_redemption_proposal( + tickets_digest: String, + gw: String, + n: u16, + ecash_bandwidth_address: String, + multisig_addr: String, +) -> StdResult { + let release_funds_req = ExecuteMsg::RedeemTickets { n, gw }; + let release_funds_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: ecash_bandwidth_address, + msg: to_binary(&release_funds_req)?, + funds: vec![], + }); + let req = MultisigExecuteMsg::Propose { + title: BATCH_REDEMPTION_PROPOSAL_TITLE.to_string(), + description: tickets_digest, + msgs: vec![release_funds_msg], + latest: None, + }; + let msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: multisig_addr, + msg: to_binary(&req)?, + funds: vec![], + }); + + let submsg = SubMsg::reply_always(msg, REDEMPTION_PROPOSAL_REPLY_ID); + + Ok(submsg) +} + +pub(crate) fn create_blacklist_proposal( + public_key: String, + ecash_bandwidth_address: String, + multisig_addr: String, +) -> StdResult { + let blacklist_req = ExecuteMsg::AddToBlacklist { + public_key: public_key.clone(), + }; + let blacklist_req_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: ecash_bandwidth_address, + msg: to_binary(&blacklist_req)?, + funds: vec![], + }); + let req = MultisigExecuteMsg::Propose { + title: String::from("Add to blacklist, as ordered by Ecash Bandwidth Contract"), + description: public_key, + msgs: vec![blacklist_req_msg], + latest: None, + }; + let msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: multisig_addr, + msg: to_binary(&req)?, + funds: vec![], + }); + + let submsg = SubMsg::reply_always(msg, BLACKLIST_PROPOSAL_REPLY_ID); + + Ok(submsg) +} + +pub(crate) trait MultisigReply { + fn multisig_proposal_id(&self) -> Result; +} + +impl MultisigReply for Reply { + fn multisig_proposal_id(&self) -> Result { + match &self.result { + SubMsgResult::Ok(res) => { + let proposal_id: u64 = + try_find_attribute(&res.events, WASM_EVENT_NAME, PROPOSAL_ID_ATTRIBUTE_NAME) + .ok_or(EcashContractError::MissingProposalId)? + .map_err(|_| EcashContractError::MalformedProposalId)?; + Ok(proposal_id) + } + SubMsgResult::Err(reply_err) => Err(StdError::generic_err(reply_err).into()), + } + } +} diff --git a/contracts/ecash/src/lib.rs b/contracts/ecash/src/lib.rs new file mode 100644 index 0000000000..a66566e247 --- /dev/null +++ b/contracts/ecash/src/lib.rs @@ -0,0 +1,15 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] +#![warn(clippy::todo)] +#![warn(clippy::dbg_macro)] + +mod constants; +pub mod contract; +mod deposit; +mod helpers; +#[cfg(test)] +pub mod multitest; +mod support; diff --git a/contracts/ecash/src/multitest.rs b/contracts/ecash/src/multitest.rs new file mode 100644 index 0000000000..de56796089 --- /dev/null +++ b/contracts/ecash/src/multitest.rs @@ -0,0 +1,73 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{coin, Addr, Coin}; +use cw_utils::PaymentError; +use nym_ecash_contract_common::EcashContractError; +use sylvia::{cw_multi_test::App as MtApp, multitest::App}; + +use crate::contract::multitest_utils::CodeId; + +#[test] +fn invalid_deposit() { + let owner = "owner"; + let denom = "unym"; + + let mtapp = MtApp::new(|router, _, storage| { + router + .bank + .init_balance( + storage, + &Addr::unchecked(owner), + vec![ + Coin::new(10000000, denom), + Coin::new(10000000, "some_denom"), + ], + ) + .unwrap() + }); + let app = App::new(mtapp); + + let code_id = CodeId::store_code(&app); + + let contract = code_id + .instantiate( + "holding_acount".to_string(), + "multisig_addr".to_string(), + "group_addr".to_string(), + coin(75000000, denom), + ) + .call(owner) + .unwrap(); + + let verification_key = "Verification key"; + + assert_eq!( + contract + .deposit_ticket_book_funds(verification_key.to_string(),) + .call(owner) + .unwrap_err(), + EcashContractError::InvalidDeposit(PaymentError::NoFunds {}) + ); + + let coin = Coin::new(1000000, denom.to_string()); + let second_coin = Coin::new(1000000, "some_denom"); + + assert_eq!( + contract + .deposit_ticket_book_funds(verification_key.to_string(),) + .with_funds(&[coin, second_coin.clone()]) + .call(owner) + .unwrap_err(), + EcashContractError::InvalidDeposit(PaymentError::MultipleDenoms {}) + ); + + assert_eq!( + contract + .deposit_ticket_book_funds(verification_key.to_string(),) + .with_funds(&[second_coin]) + .call(owner) + .unwrap_err(), + EcashContractError::InvalidDeposit(PaymentError::MissingDenom(denom.to_string())) + ); +} diff --git a/contracts/ecash/src/support/mod.rs b/contracts/ecash/src/support/mod.rs new file mode 100644 index 0000000000..f217570509 --- /dev/null +++ b/contracts/ecash/src/support/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(test)] +pub mod tests; diff --git a/contracts/ecash/src/support/tests.rs b/contracts/ecash/src/support/tests.rs new file mode 100644 index 0000000000..2f44544092 --- /dev/null +++ b/contracts/ecash/src/support/tests.rs @@ -0,0 +1,10 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use rand_chacha::rand_core::SeedableRng; +use rand_chacha::ChaCha20Rng; + +pub fn test_rng() -> ChaCha20Rng { + let dummy_seed = [42u8; 32]; + ChaCha20Rng::from_seed(dummy_seed) +} diff --git a/contracts/mixnet-vesting-integration-tests/src/decrease_mixnode_pledge.rs b/contracts/mixnet-vesting-integration-tests/src/decrease_mixnode_pledge.rs deleted file mode 100644 index a9930e23f7..0000000000 --- a/contracts/mixnet-vesting-integration-tests/src/decrease_mixnode_pledge.rs +++ /dev/null @@ -1,239 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::support::helpers::{mix_coin, mix_coins, vesting_owner}; -use crate::support::setup::{TestSetup, MIX_DENOM}; -use cosmwasm_std::Addr; -use cw_multi_test::Executor; -use nym_contracts_common::Percent; -use nym_mixnet_contract_common::error::MixnetContractError; -use nym_mixnet_contract_common::{ContractStateParams, MixNodeCostParams}; -use nym_mixnet_contract_common::{MixOwnershipResponse, QueryMsg as MixnetQueryMsg}; -use nym_vesting_contract_common::{ExecuteMsg as VestingExecuteMsg, VestingContractError}; - -#[test] -fn decrease_mixnode_pledge_from_vesting_account_with_minimum_pledge() { - let mut test = TestSetup::new_simple(); - let vesting_account = "vesting-account"; - - // 0. get the minimum pledge amount - let state_params: ContractStateParams = test - .app - .wrap() - .query_wasm_smart(test.mixnet_contract(), &MixnetQueryMsg::GetStateParams {}) - .unwrap(); - let minimum_pledge = state_params.minimum_mixnode_pledge; - - // 1. create vesting account - let create_msg = VestingExecuteMsg::CreateAccount { - owner_address: vesting_account.to_string(), - staking_address: None, - vesting_spec: None, - cap: None, - }; - - test.app - .execute_contract( - vesting_owner(), - test.vesting_contract(), - &create_msg, - &mix_coins(1_000_000_000), - ) - .unwrap(); - - // 2. bond mixnode with the vesting account - let pledge = minimum_pledge.clone(); - - let cost_params = MixNodeCostParams { - profit_margin_percent: Percent::from_percentage_value(10).unwrap(), - interval_operating_cost: mix_coin(40_000_000), - }; - - let (mix_node, owner_signature) = test.valid_mixnode_with_sig( - vesting_account, - Some(test.vesting_contract()), - cost_params.clone(), - pledge.clone(), - ); - - let bond_msg = VestingExecuteMsg::BondMixnode { - mix_node, - cost_params, - owner_signature, - amount: pledge.clone(), - }; - test.app - .execute_contract( - Addr::unchecked(vesting_account), - test.vesting_contract(), - &bond_msg, - &[], - ) - .unwrap(); - - // 3. try to decrease the pledge - - // trying to decrease by a zero amount - not valid - let decrease_pledge_msg = VestingExecuteMsg::DecreasePledge { - amount: mix_coin(0), - }; - let res_zero = test - .app - .execute_contract( - Addr::unchecked(vesting_account), - test.vesting_contract(), - &decrease_pledge_msg, - &[], - ) - .unwrap_err(); - - assert_eq!( - VestingContractError::EmptyFunds, - res_zero.downcast().unwrap() - ); - - // trying to go below the cap - also not valid - let amount = mix_coin(50_000); - let decrease_pledge_msg = VestingExecuteMsg::DecreasePledge { - amount: amount.clone(), - }; - let res_below = test - .app - .execute_contract( - Addr::unchecked(vesting_account), - test.vesting_contract(), - &decrease_pledge_msg, - &[], - ) - .unwrap_err(); - assert_eq!( - MixnetContractError::InvalidPledgeReduction { - current: pledge.amount, - decrease_by: amount.amount, - minimum: minimum_pledge.amount, - denom: minimum_pledge.denom - }, - res_below.downcast().unwrap() - ) -} - -#[test] -fn decrease_mixnode_pledge_from_vesting_account_with_sufficient_pledge() { - let mut test = TestSetup::new_simple(); - let vesting_account = "vesting-account"; - - // 1. create vesting account - let create_msg = VestingExecuteMsg::CreateAccount { - owner_address: vesting_account.to_string(), - staking_address: None, - vesting_spec: None, - cap: None, - }; - - test.app - .execute_contract( - vesting_owner(), - test.vesting_contract(), - &create_msg, - &mix_coins(10_000_000_000), - ) - .unwrap(); - - // 2. bond mixnode with the vesting account - let pledge = mix_coin(150_000_000); - - let cost_params = MixNodeCostParams { - profit_margin_percent: Percent::from_percentage_value(10).unwrap(), - interval_operating_cost: mix_coin(40_000_000), - }; - - let (mix_node, owner_signature) = test.valid_mixnode_with_sig( - vesting_account, - Some(test.vesting_contract()), - cost_params.clone(), - pledge.clone(), - ); - - let bond_msg = VestingExecuteMsg::BondMixnode { - mix_node, - cost_params, - owner_signature, - amount: pledge, - }; - test.app - .execute_contract( - Addr::unchecked(vesting_account), - test.vesting_contract(), - &bond_msg, - &[], - ) - .unwrap(); - - // 3. try to decrease the pledge - let before: MixOwnershipResponse = test - .app - .wrap() - .query_wasm_smart( - test.mixnet_contract(), - &MixnetQueryMsg::GetOwnedMixnode { - address: vesting_account.to_string(), - }, - ) - .unwrap(); - let balance_before = test - .app - .wrap() - .query_balance(test.vesting_contract(), MIX_DENOM) - .unwrap(); - assert_eq!(balance_before.amount.u128(), 9_850_000_000); - - let decrease_pledge_msg = VestingExecuteMsg::DecreasePledge { - amount: mix_coin(50_000_000), - }; - test.app - .execute_contract( - Addr::unchecked(vesting_account), - test.vesting_contract(), - &decrease_pledge_msg, - &[], - ) - .unwrap(); - - let after_decrease: MixOwnershipResponse = test - .app - .wrap() - .query_wasm_smart( - test.mixnet_contract(), - &MixnetQueryMsg::GetOwnedMixnode { - address: vesting_account.to_string(), - }, - ) - .unwrap(); - - // note: nothing has changed with the pledge because the event hasn't been resolved yet! - assert_eq!(before.address, after_decrease.address); - let before_details = before.mixnode_details.unwrap(); - let after_details = after_decrease.mixnode_details.unwrap(); - assert_eq!( - before_details.rewarding_details, - after_details.rewarding_details - ); - assert_eq!( - before_details.bond_information, - after_details.bond_information - ); - - // but we have the pending change saved now! - assert!(before_details.pending_changes.pledge_change.is_none()); - assert_eq!(Some(1), after_details.pending_changes.pledge_change); - - // 4. resolve events - test.advance_mixnet_epoch(); - - let balance_after = test - .app - .wrap() - .query_balance(test.vesting_contract(), MIX_DENOM) - .unwrap(); - assert_eq!(balance_after.amount.u128(), 9_900_000_000); -} diff --git a/contracts/mixnet-vesting-integration-tests/src/support/fixtures.rs b/contracts/mixnet-vesting-integration-tests/src/support/fixtures.rs index 5a97c2cf7e..9910720bb7 100644 --- a/contracts/mixnet-vesting-integration-tests/src/support/fixtures.rs +++ b/contracts/mixnet-vesting-integration-tests/src/support/fixtures.rs @@ -24,5 +24,7 @@ pub fn default_mixnet_init_msg() -> nym_mixnet_contract_common::InstantiateMsg { rewarded_set_size: 240, active_set_size: 100, }, + profit_margin: Default::default(), + interval_operating_cost: Default::default(), } } diff --git a/contracts/mixnet-vesting-integration-tests/src/support/helpers.rs b/contracts/mixnet-vesting-integration-tests/src/support/helpers.rs index f739d8556a..a714ccf493 100644 --- a/contracts/mixnet-vesting-integration-tests/src/support/helpers.rs +++ b/contracts/mixnet-vesting-integration-tests/src/support/helpers.rs @@ -12,27 +12,33 @@ pub fn mixnet_owner() -> Addr { Addr::unchecked(MIXNET_OWNER) } +#[allow(unused)] pub fn vesting_owner() -> Addr { Addr::unchecked(VESTING_OWNER) } +#[allow(unused)] pub fn rewarding_validator() -> Addr { Addr::unchecked(REWARDING_VALIDATOR) } +#[allow(unused)] pub fn mix_coins(amount: u128) -> Vec { coins(amount, MIX_DENOM) } +#[allow(unused)] pub fn mix_coin(amount: u128) -> Coin { coin(amount, MIX_DENOM) } +#[allow(unused)] pub fn test_rng() -> ChaCha20Rng { let dummy_seed = [42u8; 32]; ChaCha20Rng::from_seed(dummy_seed) } +#[allow(unused)] pub fn mixnet_contract_wrapper() -> Box> { Box::new( ContractWrapper::new( diff --git a/contracts/mixnet-vesting-integration-tests/src/support/setup.rs b/contracts/mixnet-vesting-integration-tests/src/support/setup.rs index 4f1a4d3bec..4ef1b04cba 100644 --- a/contracts/mixnet-vesting-integration-tests/src/support/setup.rs +++ b/contracts/mixnet-vesting-integration-tests/src/support/setup.rs @@ -26,6 +26,7 @@ pub const VESTING_OWNER: &str = "vesting-owner"; pub const REWARDING_VALIDATOR: &str = "rewarding-validator"; pub const MIX_DENOM: &str = "unym"; +#[allow(unused)] pub struct ContractInstantiationResult { mixnet_contract_address: Addr, vesting_contract_address: Addr, @@ -69,14 +70,15 @@ impl TestSetupBuilder { } } +#[allow(unused)] pub struct TestSetup { pub app: App, pub rng: ChaCha20Rng, pub mixnet_contract: Addr, - pub vesting_contract: Addr, } +#[allow(unused)] impl TestSetup { pub fn new_simple() -> Self { TestSetup::new(Default::default(), fixtures::default_mixnet_init_msg()) @@ -91,7 +93,6 @@ impl TestSetup { app, rng: test_rng(), mixnet_contract: contracts.mixnet_contract_address, - vesting_contract: contracts.vesting_contract_address, } } @@ -99,10 +100,6 @@ impl TestSetup { self.mixnet_contract.clone() } - pub fn vesting_contract(&self) -> Addr { - self.vesting_contract.clone() - } - pub fn skip_to_current_epoch_end(&mut self) { let current_interval: CurrentIntervalResponse = self .app @@ -209,7 +206,6 @@ impl TestSetup { pub fn valid_mixnode_with_sig( &mut self, owner: &str, - proxy: Option, cost_params: MixNodeCostParams, stake: Coin, ) -> (MixNode, MessageSignature) { @@ -239,8 +235,7 @@ impl TestSetup { }; let payload = MixnodeBondingPayload::new(mixnode.clone(), cost_params); - let content = - ContractMessageContent::new(Addr::unchecked(owner), proxy, vec![stake], payload); + let content = ContractMessageContent::new(Addr::unchecked(owner), vec![stake], payload); let sign_payload = SignableMixNodeBondingMsg::new(signing_nonce, content); let plaintext = sign_payload.to_plaintext().unwrap(); let signature = keypair.private_key().sign(plaintext); diff --git a/contracts/mixnet-vesting-integration-tests/src/tests.rs b/contracts/mixnet-vesting-integration-tests/src/tests.rs index d560e38394..826adcfd53 100644 --- a/contracts/mixnet-vesting-integration-tests/src/tests.rs +++ b/contracts/mixnet-vesting-integration-tests/src/tests.rs @@ -1,5 +1,4 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -mod decrease_mixnode_pledge; mod support; diff --git a/contracts/mixnet/Cargo.toml b/contracts/mixnet/Cargo.toml index 94133686d5..957f981f2e 100644 --- a/contracts/mixnet/Cargo.toml +++ b/contracts/mixnet/Cargo.toml @@ -34,6 +34,7 @@ cosmwasm-schema = { workspace = true, optional = true } cosmwasm-std = { workspace = true } cosmwasm-storage = { workspace = true } cosmwasm-derive = { workspace = true } +cw-controllers = { workspace = true } cw2 = { workspace = true } cw-storage-plus = { workspace = true } diff --git a/contracts/mixnet/schema/nym-mixnet-contract.json b/contracts/mixnet/schema/nym-mixnet-contract.json index 971cbc27fb..c754e65fdb 100644 --- a/contracts/mixnet/schema/nym-mixnet-contract.json +++ b/contracts/mixnet/schema/nym-mixnet-contract.json @@ -26,6 +26,28 @@ "initial_rewarding_params": { "$ref": "#/definitions/InitialRewardingParams" }, + "interval_operating_cost": { + "default": { + "maximum": "1000000000000000", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Uint128" + } + ] + }, + "profit_margin": { + "default": { + "maximum": "1", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Percent" + } + ] + }, "rewarding_denom": { "type": "string" }, @@ -112,6 +134,42 @@ "$ref": "#/definitions/Decimal" } ] + }, + "RangedValue_for_Percent": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Percent" + }, + "minimum": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "RangedValue_for_Uint128": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Uint128" + }, + "minimum": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" } } }, @@ -119,6 +177,28 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "ExecuteMsg", "oneOf": [ + { + "description": "Change the admin", + "type": "object", + "required": [ + "update_admin" + ], + "properties": { + "update_admin": { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -1146,6 +1226,42 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "migrate_vested_mix_node" + ], + "properties": { + "migrate_vested_mix_node": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "migrate_vested_delegation" + ], + "properties": { + "migrate_vested_delegation": { + "type": "object", + "required": [ + "mix_id" + ], + "properties": { + "mix_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": { @@ -1172,6 +1288,18 @@ "minimum_mixnode_pledge" ], "properties": { + "interval_operating_cost": { + "description": "Defines the allowed interval operating cost range of operators. default: 0 - 1'000'000'000'000'000 (1 Billion native tokens - the total supply)", + "default": { + "maximum": "1000000000000000", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Uint128" + } + ] + }, "minimum_gateway_pledge": { "description": "Minimum amount a gateway must pledge to get into the system.", "allOf": [ @@ -1198,6 +1326,18 @@ "$ref": "#/definitions/Coin" } ] + }, + "profit_margin": { + "description": "Defines the allowed profit margin range of operators. default: 0% - 100%", + "default": { + "maximum": "1", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Percent" + } + ] } }, "additionalProperties": false @@ -1532,6 +1672,38 @@ } ] }, + "RangedValue_for_Percent": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Percent" + }, + "minimum": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "RangedValue_for_Uint128": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Uint128" + }, + "minimum": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" @@ -1542,6 +1714,19 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "QueryMsg", "oneOf": [ + { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Gets the list of families registered in this contract.", "type": "object", @@ -2787,6 +2972,21 @@ }, "sudo": null, "responses": { + "admin": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AdminResponse", + "description": "Returned from Admin.query_admin()", + "type": "object", + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, "get_all_delegations": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "PagedAllDelegationsResponse", @@ -7990,7 +8190,6 @@ "description": "The current state of the mixnet contract.", "type": "object", "required": [ - "owner", "params", "rewarding_denom", "rewarding_validator_address", @@ -7999,9 +8198,14 @@ "properties": { "owner": { "description": "Address of the contract owner.", - "allOf": [ + "default": null, + "deprecated": true, + "anyOf": [ { "$ref": "#/definitions/Addr" + }, + { + "type": "null" } ] }, @@ -8063,6 +8267,18 @@ "minimum_mixnode_pledge" ], "properties": { + "interval_operating_cost": { + "description": "Defines the allowed interval operating cost range of operators. default: 0 - 1'000'000'000'000'000 (1 Billion native tokens - the total supply)", + "default": { + "maximum": "1000000000000000", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Uint128" + } + ] + }, "minimum_gateway_pledge": { "description": "Minimum amount a gateway must pledge to get into the system.", "allOf": [ @@ -8089,6 +8305,62 @@ "$ref": "#/definitions/Coin" } ] + }, + "profit_margin": { + "description": "Defines the allowed profit margin range of operators. default: 0% - 100%", + "default": { + "maximum": "1", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Percent" + } + ] + } + }, + "additionalProperties": false + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "RangedValue_for_Percent": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Percent" + }, + "minimum": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "RangedValue_for_Uint128": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Uint128" + }, + "minimum": { + "$ref": "#/definitions/Uint128" } }, "additionalProperties": false @@ -8109,6 +8381,18 @@ "minimum_mixnode_pledge" ], "properties": { + "interval_operating_cost": { + "description": "Defines the allowed interval operating cost range of operators. default: 0 - 1'000'000'000'000'000 (1 Billion native tokens - the total supply)", + "default": { + "maximum": "1000000000000000", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Uint128" + } + ] + }, "minimum_gateway_pledge": { "description": "Minimum amount a gateway must pledge to get into the system.", "allOf": [ @@ -8135,6 +8419,18 @@ "$ref": "#/definitions/Coin" } ] + }, + "profit_margin": { + "description": "Defines the allowed profit margin range of operators. default: 0% - 100%", + "default": { + "maximum": "1", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Percent" + } + ] } }, "additionalProperties": false, @@ -8154,6 +8450,50 @@ } } }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "RangedValue_for_Percent": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Percent" + }, + "minimum": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "RangedValue_for_Uint128": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Uint128" + }, + "minimum": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" diff --git a/contracts/mixnet/schema/raw/execute.json b/contracts/mixnet/schema/raw/execute.json index c4f1817fc9..c5c773e76d 100644 --- a/contracts/mixnet/schema/raw/execute.json +++ b/contracts/mixnet/schema/raw/execute.json @@ -2,6 +2,28 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "ExecuteMsg", "oneOf": [ + { + "description": "Change the admin", + "type": "object", + "required": [ + "update_admin" + ], + "properties": { + "update_admin": { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -1029,6 +1051,42 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "migrate_vested_mix_node" + ], + "properties": { + "migrate_vested_mix_node": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "migrate_vested_delegation" + ], + "properties": { + "migrate_vested_delegation": { + "type": "object", + "required": [ + "mix_id" + ], + "properties": { + "mix_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": { @@ -1055,6 +1113,18 @@ "minimum_mixnode_pledge" ], "properties": { + "interval_operating_cost": { + "description": "Defines the allowed interval operating cost range of operators. default: 0 - 1'000'000'000'000'000 (1 Billion native tokens - the total supply)", + "default": { + "maximum": "1000000000000000", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Uint128" + } + ] + }, "minimum_gateway_pledge": { "description": "Minimum amount a gateway must pledge to get into the system.", "allOf": [ @@ -1081,6 +1151,18 @@ "$ref": "#/definitions/Coin" } ] + }, + "profit_margin": { + "description": "Defines the allowed profit margin range of operators. default: 0% - 100%", + "default": { + "maximum": "1", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Percent" + } + ] } }, "additionalProperties": false @@ -1415,6 +1497,38 @@ } ] }, + "RangedValue_for_Percent": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Percent" + }, + "minimum": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "RangedValue_for_Uint128": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Uint128" + }, + "minimum": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" diff --git a/contracts/mixnet/schema/raw/instantiate.json b/contracts/mixnet/schema/raw/instantiate.json index 60cf202f08..e025d9f11f 100644 --- a/contracts/mixnet/schema/raw/instantiate.json +++ b/contracts/mixnet/schema/raw/instantiate.json @@ -22,6 +22,28 @@ "initial_rewarding_params": { "$ref": "#/definitions/InitialRewardingParams" }, + "interval_operating_cost": { + "default": { + "maximum": "1000000000000000", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Uint128" + } + ] + }, + "profit_margin": { + "default": { + "maximum": "1", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Percent" + } + ] + }, "rewarding_denom": { "type": "string" }, @@ -108,6 +130,42 @@ "$ref": "#/definitions/Decimal" } ] + }, + "RangedValue_for_Percent": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Percent" + }, + "minimum": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "RangedValue_for_Uint128": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Uint128" + }, + "minimum": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" } } } diff --git a/contracts/mixnet/schema/raw/query.json b/contracts/mixnet/schema/raw/query.json index f4738a91fe..c938a56f64 100644 --- a/contracts/mixnet/schema/raw/query.json +++ b/contracts/mixnet/schema/raw/query.json @@ -2,6 +2,19 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "QueryMsg", "oneOf": [ + { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Gets the list of families registered in this contract.", "type": "object", diff --git a/contracts/mixnet/schema/raw/response_to_admin.json b/contracts/mixnet/schema/raw/response_to_admin.json new file mode 100644 index 0000000000..c73969ab04 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_admin.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AdminResponse", + "description": "Returned from Admin.query_admin()", + "type": "object", + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false +} diff --git a/contracts/mixnet/schema/raw/response_to_get_state.json b/contracts/mixnet/schema/raw/response_to_get_state.json index 0888e30787..9103e87b72 100644 --- a/contracts/mixnet/schema/raw/response_to_get_state.json +++ b/contracts/mixnet/schema/raw/response_to_get_state.json @@ -4,7 +4,6 @@ "description": "The current state of the mixnet contract.", "type": "object", "required": [ - "owner", "params", "rewarding_denom", "rewarding_validator_address", @@ -13,9 +12,14 @@ "properties": { "owner": { "description": "Address of the contract owner.", - "allOf": [ + "default": null, + "deprecated": true, + "anyOf": [ { "$ref": "#/definitions/Addr" + }, + { + "type": "null" } ] }, @@ -77,6 +81,18 @@ "minimum_mixnode_pledge" ], "properties": { + "interval_operating_cost": { + "description": "Defines the allowed interval operating cost range of operators. default: 0 - 1'000'000'000'000'000 (1 Billion native tokens - the total supply)", + "default": { + "maximum": "1000000000000000", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Uint128" + } + ] + }, "minimum_gateway_pledge": { "description": "Minimum amount a gateway must pledge to get into the system.", "allOf": [ @@ -103,6 +119,62 @@ "$ref": "#/definitions/Coin" } ] + }, + "profit_margin": { + "description": "Defines the allowed profit margin range of operators. default: 0% - 100%", + "default": { + "maximum": "1", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Percent" + } + ] + } + }, + "additionalProperties": false + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "RangedValue_for_Percent": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Percent" + }, + "minimum": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "RangedValue_for_Uint128": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Uint128" + }, + "minimum": { + "$ref": "#/definitions/Uint128" } }, "additionalProperties": false diff --git a/contracts/mixnet/schema/raw/response_to_get_state_params.json b/contracts/mixnet/schema/raw/response_to_get_state_params.json index 6e05de3035..52d0191167 100644 --- a/contracts/mixnet/schema/raw/response_to_get_state_params.json +++ b/contracts/mixnet/schema/raw/response_to_get_state_params.json @@ -8,6 +8,18 @@ "minimum_mixnode_pledge" ], "properties": { + "interval_operating_cost": { + "description": "Defines the allowed interval operating cost range of operators. default: 0 - 1'000'000'000'000'000 (1 Billion native tokens - the total supply)", + "default": { + "maximum": "1000000000000000", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Uint128" + } + ] + }, "minimum_gateway_pledge": { "description": "Minimum amount a gateway must pledge to get into the system.", "allOf": [ @@ -34,6 +46,18 @@ "$ref": "#/definitions/Coin" } ] + }, + "profit_margin": { + "description": "Defines the allowed profit margin range of operators. default: 0% - 100%", + "default": { + "maximum": "1", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Percent" + } + ] } }, "additionalProperties": false, @@ -53,6 +77,50 @@ } } }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "RangedValue_for_Percent": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Percent" + }, + "minimum": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "RangedValue_for_Uint128": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Uint128" + }, + "minimum": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" diff --git a/contracts/mixnet/src/constants.rs b/contracts/mixnet/src/constants.rs index 17a62a25a8..0ea867c00c 100644 --- a/contracts/mixnet/src/constants.rs +++ b/contracts/mixnet/src/constants.rs @@ -57,6 +57,7 @@ pub const PENDING_INTERVAL_EVENTS_NAMESPACE: &str = "pie"; pub const LAST_EPOCH_EVENT_ID_KEY: &str = "lee"; pub const LAST_INTERVAL_EVENT_ID_KEY: &str = "lie"; +pub const ADMIN_STORAGE_KEY: &str = "admin"; pub const CONTRACT_STATE_KEY: &str = "state"; pub const LAYER_DISTRIBUTION_KEY: &str = "layers"; diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index e6c755d05d..f56af17cd7 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -11,7 +11,8 @@ use cosmwasm_std::{ }; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::{ - ContractState, ContractStateParams, ExecuteMsg, InstantiateMsg, Interval, MigrateMsg, QueryMsg, + ContractState, ContractStateParams, ExecuteMsg, InstantiateMsg, Interval, MigrateMsg, + OperatingCostRange, ProfitMarginRange, QueryMsg, }; use nym_contracts_common::set_build_information; @@ -24,9 +25,13 @@ fn default_initial_state( rewarding_validator_address: Addr, rewarding_denom: String, vesting_contract_address: Addr, + profit_margin: ProfitMarginRange, + interval_operating_cost: OperatingCostRange, ) -> ContractState { + // we have to temporarily preserve this functionalities until it can be removed + #[allow(deprecated)] ContractState { - owner, + owner: Some(owner), rewarding_validator_address, vesting_contract_address, rewarding_denom: rewarding_denom.clone(), @@ -40,6 +45,8 @@ fn default_initial_state( denom: rewarding_denom, amount: INITIAL_GATEWAY_PLEDGE_AMOUNT, }, + profit_margin, + interval_operating_cost, }, } } @@ -51,7 +58,7 @@ fn default_initial_state( /// `msg` is the contract initialization message, sort of like a constructor call. #[entry_point] pub fn instantiate( - deps: DepsMut<'_>, + mut deps: DepsMut<'_>, env: Env, info: MessageInfo, msg: InstantiateMsg, @@ -67,10 +74,12 @@ pub fn instantiate( let rewarding_validator_address = deps.api.addr_validate(&msg.rewarding_validator_address)?; let vesting_contract_address = deps.api.addr_validate(&msg.vesting_contract_address)?; let state = default_initial_state( - info.sender, + info.sender.clone(), rewarding_validator_address.clone(), msg.rewarding_denom, vesting_contract_address, + msg.profit_margin, + msg.interval_operating_cost, ); let starting_interval = Interval::init_interval(msg.epochs_in_interval, msg.epoch_duration, &env); @@ -83,7 +92,7 @@ pub fn instantiate( starting_interval, rewarding_validator_address, )?; - mixnet_params_storage::initialise_storage(deps.storage, state)?; + mixnet_params_storage::initialise_storage(deps.branch(), state, info.sender)?; mixnode_storage::initialise_storage(deps.storage)?; rewards_storage::initialise_storage(deps.storage, reward_params)?; cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; @@ -101,6 +110,11 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { + ExecuteMsg::UpdateAdmin { admin } => { + crate::mixnet_contract_settings::transactions::try_update_contract_admin( + deps, info, admin, + ) + } ExecuteMsg::AssignNodeLayer { mix_id, layer } => { crate::mixnodes::transactions::assign_mixnode_layer(deps, info, mix_id, layer) } @@ -118,44 +132,6 @@ pub fn execute( ExecuteMsg::KickFamilyMember { member } => { crate::families::transactions::try_head_kick_member(deps, info, member) } - ExecuteMsg::CreateFamilyOnBehalf { - owner_address, - label, - } => crate::families::transactions::try_create_family_on_behalf( - deps, - info, - owner_address, - label, - ), - ExecuteMsg::JoinFamilyOnBehalf { - member_address, - join_permit, - family_head, - } => crate::families::transactions::try_join_family_on_behalf( - deps, - info, - member_address, - join_permit, - family_head, - ), - ExecuteMsg::LeaveFamilyOnBehalf { - member_address, - family_head, - } => crate::families::transactions::try_leave_family_on_behalf( - deps, - info, - member_address, - family_head, - ), - ExecuteMsg::KickFamilyMemberOnBehalf { - head_address, - member, - } => crate::families::transactions::try_head_kick_member_on_behalf( - deps, - info, - head_address, - member, - ), // state/sys-params-related ExecuteMsg::UpdateRewardingValidatorAddress { address } => { crate::mixnet_contract_settings::transactions::try_update_rewarding_validator_address( @@ -232,62 +208,23 @@ pub fn execute( cost_params, owner_signature, ), - ExecuteMsg::BondMixnodeOnBehalf { - mix_node, - cost_params, - owner, - owner_signature, - } => crate::mixnodes::transactions::try_add_mixnode_on_behalf( - deps, - env, - info, - mix_node, - cost_params, - owner, - owner_signature, - ), ExecuteMsg::PledgeMore {} => { crate::mixnodes::transactions::try_increase_pledge(deps, env, info) } - ExecuteMsg::PledgeMoreOnBehalf { owner } => { - crate::mixnodes::transactions::try_increase_pledge_on_behalf(deps, env, info, owner) - } ExecuteMsg::DecreasePledge { decrease_by } => { crate::mixnodes::transactions::try_decrease_pledge(deps, env, info, decrease_by) } - ExecuteMsg::DecreasePledgeOnBehalf { owner, decrease_by } => { - crate::mixnodes::transactions::try_decrease_pledge_on_behalf( - deps, - env, - info, - decrease_by, - owner, - ) - } ExecuteMsg::UnbondMixnode {} => { crate::mixnodes::transactions::try_remove_mixnode(deps, env, info) } - ExecuteMsg::UnbondMixnodeOnBehalf { owner } => { - crate::mixnodes::transactions::try_remove_mixnode_on_behalf(deps, env, info, owner) - } ExecuteMsg::UpdateMixnodeCostParams { new_costs } => { crate::mixnodes::transactions::try_update_mixnode_cost_params( deps, env, info, new_costs, ) } - ExecuteMsg::UpdateMixnodeCostParamsOnBehalf { new_costs, owner } => { - crate::mixnodes::transactions::try_update_mixnode_cost_params_on_behalf( - deps, env, info, new_costs, owner, - ) - } ExecuteMsg::UpdateMixnodeConfig { new_config } => { crate::mixnodes::transactions::try_update_mixnode_config(deps, info, new_config) } - ExecuteMsg::UpdateMixnodeConfigOnBehalf { new_config, owner } => { - crate::mixnodes::transactions::try_update_mixnode_config_on_behalf( - deps, info, new_config, owner, - ) - } // gateway-related: ExecuteMsg::BondGateway { @@ -300,52 +237,22 @@ pub fn execute( gateway, owner_signature, ), - ExecuteMsg::BondGatewayOnBehalf { - gateway, - owner, - owner_signature, - } => crate::gateways::transactions::try_add_gateway_on_behalf( - deps, - env, - info, - gateway, - owner, - owner_signature, - ), ExecuteMsg::UnbondGateway {} => { crate::gateways::transactions::try_remove_gateway(deps, info) } - ExecuteMsg::UnbondGatewayOnBehalf { owner } => { - crate::gateways::transactions::try_remove_gateway_on_behalf(deps, info, owner) - } ExecuteMsg::UpdateGatewayConfig { new_config } => { crate::gateways::transactions::try_update_gateway_config(deps, info, new_config) } - ExecuteMsg::UpdateGatewayConfigOnBehalf { new_config, owner } => { - crate::gateways::transactions::try_update_gateway_config_on_behalf( - deps, info, new_config, owner, - ) - } // delegation-related: ExecuteMsg::DelegateToMixnode { mix_id } => { crate::delegations::transactions::try_delegate_to_mixnode(deps, env, info, mix_id) } - ExecuteMsg::DelegateToMixnodeOnBehalf { mix_id, delegate } => { - crate::delegations::transactions::try_delegate_to_mixnode_on_behalf( - deps, env, info, mix_id, delegate, - ) - } ExecuteMsg::UndelegateFromMixnode { mix_id } => { crate::delegations::transactions::try_remove_delegation_from_mixnode( deps, env, info, mix_id, ) } - ExecuteMsg::UndelegateFromMixnodeOnBehalf { mix_id, delegate } => { - crate::delegations::transactions::try_remove_delegation_from_mixnode_on_behalf( - deps, env, info, mix_id, delegate, - ) - } // reward-related ExecuteMsg::RewardMixnode { @@ -356,16 +263,37 @@ pub fn execute( ExecuteMsg::WithdrawOperatorReward {} => { crate::rewards::transactions::try_withdraw_operator_reward(deps, info) } - ExecuteMsg::WithdrawOperatorRewardOnBehalf { owner } => { - crate::rewards::transactions::try_withdraw_operator_reward_on_behalf(deps, info, owner) - } ExecuteMsg::WithdrawDelegatorReward { mix_id } => { crate::rewards::transactions::try_withdraw_delegator_reward(deps, info, mix_id) } - ExecuteMsg::WithdrawDelegatorRewardOnBehalf { mix_id, owner } => { - crate::rewards::transactions::try_withdraw_delegator_reward_on_behalf( - deps, info, mix_id, owner, - ) + + // vesting migration: + ExecuteMsg::MigrateVestedMixNode { .. } => { + crate::vesting_migration::try_migrate_vested_mixnode(deps, info) + } + ExecuteMsg::MigrateVestedDelegation { mix_id } => { + crate::vesting_migration::try_migrate_vested_delegation(deps, info, mix_id) + } + + // legacy vesting + ExecuteMsg::CreateFamilyOnBehalf { .. } + | ExecuteMsg::JoinFamilyOnBehalf { .. } + | ExecuteMsg::LeaveFamilyOnBehalf { .. } + | ExecuteMsg::KickFamilyMemberOnBehalf { .. } + | ExecuteMsg::BondMixnodeOnBehalf { .. } + | ExecuteMsg::PledgeMoreOnBehalf { .. } + | ExecuteMsg::DecreasePledgeOnBehalf { .. } + | ExecuteMsg::UnbondMixnodeOnBehalf { .. } + | ExecuteMsg::UpdateMixnodeCostParamsOnBehalf { .. } + | ExecuteMsg::UpdateMixnodeConfigOnBehalf { .. } + | ExecuteMsg::BondGatewayOnBehalf { .. } + | ExecuteMsg::UnbondGatewayOnBehalf { .. } + | ExecuteMsg::UpdateGatewayConfigOnBehalf { .. } + | ExecuteMsg::DelegateToMixnodeOnBehalf { .. } + | ExecuteMsg::UndelegateFromMixnodeOnBehalf { .. } + | ExecuteMsg::WithdrawOperatorRewardOnBehalf { .. } + | ExecuteMsg::WithdrawDelegatorRewardOnBehalf { .. } => { + Err(MixnetContractError::DisabledVestingOperation) } // testing-only @@ -414,6 +342,9 @@ pub fn query( QueryMsg::GetState {} => { to_binary(&crate::mixnet_contract_settings::queries::query_contract_state(deps)?) } + QueryMsg::Admin {} => to_binary(&crate::mixnet_contract_settings::queries::query_admin( + deps, + )?), QueryMsg::GetRewardingParams {} => { to_binary(&crate::rewards::queries::query_rewarding_params(deps)?) } @@ -607,13 +538,16 @@ pub fn query( #[entry_point] pub fn migrate( - deps: DepsMut<'_>, + mut deps: DepsMut<'_>, _env: Env, msg: MigrateMsg, ) -> Result { set_build_information!(deps.storage)?; cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + crate::queued_migrations::vesting_purge(deps.branch())?; + crate::queued_migrations::explicit_contract_admin(deps.branch())?; + // 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 @@ -631,7 +565,7 @@ pub fn migrate( mod tests { use super::*; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - use cosmwasm_std::Decimal; + use cosmwasm_std::{Decimal, Uint128}; use mixnet_contract_common::reward_params::{IntervalRewardParams, RewardingParams}; use mixnet_contract_common::{InitialRewardingParams, Percent}; use std::time::Duration; @@ -657,14 +591,23 @@ mod tests { rewarded_set_size: 543, active_set_size: 123, }, + profit_margin: ProfitMarginRange { + minimum: "0.05".parse().unwrap(), + maximum: "0.95".parse().unwrap(), + }, + interval_operating_cost: OperatingCostRange { + minimum: "1000".parse().unwrap(), + maximum: "10000".parse().unwrap(), + }, }; let sender = mock_info("sender", &[]); let res = instantiate(deps.as_mut(), env, sender, init_msg); assert!(res.is_ok()); + #[allow(deprecated)] let expected_state = ContractState { - owner: Addr::unchecked("sender"), + owner: Some(Addr::unchecked("sender")), rewarding_validator_address: Addr::unchecked("foomp123"), vesting_contract_address: Addr::unchecked("bar456"), rewarding_denom: "uatom".into(), @@ -678,6 +621,14 @@ mod tests { denom: "uatom".into(), amount: INITIAL_GATEWAY_PLEDGE_AMOUNT, }, + profit_margin: ProfitMarginRange { + minimum: Percent::from_percentage_value(5).unwrap(), + maximum: Percent::from_percentage_value(95).unwrap(), + }, + interval_operating_cost: OperatingCostRange { + minimum: Uint128::new(1000), + maximum: Uint128::new(10000), + }, }, }; diff --git a/contracts/mixnet/src/delegations/queries.rs b/contracts/mixnet/src/delegations/queries.rs index 5a5d07c32f..01e7b65648 100644 --- a/contracts/mixnet/src/delegations/queries.rs +++ b/contracts/mixnet/src/delegations/queries.rs @@ -302,10 +302,7 @@ mod tests { mod delegator_delegations { use super::*; - use crate::delegations::transactions::try_delegate_to_mixnode_on_behalf; - use crate::support::tests::fixtures::TEST_COIN_DENOM; - use cosmwasm_std::testing::mock_info; - use cosmwasm_std::{coin, Addr}; + use cosmwasm_std::Addr; #[test] fn obeys_limits() { @@ -453,25 +450,10 @@ mod tests { #[test] fn all_retrieved_delegations_are_from_the_specified_delegator() { let mut test = TestSetup::new(); - let env = test.env(); + // it means we have, for example, delegation from "delegator1" towards mix1, mix2, ...., from "delegator2" towards mix1, mix2, ...., etc add_dummy_mixes_with_delegations(&mut test, 50, 100); - // add some proxies while we're at it to make sure they're queried for separately - let with_proxy = "delegator42"; - let vesting_contract = test.vesting_contract(); - for mix_id in 1..=25 { - try_delegate_to_mixnode_on_behalf( - test.deps_mut(), - env.clone(), - mock_info(vesting_contract.as_ref(), &[coin(100_000, TEST_COIN_DENOM)]), - mix_id, - with_proxy.into(), - ) - .unwrap(); - } - test.execute_all_pending_events(); - // make few queries let res1 = query_delegator_delegations_paged(test.deps(), "delegator2".into(), None, None) @@ -490,59 +472,6 @@ mod tests { .delegations .into_iter() .all(|d| d.owner == Addr::unchecked("delegator35"))); - - let with_proxy_full = - query_delegator_delegations_paged(test.deps(), with_proxy.into(), None, None) - .unwrap(); - assert_eq!(with_proxy_full.delegations.len(), 125); - - // all delegations have correct owner - assert!(with_proxy_full - .delegations - .iter() - .all(|d| d.owner == Addr::unchecked(with_proxy))); - - // and we have 100 delegations without proxy and 25 with - let no_proxy = with_proxy_full - .delegations - .iter() - .filter(|d| d.proxy.is_none()) - .count(); - assert_eq!(no_proxy, 100); - let proxy = with_proxy_full - .delegations - .iter() - .filter(|d| d.proxy.is_some()) - .count(); - assert_eq!(proxy, 25); - - assert!(with_proxy_full - .delegations - .iter() - .filter(|d| d.proxy.is_some()) - .all(|d| d.proxy.as_ref().unwrap() == vesting_contract)); - - // now make sure that if we do it in paged manner, we'll get exactly the same result - let per_page = Some(15); - let mut delegations = Vec::new(); - let mut start_after = None; - loop { - let mut paged_response = query_delegator_delegations_paged( - test.deps(), - with_proxy.into(), - start_after, - per_page, - ) - .unwrap(); - delegations.append(&mut paged_response.delegations); - - if let Some(start_after_res) = paged_response.start_next_after { - start_after = Some(start_after_res) - } else { - break; - } - } - assert_eq!(with_proxy_full.delegations, delegations) } } diff --git a/contracts/mixnet/src/delegations/transactions.rs b/contracts/mixnet/src/delegations/transactions.rs index adc70c54ce..b91c6c8582 100644 --- a/contracts/mixnet/src/delegations/transactions.rs +++ b/contracts/mixnet/src/delegations/transactions.rs @@ -1,14 +1,12 @@ -// Copyright 2021-2023 - Nym Technologies SA +// Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use super::storage; use crate::interval::storage as interval_storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::mixnodes::storage as mixnodes_storage; -use crate::support::helpers::{ - ensure_epoch_in_progress_state, ensure_sent_by_vesting_contract, validate_delegation_stake, -}; -use cosmwasm_std::{Addr, Coin, DepsMut, Env, MessageInfo, Response}; +use crate::support::helpers::{ensure_epoch_in_progress_state, validate_delegation_stake}; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ new_pending_delegation_event, new_pending_undelegation_event, @@ -21,30 +19,6 @@ pub(crate) fn try_delegate_to_mixnode( env: Env, info: MessageInfo, mix_id: MixId, -) -> Result { - _try_delegate_to_mixnode(deps, env, mix_id, info.sender, info.funds, None) -} - -pub(crate) fn try_delegate_to_mixnode_on_behalf( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - mix_id: MixId, - delegate: String, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let delegate = deps.api.addr_validate(&delegate)?; - _try_delegate_to_mixnode(deps, env, mix_id, delegate, info.funds, Some(info.sender)) -} - -pub(crate) fn _try_delegate_to_mixnode( - deps: DepsMut<'_>, - env: Env, - mix_id: MixId, - delegate: Addr, - amount: Vec, - proxy: Option, ) -> Result { // delegation is only allowed if the epoch is currently not in the process of being advanced ensure_epoch_in_progress_state(deps.storage)?; @@ -52,7 +26,7 @@ pub(crate) fn _try_delegate_to_mixnode( // check if the delegation contains any funds of the appropriate denomination let contract_state = mixnet_params_storage::CONTRACT_STATE.load(deps.storage)?; let delegation = validate_delegation_stake( - amount, + info.funds, contract_state.params.minimum_mixnode_delegation, contract_state.rewarding_denom, )?; @@ -67,14 +41,9 @@ pub(crate) fn _try_delegate_to_mixnode( } // push the event onto the queue and wait for it to be picked up at the end of the epoch - let cosmos_event = new_pending_delegation_event(&delegate, &proxy, &delegation, mix_id); + let cosmos_event = new_pending_delegation_event(&info.sender, &delegation, mix_id); - let epoch_event = PendingEpochEventKind::Delegate { - owner: delegate, - mix_id, - amount: delegation, - proxy, - }; + let epoch_event = PendingEpochEventKind::new_delegate(info.sender, mix_id, delegation); interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?; Ok(Response::new().add_event(cosmos_event)) @@ -85,35 +54,12 @@ pub(crate) fn try_remove_delegation_from_mixnode( env: Env, info: MessageInfo, mix_id: MixId, -) -> Result { - _try_remove_delegation_from_mixnode(deps, env, mix_id, info.sender, None) -} - -pub(crate) fn try_remove_delegation_from_mixnode_on_behalf( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - mix_id: MixId, - delegate: String, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let delegate = deps.api.addr_validate(&delegate)?; - _try_remove_delegation_from_mixnode(deps, env, mix_id, delegate, Some(info.sender)) -} - -pub(crate) fn _try_remove_delegation_from_mixnode( - deps: DepsMut<'_>, - env: Env, - mix_id: MixId, - delegate: Addr, - proxy: Option, ) -> Result { // undelegation is only allowed if the epoch is currently not in the process of being advanced ensure_epoch_in_progress_state(deps.storage)?; // see if the delegation even exists - let storage_key = Delegation::generate_storage_key(mix_id, &delegate, proxy.as_ref()); + let storage_key = Delegation::generate_storage_key(mix_id, &info.sender, None); if storage::delegations() .may_load(deps.storage, storage_key)? @@ -121,19 +67,15 @@ pub(crate) fn _try_remove_delegation_from_mixnode( { return Err(MixnetContractError::NoMixnodeDelegationFound { mix_id, - address: delegate.into_string(), - proxy: proxy.map(Addr::into_string), + address: info.sender.into_string(), + proxy: None, }); } // push the event onto the queue and wait for it to be picked up at the end of the epoch - let cosmos_event = new_pending_undelegation_event(&delegate, &proxy, mix_id); + let cosmos_event = new_pending_undelegation_event(&info.sender, mix_id); - let epoch_event = PendingEpochEventKind::Undelegate { - owner: delegate, - mix_id, - proxy, - }; + let epoch_event = PendingEpochEventKind::new_undelegate(info.sender, mix_id); interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?; Ok(Response::new().add_event(cosmos_event)) @@ -151,7 +93,7 @@ mod tests { use crate::support::tests::fixtures::TEST_COIN_DENOM; use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::testing::mock_info; - use cosmwasm_std::{coin, Decimal}; + use cosmwasm_std::{coin, Addr, Decimal}; use mixnet_contract_common::{EpochState, EpochStatus}; #[test] @@ -368,65 +310,17 @@ mod tests { let mix_id = test.add_dummy_mixnode("mix-owner", None); let amount1 = coin(100_000_000, TEST_COIN_DENOM); - let amount2 = coin(50_000_000, TEST_COIN_DENOM); let sender1 = mock_info(owner, &[amount1.clone()]); - let sender2 = mock_info(test.vesting_contract().as_str(), &[amount2.clone()]); try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id).unwrap(); - try_delegate_to_mixnode_on_behalf(test.deps_mut(), env, sender2, mix_id, owner.into()) - .unwrap(); let events = test.pending_epoch_events(); assert_eq!( events[0].kind, - PendingEpochEventKind::Delegate { - owner: Addr::unchecked(owner), - mix_id, - amount: amount1, - proxy: None - } + PendingEpochEventKind::new_delegate(Addr::unchecked(owner), mix_id, amount1,) ); - - assert_eq!( - events[1].kind, - PendingEpochEventKind::Delegate { - owner: Addr::unchecked(owner), - mix_id, - amount: amount2, - proxy: Some(test.vesting_contract()) - } - ); - } - - #[test] - fn fails_for_illegal_proxy() { - let mut test = TestSetup::new(); - let env = test.env(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let owner = "delegator"; - let mix_id = test.add_dummy_mixnode("mix-owner", None); - - let res = try_delegate_to_mixnode_on_behalf( - test.deps_mut(), - env, - mock_info(illegal_proxy.as_ref(), &[coin(123, TEST_COIN_DENOM)]), - mix_id, - owner.into(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract - } - ) } } @@ -573,40 +467,5 @@ mod tests { ); assert!(res.is_ok()); } - - #[test] - fn fails_for_illegal_proxy() { - let mut test = TestSetup::new(); - let env = test.env(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let owner = "delegator"; - let mix_id = test.add_dummy_mixnode("mix-owner", None); - test.add_immediate_delegation_with_illegal_proxy( - owner, - 10000u32, - mix_id, - illegal_proxy.clone(), - ); - - let res = try_remove_delegation_from_mixnode_on_behalf( - test.deps_mut(), - env, - mock_info(illegal_proxy.as_ref(), &[coin(123, TEST_COIN_DENOM)]), - mix_id, - owner.into(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract - } - ) - } } } diff --git a/contracts/mixnet/src/families/signature_helpers.rs b/contracts/mixnet/src/families/signature_helpers.rs index 222c7548cd..3c6bb79626 100644 --- a/contracts/mixnet/src/families/signature_helpers.rs +++ b/contracts/mixnet/src/families/signature_helpers.rs @@ -4,7 +4,7 @@ use crate::mixnodes::storage as mixnodes_storage; use crate::signing::storage as signing_storage; use crate::support::helpers::decode_ed25519_identity_key; -use cosmwasm_std::{Addr, Deps}; +use cosmwasm_std::Deps; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::families::FamilyHead; use mixnet_contract_common::{construct_family_join_permit, IdentityKeyRef}; @@ -13,7 +13,6 @@ use nym_contracts_common::signing::{MessageSignature, Verifier}; pub(crate) fn verify_family_join_permit( deps: Deps<'_>, granter: FamilyHead, - proxy: Option, member: IdentityKeyRef, signature: MessageSignature, ) -> Result<(), MixnetContractError> { @@ -32,7 +31,7 @@ pub(crate) fn verify_family_join_permit( }); }; let nonce = signing_storage::get_signing_nonce(deps.storage, head_mixnode.owner)?; - let msg = construct_family_join_permit(nonce, granter, proxy, member.to_owned()); + let msg = construct_family_join_permit(nonce, granter, member.to_owned()); if deps.api.verify_message(msg, signature, &public_key)? { Ok(()) diff --git a/contracts/mixnet/src/families/transactions.rs b/contracts/mixnet/src/families/transactions.rs index 732acddb43..ee88f98e63 100644 --- a/contracts/mixnet/src/families/transactions.rs +++ b/contracts/mixnet/src/families/transactions.rs @@ -1,4 +1,4 @@ -// Copyright 2022-2023 - Nym Technologies SA +// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use super::storage::{ @@ -7,41 +7,20 @@ use super::storage::{ }; use crate::families::queries::get_family_by_label; use crate::families::signature_helpers::verify_family_join_permit; -use crate::support::helpers::{ensure_bonded, ensure_sent_by_vesting_contract}; -use cosmwasm_std::{Addr, DepsMut, MessageInfo, Response}; +use crate::support::helpers::ensure_bonded; +use cosmwasm_std::{DepsMut, MessageInfo, Response}; use mixnet_contract_common::families::{Family, FamilyHead}; use mixnet_contract_common::{error::MixnetContractError, IdentityKey}; use nym_contracts_common::signing::MessageSignature; /// Creates a new MixNode family with senders node as head -pub fn try_create_family( +pub(crate) fn try_create_family( deps: DepsMut, info: MessageInfo, label: String, -) -> Result { - _try_create_family(deps, &info.sender, label, None) -} - -pub fn try_create_family_on_behalf( - deps: DepsMut, - info: MessageInfo, - owner_address: String, - label: String, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let owner_address = deps.api.addr_validate(&owner_address)?; - _try_create_family(deps, &owner_address, label, Some(info.sender)) -} - -fn _try_create_family( - deps: DepsMut, - owner: &Addr, - label: String, - proxy: Option, ) -> Result { let existing_bond = - crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, owner)?; + crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; ensure_bonded(&existing_bond)?; @@ -60,43 +39,19 @@ fn _try_create_family( return Err(MixnetContractError::FamilyWithLabelExists(label)); } - let family = Family::new(family_head, proxy, label); + let family = Family::new(family_head, label); save_family(&family, deps.storage)?; Ok(Response::default()) } -pub fn try_join_family( +pub(crate) fn try_join_family( deps: DepsMut, info: MessageInfo, join_permit: MessageSignature, family_head: FamilyHead, -) -> Result { - _try_join_family(deps, &info.sender, join_permit, family_head, None) -} - -pub fn try_join_family_on_behalf( - deps: DepsMut, - info: MessageInfo, - member_address: String, - join_permit: MessageSignature, - family_head: FamilyHead, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let member_address = deps.api.addr_validate(&member_address)?; - let proxy = Some(info.sender); - _try_join_family(deps, &member_address, join_permit, family_head, proxy) -} - -fn _try_join_family( - deps: DepsMut, - owner: &Addr, - join_permit: MessageSignature, - family_head: FamilyHead, - proxy: Option, ) -> Result { let existing_bond = - crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, owner)?; + crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; ensure_bonded(&existing_bond)?; @@ -116,7 +71,6 @@ fn _try_join_family( verify_family_join_permit( deps.as_ref(), family_head.clone(), - proxy, existing_bond.identity(), join_permit, )?; @@ -128,33 +82,13 @@ fn _try_join_family( Ok(Response::default()) } -pub fn try_leave_family( +pub(crate) fn try_leave_family( deps: DepsMut, info: MessageInfo, family_head: FamilyHead, -) -> Result { - _try_leave_family(deps, &info.sender, family_head) -} - -pub fn try_leave_family_on_behalf( - deps: DepsMut, - info: MessageInfo, - member_address: String, - family_head: FamilyHead, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let member_address = deps.api.addr_validate(&member_address)?; - _try_leave_family(deps, &member_address, family_head) -} - -fn _try_leave_family( - deps: DepsMut, - owner: &Addr, - family_head: FamilyHead, ) -> Result { let existing_bond = - crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, owner)?; + crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; ensure_bonded(&existing_bond)?; @@ -178,32 +112,13 @@ fn _try_leave_family( Ok(Response::default()) } -pub fn try_head_kick_member( +pub(crate) fn try_head_kick_member( deps: DepsMut, info: MessageInfo, member: IdentityKey, ) -> Result { - _try_head_kick_member(deps, &info.sender, member) -} - -pub fn try_head_kick_member_on_behalf( - deps: DepsMut, - info: MessageInfo, - head_address: String, - member: IdentityKey, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let head_address = deps.api.addr_validate(&head_address)?; - _try_head_kick_member(deps, &head_address, member) -} - -fn _try_head_kick_member( - deps: DepsMut, - owner: &Addr, - member: IdentityKey, -) -> Result { - let head_bond = crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, owner)?; + let head_bond = + crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; // make sure we're still in the mixnet ensure_bonded(&head_bond)?; @@ -321,7 +236,7 @@ mod test { assert_eq!(family.head_identity(), family_head.identity()); let join_permit = - test.generate_family_join_permit(&head_keypair, &member_mixnode.identity_key, false); + test.generate_family_join_permit(&head_keypair, &member_mixnode.identity_key); try_join_family( test.deps_mut(), @@ -345,7 +260,7 @@ mod test { ); let new_join_permit = - test.generate_family_join_permit(&head_keypair, &member_mixnode.identity_key, false); + test.generate_family_join_permit(&head_keypair, &member_mixnode.identity_key); try_join_family( test.deps_mut(), @@ -373,189 +288,4 @@ mod test { !is_family_member(test.deps().storage, &family, &member_mixnode.identity_key).unwrap() ); } - - #[cfg(test)] - mod creating_family { - use super::*; - - #[test] - fn fails_for_illegal_proxy() { - let mut test = TestSetup::new(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let head = "alice"; - - test.add_dummy_mixnode(head, None); - - let res = try_create_family_on_behalf( - test.deps_mut(), - mock_info(illegal_proxy.as_ref(), &[]), - head.to_string(), - "label".to_string(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract - } - ) - } - } - - #[cfg(test)] - mod joining_family { - use super::*; - - #[test] - fn fails_for_illegal_proxy() { - let mut test = TestSetup::new(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let head = "alice"; - let label = "family"; - let new_member = "vin-diesel"; - - let (_, head_keys) = test.create_dummy_mixnode_with_new_family(head, label); - let (_, member_keys) = test.add_dummy_mixnode_with_keypair(new_member, None); - - let join_permit = test.generate_family_join_permit( - &head_keys, - &member_keys.public_key().to_base58_string(), - false, - ); - - let head_identity = head_keys.public_key().to_base58_string(); - let family_head = FamilyHead::new(head_identity); - let res = try_join_family_on_behalf( - test.deps_mut(), - mock_info(illegal_proxy.as_ref(), &[]), - new_member.to_string(), - join_permit, - family_head, - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract - } - ) - } - } - - #[cfg(test)] - mod leaving_family { - use super::*; - - #[test] - fn fails_for_illegal_proxy() { - let mut test = TestSetup::new(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let head = "alice"; - let label = "family"; - let new_member = "vin-diesel"; - - let (_, head_keys) = test.create_dummy_mixnode_with_new_family(head, label); - let (_, member_keys) = test.add_dummy_mixnode_with_keypair(new_member, None); - - let join_permit = test.generate_family_join_permit( - &head_keys, - &member_keys.public_key().to_base58_string(), - true, - ); - - let head_identity = head_keys.public_key().to_base58_string(); - let family_head = FamilyHead::new(head_identity); - try_join_family_on_behalf( - test.deps_mut(), - mock_info(vesting_contract.as_ref(), &[]), - new_member.to_string(), - join_permit, - family_head.clone(), - ) - .unwrap(); - - let res = try_leave_family_on_behalf( - test.deps_mut(), - mock_info(illegal_proxy.as_ref(), &[]), - new_member.to_string(), - family_head, - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract - } - ) - } - } - - #[cfg(test)] - mod kicking_family_member { - use super::*; - - #[test] - fn fails_for_illegal_proxy() { - let mut test = TestSetup::new(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let head = "alice"; - let label = "family"; - let new_member = "vin-diesel"; - - let (_, head_keys) = test.create_dummy_mixnode_with_new_family(head, label); - let (_, member_keys) = test.add_dummy_mixnode_with_keypair(new_member, None); - - let join_permit = test.generate_family_join_permit( - &head_keys, - &member_keys.public_key().to_base58_string(), - true, - ); - - let head_identity = head_keys.public_key().to_base58_string(); - let family_head = FamilyHead::new(head_identity); - - try_join_family_on_behalf( - test.deps_mut(), - mock_info(vesting_contract.as_ref(), &[]), - new_member.to_string(), - join_permit, - family_head, - ) - .unwrap(); - - let res = try_head_kick_member_on_behalf( - test.deps_mut(), - mock_info(illegal_proxy.as_ref(), &[]), - head.to_string(), - member_keys.public_key().to_base58_string(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract - } - ) - } - } } diff --git a/contracts/mixnet/src/gateways/signature_helpers.rs b/contracts/mixnet/src/gateways/signature_helpers.rs index e6ce7f5ad5..6f9b712554 100644 --- a/contracts/mixnet/src/gateways/signature_helpers.rs +++ b/contracts/mixnet/src/gateways/signature_helpers.rs @@ -5,14 +5,15 @@ use crate::signing::storage as signing_storage; use crate::support::helpers::decode_ed25519_identity_key; use cosmwasm_std::{Addr, Coin, Deps}; use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::{construct_gateway_bonding_sign_payload, Gateway}; +use mixnet_contract_common::{ + construct_gateway_bonding_sign_payload, construct_legacy_gateway_bonding_sign_payload, Gateway, +}; use nym_contracts_common::signing::MessageSignature; use nym_contracts_common::signing::Verifier; pub(crate) fn verify_gateway_bonding_signature( deps: Deps<'_>, sender: Addr, - proxy: Option, pledge: Coin, gateway: Gateway, signature: MessageSignature, @@ -20,13 +21,31 @@ pub(crate) fn verify_gateway_bonding_signature( // recover the public key let public_key = decode_ed25519_identity_key(&gateway.identity_key)?; - // reconstruct the payload + // reconstruct the payload, first try the current format, then attempt legacy let nonce = signing_storage::get_signing_nonce(deps.storage, sender.clone())?; - let msg = construct_gateway_bonding_sign_payload(nonce, sender, proxy, pledge, gateway); + let msg = construct_gateway_bonding_sign_payload( + nonce, + sender.clone(), + pledge.clone(), + gateway.clone(), + ); - if deps.api.verify_message(msg, signature, &public_key)? { + if deps + .api + .verify_message(msg, signature.clone(), &public_key)? + { Ok(()) } else { - Err(MixnetContractError::InvalidEd25519Signature) + // attempt to use legacy + let msg_legacy = + construct_legacy_gateway_bonding_sign_payload(nonce, sender, pledge, gateway); + if deps + .api + .verify_message(msg_legacy, signature, &public_key)? + { + Ok(()) + } else { + Err(MixnetContractError::InvalidEd25519Signature) + } } } diff --git a/contracts/mixnet/src/gateways/transactions.rs b/contracts/mixnet/src/gateways/transactions.rs index f747c4c7ba..6075c25894 100644 --- a/contracts/mixnet/src/gateways/transactions.rs +++ b/contracts/mixnet/src/gateways/transactions.rs @@ -1,4 +1,4 @@ -// Copyright 2021-2023 - Nym Technologies SA +// Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use super::helpers::must_get_gateway_bond_by_owner; @@ -6,10 +6,8 @@ use super::storage; use crate::gateways::signature_helpers::verify_gateway_bonding_signature; use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::signing::storage as signing_storage; -use crate::support::helpers::{ - ensure_no_existing_bond, ensure_proxy_match, ensure_sent_by_vesting_contract, validate_pledge, -}; -use cosmwasm_std::{wasm_execute, Addr, BankMsg, Coin, DepsMut, Env, MessageInfo, Response}; +use crate::support::helpers::{ensure_no_existing_bond, validate_pledge}; +use cosmwasm_std::{BankMsg, DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ new_gateway_bonding_event, new_gateway_config_update_event, new_gateway_unbonding_event, @@ -17,72 +15,28 @@ use mixnet_contract_common::events::{ use mixnet_contract_common::gateway::GatewayConfigUpdate; use mixnet_contract_common::{Gateway, GatewayBond}; use nym_contracts_common::signing::MessageSignature; -use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; - -pub fn try_add_gateway( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - gateway: Gateway, - owner_signature: MessageSignature, -) -> Result { - _try_add_gateway( - deps, - env, - gateway, - info.funds, - info.sender, - owner_signature, - None, - ) -} - -pub fn try_add_gateway_on_behalf( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - gateway: Gateway, - owner: String, - owner_signature: MessageSignature, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let proxy = info.sender; - let owner = deps.api.addr_validate(&owner)?; - _try_add_gateway( - deps, - env, - gateway, - info.funds, - owner, - owner_signature, - Some(proxy), - ) -} // TODO: perhaps also require the user to explicitly provide what it thinks is the current nonce // so that we could return a better error message if it doesn't match? -pub(crate) fn _try_add_gateway( +pub(crate) fn try_add_gateway( deps: DepsMut<'_>, env: Env, + info: MessageInfo, gateway: Gateway, - pledge: Vec, - owner: Addr, owner_signature: MessageSignature, - proxy: Option, ) -> Result { // check if the pledge contains any funds of the appropriate denomination let minimum_pledge = mixnet_params_storage::minimum_gateway_pledge(deps.storage)?; - let pledge = validate_pledge(pledge, minimum_pledge)?; + let pledge = validate_pledge(info.funds, minimum_pledge)?; // if the client has an active bonded mixnode or gateway, don't allow bonding - ensure_no_existing_bond(&owner, deps.storage)?; + ensure_no_existing_bond(&info.sender, deps.storage)?; // check if somebody else has already bonded a gateway with this identity if let Some(existing_bond) = storage::gateways().may_load(deps.storage, &gateway.identity_key)? { - if existing_bond.owner != owner { + if existing_bond.owner != info.sender { return Err(MixnetContractError::DuplicateGateway { owner: existing_bond.owner, }); @@ -92,105 +46,62 @@ pub(crate) fn _try_add_gateway( // check if this sender actually owns the gateway by checking the signature verify_gateway_bonding_signature( deps.as_ref(), - owner.clone(), - proxy.clone(), + info.sender.clone(), pledge.clone(), gateway.clone(), owner_signature, )?; // update the signing nonce associated with this sender so that the future signature would be made on the new value - signing_storage::increment_signing_nonce(deps.storage, owner.clone())?; + signing_storage::increment_signing_nonce(deps.storage, info.sender.clone())?; let gateway_identity = gateway.identity_key.clone(); let bond = GatewayBond::new( pledge.clone(), - owner.clone(), + info.sender.clone(), env.block.height, gateway, - proxy.clone(), ); storage::gateways().save(deps.storage, bond.identity(), &bond)?; Ok(Response::new().add_event(new_gateway_bonding_event( - &owner, - &proxy, + &info.sender, &pledge, &gateway_identity, ))) } -pub fn try_remove_gateway_on_behalf( +pub(crate) fn try_remove_gateway( deps: DepsMut<'_>, info: MessageInfo, - owner: String, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let proxy = info.sender; - let owner = deps.api.addr_validate(&owner)?; - _try_remove_gateway(deps, owner, Some(proxy)) -} - -pub fn try_remove_gateway( - deps: DepsMut<'_>, - info: MessageInfo, -) -> Result { - _try_remove_gateway(deps, info.sender, None) -} - -pub(crate) fn _try_remove_gateway( - deps: DepsMut<'_>, - owner: Addr, - proxy: Option, ) -> Result { // try to find the node of the sender let gateway_bond = match storage::gateways() .idx .owner - .item(deps.storage, owner.clone())? + .item(deps.storage, info.sender.clone())? { Some(record) => record.1, - None => return Err(MixnetContractError::NoAssociatedGatewayBond { owner }), + None => return Err(MixnetContractError::NoAssociatedGatewayBond { owner: info.sender }), }; - if proxy != gateway_bond.proxy { - return Err(MixnetContractError::ProxyMismatch { - existing: gateway_bond - .proxy - .map_or_else(|| "None".to_string(), |a| a.as_str().to_string()), - incoming: proxy.map_or_else(|| "None".to_string(), |a| a.as_str().to_string()), - }); - } - // send bonded funds back to the bond owner let return_tokens = BankMsg::Send { - to_address: proxy.as_ref().unwrap_or(&owner).to_string(), + to_address: info.sender.to_string(), amount: vec![gateway_bond.pledge_amount()], }; // remove the bond storage::gateways().remove(deps.storage, gateway_bond.identity())?; - let mut response = Response::new().add_message(return_tokens); - - if let Some(proxy) = &proxy { - let msg = VestingContractExecuteMsg::TrackUnbondGateway { - owner: owner.as_str().to_string(), - amount: gateway_bond.pledge_amount(), - }; - - let track_unbond_message = wasm_execute(proxy, &msg, vec![])?; - response = response.add_message(track_unbond_message); - } - - Ok(response.add_event(new_gateway_unbonding_event( - &owner, - &proxy, - &gateway_bond.pledge_amount, - gateway_bond.identity(), - ))) + Ok(Response::new() + .add_message(return_tokens) + .add_event(new_gateway_unbonding_event( + &info.sender, + &gateway_bond.pledge_amount, + gateway_bond.identity(), + ))) } pub(crate) fn try_update_gateway_config( @@ -198,36 +109,9 @@ pub(crate) fn try_update_gateway_config( info: MessageInfo, new_config: GatewayConfigUpdate, ) -> Result { - let owner = info.sender; - _try_update_gateway_config(deps, new_config, owner, None) -} + let existing_bond = must_get_gateway_bond_by_owner(deps.storage, &info.sender)?; + let cfg_update_event = new_gateway_config_update_event(&info.sender, &new_config); -pub(crate) fn try_update_gateway_config_on_behalf( - deps: DepsMut, - info: MessageInfo, - new_config: GatewayConfigUpdate, - owner: String, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let owner = deps.api.addr_validate(&owner)?; - let proxy = info.sender; - _try_update_gateway_config(deps, new_config, owner, Some(proxy)) -} - -pub(crate) fn _try_update_gateway_config( - deps: DepsMut, - new_config: GatewayConfigUpdate, - owner: Addr, - proxy: Option, -) -> Result { - let existing_bond = must_get_gateway_bond_by_owner(deps.storage, &owner)?; - ensure_proxy_match(&proxy, &existing_bond.proxy)?; - - let cfg_update_event = new_gateway_config_update_event(&owner, &proxy, &new_config); - - // clippy beta 1.70.0-beta.1 false positive - #[allow(clippy::redundant_clone)] let mut updated_bond = existing_bond.clone(); updated_bond.gateway.host = new_config.host; updated_bond.gateway.mix_port = new_config.mix_port; @@ -254,10 +138,10 @@ pub mod tests { use crate::mixnet_contract_settings::storage::minimum_gateway_pledge; use crate::support::tests; use crate::support::tests::fixtures; - use crate::support::tests::fixtures::{good_gateway_pledge, good_mixnode_pledge}; + use crate::support::tests::fixtures::good_mixnode_pledge; use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::testing::mock_info; - use cosmwasm_std::Uint128; + use cosmwasm_std::{Addr, Uint128}; use mixnet_contract_common::ExecuteMsg; #[test] @@ -392,42 +276,12 @@ pub mod tests { .unwrap(); assert_eq!(1, updated_nonce); - _try_remove_gateway(test.deps_mut(), Addr::unchecked(sender), None).unwrap(); + try_remove_gateway(test.deps_mut(), info.clone()).unwrap(); let res = try_add_gateway(test.deps_mut(), env, info, gateway, signature); assert_eq!(res, Err(MixnetContractError::InvalidEd25519Signature)); } - #[test] - fn gateway_add_with_illegal_proxy() { - let mut test = TestSetup::new(); - let env = test.env(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let owner = "alice"; - let (gateway, sig) = test.gateway_with_signature(owner, None); - - let res = try_add_gateway_on_behalf( - test.deps_mut(), - env, - mock_info(illegal_proxy.as_ref(), &good_gateway_pledge()), - gateway, - owner.to_string(), - sig, - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract - } - ) - } - #[test] fn gateway_remove() { let mut test = TestSetup::new(); @@ -495,7 +349,6 @@ pub mod tests { .add_message(expected_message) .add_event(new_gateway_unbonding_event( &Addr::unchecked("fred"), - &None, &tests::fixtures::good_gateway_pledge()[0], &fred_identity, )); @@ -510,33 +363,6 @@ pub mod tests { assert_eq!(&Addr::unchecked("bob"), nodes[0].owner()); } - #[test] - fn gateway_remove_with_illegal_proxy() { - let mut test = TestSetup::new(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let owner = "alice"; - - test.add_dummy_gateway_with_illegal_proxy(owner, None, illegal_proxy.clone()); - - let res = try_remove_gateway_on_behalf( - test.deps_mut(), - mock_info(illegal_proxy.as_ref(), &good_gateway_pledge()), - owner.to_string(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract - } - ) - } - #[test] fn update_gateway_config() { let mut test = TestSetup::new(); @@ -561,22 +387,6 @@ pub mod tests { ); test.add_dummy_gateway(owner, None); - let vesting_contract = test.vesting_contract(); - - // attempted to remove on behalf with invalid proxy (current is `None`) - let res = try_update_gateway_config_on_behalf( - test.deps_mut(), - mock_info(vesting_contract.as_ref(), &[]), - update.clone(), - owner.to_string(), - ); - assert_eq!( - res, - Err(MixnetContractError::ProxyMismatch { - existing: "None".to_string(), - incoming: vesting_contract.into_string() - }) - ); // "normal" update succeeds let res = try_update_gateway_config(test.deps_mut(), info, update.clone()); @@ -591,39 +401,4 @@ pub mod tests { assert_eq!(bond.gateway.location, update.location); assert_eq!(bond.gateway.version, update.version); } - - #[test] - fn updating_gateway_config_with_illegal_proxy() { - let mut test = TestSetup::new(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let owner = "alice"; - - test.add_dummy_gateway_with_illegal_proxy(owner, None, illegal_proxy.clone()); - let update = GatewayConfigUpdate { - host: "1.1.1.1:1234".to_string(), - mix_port: 1234, - clients_port: 1235, - location: "at home".to_string(), - version: "v1.2.3".to_string(), - }; - - let res = try_update_gateway_config_on_behalf( - test.deps_mut(), - mock_info(illegal_proxy.as_ref(), &[]), - update, - owner.to_string(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract - } - ) - } } diff --git a/contracts/mixnet/src/interval/pending_events.rs b/contracts/mixnet/src/interval/pending_events.rs index e0c5077a8f..fa44e36ec3 100644 --- a/contracts/mixnet/src/interval/pending_events.rs +++ b/contracts/mixnet/src/interval/pending_events.rs @@ -24,7 +24,7 @@ use crate::interval::storage; use crate::mixnodes::helpers::{cleanup_post_unbond_mixnode_storage, get_mixnode_details_by_id}; use crate::mixnodes::storage as mixnodes_storage; use crate::rewards::storage as rewards_storage; -use crate::support::helpers::{send_to_proxy_or_owner, VestingTracking}; +use crate::support::helpers::AttachSendTokens; pub(crate) trait ContractExecutableEvent { // note: the error only means a HARD error like we failed to read from storage. @@ -40,7 +40,6 @@ pub(crate) fn delegate( owner: Addr, mix_id: MixId, amount: Coin, - proxy: Option, ) -> Result { // check if the target node still exists (it might have unbonded between this event getting created // and being executed). Do note that it's absolutely possible for a mixnode to get immediately @@ -56,20 +55,9 @@ pub(crate) fn delegate( _ => { // if mixnode is no longer bonded or in the process of unbonding, return the tokens back to the // delegator; - // (read the notes regarding possible epoch progressiong halting behaviour in `maybe_add_track_undelegation_message`) - let return_tokens = send_to_proxy_or_owner(&proxy, &owner, vec![amount.clone()]); let response = Response::new() - .add_message(return_tokens) - .add_event(new_delegation_on_unbonded_node_event( - &owner, &proxy, mix_id, - )) - .maybe_add_track_vesting_undelegation_message( - deps.storage, - proxy, - owner.to_string(), - mix_id, - amount, - )?; + .send_tokens(&owner, amount.clone()) + .add_event(new_delegation_on_unbonded_node_event(&owner, mix_id)); return Ok(response); } @@ -84,7 +72,7 @@ pub(crate) fn delegate( // if there's an existing delegation, then withdraw the full reward and create a new delegation // with the sum of both - let storage_key = Delegation::generate_storage_key(mix_id, &owner, proxy.as_ref()); + let storage_key = Delegation::generate_storage_key(mix_id, &owner, None); let old_delegation = if let Some(existing_delegation) = delegations_storage::delegations().may_load(deps.storage, storage_key.clone())? { @@ -106,7 +94,6 @@ pub(crate) fn delegate( let cosmos_event = new_delegation_event( created_at, &owner, - &proxy, &new_delegation_amount, mix_id, mix_rewarding.total_unit_reward, @@ -118,7 +105,6 @@ pub(crate) fn delegate( mix_rewarding.total_unit_reward, stored_delegation_amount, env.block.height, - proxy, ); // save on reading since `.save()` would have attempted to read old data that we already have on hand @@ -138,11 +124,10 @@ pub(crate) fn undelegate( created_at: BlockHeight, owner: Addr, mix_id: MixId, - proxy: Option, ) -> Result { // see if the delegation still exists (in case of impatient user who decided to send multiple // undelegation requests in an epoch) - let storage_key = Delegation::generate_storage_key(mix_id, &owner, proxy.as_ref()); + let storage_key = Delegation::generate_storage_key(mix_id, &owner, None); let delegation = match delegations_storage::delegations().may_load(deps.storage, storage_key)? { None => return Ok(Response::default()), Some(delegation) => delegation, @@ -155,18 +140,9 @@ pub(crate) fn undelegate( let tokens_to_return = delegations::helpers::undelegate(deps.storage, delegation, mix_rewarding)?; - // (read the notes regarding possible epoch progressiong halting behaviour in `maybe_add_track_undelegation_message`) - let return_tokens = send_to_proxy_or_owner(&proxy, &owner, vec![tokens_to_return.clone()]); let response = Response::new() - .add_message(return_tokens) - .add_event(new_undelegation_event(created_at, &owner, &proxy, mix_id)) - .maybe_add_track_vesting_undelegation_message( - deps.storage, - proxy, - owner.to_string(), - mix_id, - tokens_to_return, - )?; + .send_tokens(&owner, tokens_to_return.clone()) + .add_event(new_undelegation_event(created_at, &owner, mix_id)); Ok(response) } @@ -197,25 +173,15 @@ pub(crate) fn unbond_mixnode( .rewarding_details .operator_pledge_with_reward(rewarding_denom); - let proxy = &node_details.bond_information.proxy; let owner = &node_details.bond_information.owner; - // send bonded funds (alongside all earned rewards) to the bond owner - let return_tokens = send_to_proxy_or_owner(proxy, owner, vec![tokens.clone()]); - // remove the bond and if there are no delegations left, also the rewarding information // decrement the associated layer count cleanup_post_unbond_mixnode_storage(deps.storage, env, &node_details)?; let response = Response::new() - .add_message(return_tokens) - .add_event(new_mixnode_unbonding_event(created_at, mix_id)) - .maybe_add_track_vesting_unbond_mixnode_message( - deps.storage, - proxy.clone(), - owner.clone().into_string(), - tokens, - )?; + .send_tokens(owner, tokens.clone()) + .add_event(new_mixnode_unbonding_event(created_at, mix_id)); Ok(response) } @@ -311,12 +277,8 @@ pub(crate) fn decrease_pledge( updated_bond.original_pledge.amount -= decrease_by.amount; updated_rewarding.decrease_operator_uint128(decrease_by.amount)?; - let proxy = &mix_details.bond_information.proxy; let owner = &mix_details.bond_information.owner; - // send the removed tokens back to the operator - let return_tokens = send_to_proxy_or_owner(proxy, owner, vec![decrease_by.clone()]); - // update all: bond information, rewarding details and pending pledge changes mixnodes_storage::mixnode_bonds().replace( deps.storage, @@ -328,14 +290,8 @@ pub(crate) fn decrease_pledge( mixnodes_storage::PENDING_MIXNODE_CHANGES.save(deps.storage, mix_id, &pending_changes)?; let response = Response::new() - .add_message(return_tokens) - .add_event(new_pledge_decrease_event(created_at, mix_id, &decrease_by)) - .maybe_add_track_vesting_decrease_mixnode_pledge( - deps.storage, - proxy.clone(), - owner.clone().to_string(), - decrease_by, - )?; + .send_tokens(owner, decrease_by.clone()) + .add_event(new_pledge_decrease_event(created_at, mix_id, &decrease_by)); Ok(response) } @@ -349,13 +305,11 @@ impl ContractExecutableEvent for PendingEpochEventData { owner, mix_id, amount, - proxy, - } => delegate(deps, env, self.created_at, owner, mix_id, amount, proxy), - PendingEpochEventKind::Undelegate { - owner, - mix_id, - proxy, - } => undelegate(deps, self.created_at, owner, mix_id, proxy), + .. + } => delegate(deps, env, self.created_at, owner, mix_id, amount), + PendingEpochEventKind::Undelegate { owner, mix_id, .. } => { + undelegate(deps, self.created_at, owner, mix_id) + } PendingEpochEventKind::PledgeMore { mix_id, amount } => { increase_pledge(deps, self.created_at, mix_id, amount) } @@ -472,33 +426,25 @@ impl ContractExecutableEvent for PendingIntervalEventData { #[cfg(test)] mod tests { - use std::time::Duration; - - use cosmwasm_std::Decimal; - - use mixnet_contract_common::Percent; - use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; - + use super::*; use crate::support::tests::test_helpers; use crate::support::tests::test_helpers::{assert_decimals, TestSetup}; - - use super::*; + use cosmwasm_std::Decimal; + use mixnet_contract_common::Percent; + use std::time::Duration; // note that authorization and basic validation has already been performed for all of those // before being pushed onto the event queues #[cfg(test)] mod delegating { - use cosmwasm_std::testing::mock_info; - use cosmwasm_std::{coin, to_binary, CosmosMsg, WasmMsg}; - - use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; - + use super::*; use crate::mixnodes::transactions::try_remove_mixnode; use crate::support::tests::fixtures::TEST_COIN_DENOM; use crate::support::tests::test_helpers::get_bank_send_msg; - - use super::*; + use cosmwasm_std::coin; + use cosmwasm_std::testing::mock_info; + use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; #[test] fn returns_the_tokens_if_mixnode_has_unbonded() { @@ -523,7 +469,6 @@ mod tests { Addr::unchecked(owner1), mix_id, delegation_coin.clone(), - None, ) .unwrap(); @@ -549,7 +494,6 @@ mod tests { Addr::unchecked(owner2), mix_id, delegation_coin.clone(), - None, ) .unwrap(); let storage_key = @@ -588,7 +532,6 @@ mod tests { Addr::unchecked(owner1), mix_id, delegation_coin.clone(), - None, ) .unwrap(); @@ -614,7 +557,6 @@ mod tests { Addr::unchecked(owner2), mix_id, delegation_coin.clone(), - None, ) .unwrap(); let storage_key = @@ -650,7 +592,6 @@ mod tests { Addr::unchecked(owner), mix_id, delegation_coin_new, - None, ) .unwrap(); @@ -725,7 +666,6 @@ mod tests { Addr::unchecked(owner), mix_id, delegation_coin_new, - None, ) .unwrap(); @@ -797,7 +737,6 @@ mod tests { Addr::unchecked(owner), mix_id, delegation_coin.clone(), - None, ) .unwrap(); assert!(get_bank_send_msg(&res).is_none()); @@ -816,117 +755,13 @@ mod tests { Decimal::from_atomics(delegation, 0).unwrap() ) } - - #[test] - fn attaches_vesting_contract_track_message_if_tokens_are_returned() { - let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - - let delegation = 120_000_000u128; - let delegation_coin = coin(delegation, TEST_COIN_DENOM); - let owner = "delegator"; - - let env = test.env(); - unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap(); - - let vesting_contract = test.vesting_contract(); - - // for a fresh delegation, nothing was added to the storage either - let res_vesting = delegate( - test.deps_mut(), - &env, - 123, - Addr::unchecked(owner), - mix_id, - delegation_coin.clone(), - Some(vesting_contract.clone()), - ) - .unwrap(); - let storage_key = Delegation::generate_storage_key( - mix_id, - &Addr::unchecked(owner), - Some(vesting_contract.clone()).as_ref(), - ); - assert!(delegations_storage::delegations() - .may_load(test.deps().storage, storage_key) - .unwrap() - .is_none()); - // and all tokens are returned back to the proxy - let (receiver, sent_amount) = get_bank_send_msg(&res_vesting).unwrap(); - assert_eq!(receiver, vesting_contract.as_str()); - assert_eq!(sent_amount[0], delegation_coin); - - // and we get appropriate track message - let mut found_track = true; - for msg in &res_vesting.messages { - if let CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr, - msg, - funds, - }) = &msg.msg - { - found_track = true; - assert_eq!(contract_addr, vesting_contract.as_str()); - let expected_msg = to_binary(&VestingContractExecuteMsg::TrackUndelegation { - owner: owner.to_string(), - mix_id, - amount: delegation_coin.clone(), - }) - .unwrap(); - assert_eq!(&expected_msg, msg); - assert!(funds.is_empty()) - } - } - assert!(found_track); - } - - #[test] - fn returns_error_for_illegal_proxy() { - let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - - let delegation = 120_000_000u128; - let delegation_coin = coin(delegation, TEST_COIN_DENOM); - let owner = "delegator"; - let dummy_proxy = Addr::unchecked("not-vesting-contract"); - - let env = test.env(); - unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap(); - - let vesting_contract = test.vesting_contract(); - - // try to add illegal delegation (with invalid proxy) - let res_other_proxy = delegate( - test.deps_mut(), - &env, - 123, - Addr::unchecked(owner), - mix_id, - delegation_coin, - Some(dummy_proxy.clone()), - ) - .unwrap_err(); - - assert_eq!( - res_other_proxy, - MixnetContractError::ProxyIsNotVestingContract { - received: dummy_proxy, - vesting_contract, - } - ); - } } #[cfg(test)] mod undelegating { - use cosmwasm_std::{coin, to_binary, CosmosMsg, WasmMsg}; - - use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; - - use crate::support::tests::fixtures::TEST_COIN_DENOM; - use crate::support::tests::test_helpers::get_bank_send_msg; - use super::*; + use crate::support::tests::test_helpers::get_bank_send_msg; + use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; #[test] fn doesnt_return_any_tokens_if_it_doesnt_exist() { @@ -935,7 +770,7 @@ mod tests { let owner = Addr::unchecked("delegator"); - let res = undelegate(test.deps_mut(), 123, owner, mix_id, None).unwrap(); + let res = undelegate(test.deps_mut(), 123, owner, mix_id).unwrap(); assert!(get_bank_send_msg(&res).is_none()); } @@ -950,7 +785,7 @@ mod tests { // this should never happen in actual code, but if we manually messed something up, // lets make sure this throws an error rewards_storage::MIXNODE_REWARDING.remove(test.deps_mut().storage, mix_id); - let res = undelegate(test.deps_mut(), 123, owner, mix_id, None); + let res = undelegate(test.deps_mut(), 123, owner, mix_id); assert!(matches!( res, Err(MixnetContractError::InconsistentState { .. }) @@ -996,8 +831,7 @@ mod tests { let expected_return = delegation + truncated_reward.u128(); - let res = - undelegate(test.deps_mut(), 123, Addr::unchecked(owner), mix_id, None).unwrap(); + let res = undelegate(test.deps_mut(), 123, Addr::unchecked(owner), mix_id).unwrap(); let (receiver, sent_amount) = get_bank_send_msg(&res).unwrap(); assert_eq!(receiver, owner); assert_eq!(sent_amount[0].amount.u128(), expected_return); @@ -1015,117 +849,19 @@ mod tests { assert!(rewarding.delegates.is_zero()); assert_eq!(rewarding.unique_delegations, 0); } - - #[test] - fn attaches_vesting_contract_track_message_if_tokens_are_returned() { - let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - - let delegation = 120_000_000u128; - let delegation_coin = coin(delegation, TEST_COIN_DENOM); - let owner = "delegator"; - - let vesting_contract = test.vesting_contract(); - - test.add_immediate_delegation_with_legal_proxy(owner, delegation, mix_id); - - let res_vesting = undelegate( - test.deps_mut(), - 123, - Addr::unchecked(owner), - mix_id, - Some(vesting_contract.clone()), - ) - .unwrap(); - let storage_key = Delegation::generate_storage_key( - mix_id, - &Addr::unchecked(owner), - Some(vesting_contract.clone()).as_ref(), - ); - assert!(delegations_storage::delegations() - .may_load(test.deps().storage, storage_key) - .unwrap() - .is_none()); - - // and all tokens are returned back to the proxy - let (receiver, sent_amount) = get_bank_send_msg(&res_vesting).unwrap(); - assert_eq!(receiver, vesting_contract.as_str()); - assert_eq!(sent_amount[0], delegation_coin); - - // and we get appropriate track message - let mut found_track = true; - for msg in &res_vesting.messages { - if let CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr, - msg, - funds, - }) = &msg.msg - { - found_track = true; - assert_eq!(contract_addr, vesting_contract.as_str()); - let expected_msg = to_binary(&VestingContractExecuteMsg::TrackUndelegation { - owner: owner.to_string(), - mix_id, - amount: delegation_coin.clone(), - }) - .unwrap(); - assert_eq!(&expected_msg, msg); - assert!(funds.is_empty()) - } - } - assert!(found_track); - } - - #[test] - fn returns_error_for_illegal_proxy() { - let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - - let delegation = 120_000_000u128; - let owner = "delegator1"; - - let vesting_contract = test.vesting_contract(); - let dummy_proxy = Addr::unchecked("not-vesting-contract"); - - test.add_immediate_delegation_with_illegal_proxy( - owner, - delegation, - mix_id, - dummy_proxy.clone(), - ); - - let res_other_proxy = undelegate( - test.deps_mut(), - 123, - Addr::unchecked(owner), - mix_id, - Some(dummy_proxy.clone()), - ) - .unwrap_err(); - assert_eq!( - res_other_proxy, - MixnetContractError::ProxyIsNotVestingContract { - received: dummy_proxy, - vesting_contract, - } - ); - } } #[cfg(test)] mod mixnode_unbonding { - use cosmwasm_std::{coin, to_binary, CosmosMsg, Uint128, WasmMsg}; - + use super::*; + use crate::mixnodes::storage as mixnodes_storage; + use crate::mixnodes::transactions::{try_decrease_pledge, try_increase_pledge}; + use crate::support::tests::test_helpers::get_bank_send_msg; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::Uint128; use mixnet_contract_common::mixnode::{PendingMixNodeChanges, UnbondedMixnode}; use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; - use crate::mixnodes::storage as mixnodes_storage; - use crate::mixnodes::transactions::{_try_decrease_pledge, _try_increase_pledge}; - use crate::support::tests::fixtures::TEST_COIN_DENOM; - use crate::support::tests::test_helpers::get_bank_send_msg; - - use super::*; - #[test] fn returns_hard_error_if_mixnode_doesnt_exist() { // this should have never happened so hard error MUST be thrown here @@ -1150,12 +886,10 @@ mod tests { let pledge = Uint128::new(250_000_000); let mix_id = test.add_dummy_mixnode(owner, Some(pledge)); - _try_increase_pledge( + try_increase_pledge( test.deps_mut(), env.clone(), - change.clone(), - Addr::unchecked(owner), - None, + mock_info(owner, &change.clone()), ) .unwrap(); @@ -1170,12 +904,11 @@ mod tests { let pledge = Uint128::new(250_000_000); let mix_id = test.add_dummy_mixnode(owner, Some(pledge)); - _try_decrease_pledge( + try_decrease_pledge( test.deps_mut(), env.clone(), + mock_info(owner, &[]), change[0].clone(), - Addr::unchecked(owner), - None, ) .unwrap(); @@ -1263,79 +996,6 @@ mod tests { 0 ) } - - #[test] - fn attaches_vesting_contract_track_message_if_tokens_are_returned() { - let mut test = TestSetup::new(); - - let vesting_contract = test.vesting_contract(); - - let pledge = Uint128::new(250_000_000); - let pledge_coin = coin(250_000_000, TEST_COIN_DENOM); - let owner = "mix-owner1"; - let mix_id_vesting = test.add_dummy_mixnode_with_legal_proxy(owner, Some(pledge)); - - let env = test.env(); - let res = unbond_mixnode(test.deps_mut(), &env, 123, mix_id_vesting).unwrap(); - - assert!(mixnodes_storage::mixnode_bonds() - .may_load(test.deps().storage, mix_id_vesting) - .unwrap() - .is_none()); - - // and all tokens are returned back to the proxy - let (receiver, sent_amount) = get_bank_send_msg(&res).unwrap(); - assert_eq!(receiver, vesting_contract.as_str()); - assert_eq!(sent_amount[0], pledge_coin); - - // and we get appropriate track message - let mut found_track = true; - for msg in &res.messages { - if let CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr, - msg, - funds, - }) = &msg.msg - { - found_track = true; - assert_eq!(contract_addr, vesting_contract.as_str()); - let expected_msg = to_binary(&VestingContractExecuteMsg::TrackUnbondMixnode { - owner: owner.to_string(), - amount: pledge_coin.clone(), - }) - .unwrap(); - assert_eq!(&expected_msg, msg); - assert!(funds.is_empty()) - } - } - assert!(found_track); - } - - #[test] - fn returns_error_for_illegal_proxy() { - let mut test = TestSetup::new(); - - let dummy_proxy = Addr::unchecked("not-vesting-contract"); - let env = test.env(); - - let vesting_contract = test.vesting_contract(); - let owner = "mix-owner"; - let pledge = Uint128::new(250_000_000); - - let mix_id_illegal_proxy = - test.add_dummy_mixnode_with_illegal_proxy(owner, Some(pledge), dummy_proxy.clone()); - - // this is the halting issue that should have never occurred - let res_other_proxy = - unbond_mixnode(test.deps_mut(), &env, 123, mix_id_illegal_proxy).unwrap_err(); - assert_eq!( - res_other_proxy, - MixnetContractError::ProxyIsNotVestingContract { - received: dummy_proxy, - vesting_contract, - } - ); - } } #[cfg(test)] @@ -1615,11 +1275,9 @@ mod tests { #[cfg(test)] mod decreasing_pledge { - use cosmwasm_std::{to_binary, BankMsg, CosmosMsg, Uint128, WasmMsg}; - - use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; - use super::*; + use cosmwasm_std::{BankMsg, CosmosMsg, Uint128}; + use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; #[test] fn returns_hard_error_if_mixnode_doesnt_exist() { @@ -1699,64 +1357,6 @@ mod tests { ) } - #[test] - fn returns_tokens_back_to_the_proxy_if_bonded_with_vesting() { - let mut test = TestSetup::new(); - let owner = "mix-owner"; - let mix_id = test.add_dummy_mixnode_with_legal_proxy(owner, None); - test.set_pending_pledge_change(mix_id, None); - - let vesting_contract = test.vesting_contract(); - - let amount = test.coin(12345); - let res = decrease_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap(); - - assert_eq!(res.messages.len(), 2); - assert_eq!( - res.messages[0].msg, - CosmosMsg::Bank(BankMsg::Send { - to_address: vesting_contract.to_string(), - amount: vec![amount], - }) - ) - } - - #[test] - fn attaches_vesting_track_message() { - let mut test = TestSetup::new(); - let mix_id_no_proxy = test.add_dummy_mixnode("mix-owner1", None); - test.set_pending_pledge_change(mix_id_no_proxy, None); - - let mix_id_proxy = test.add_dummy_mixnode_with_legal_proxy("mix-owner2", None); - test.set_pending_pledge_change(mix_id_proxy, None); - - let vesting_contract = test.vesting_contract(); - - let amount = test.coin(12345); - let res_no_proxy = - decrease_pledge(test.deps_mut(), 123, mix_id_no_proxy, amount.clone()).unwrap(); - - // nothing was attached (apart from bank message tested in `returns_tokens_back_to_the_owner`) - // because it wasn't done with proxy! - assert_eq!(res_no_proxy.messages.len(), 1); - - let res_proxy = - decrease_pledge(test.deps_mut(), 123, mix_id_proxy, amount.clone()).unwrap(); - assert_eq!(res_proxy.messages.len(), 2); - assert_eq!( - res_proxy.messages[1].msg, - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: vesting_contract.to_string(), - msg: to_binary(&VestingContractExecuteMsg::TrackDecreasePledge { - owner: "mix-owner2".to_string(), - amount, - }) - .unwrap(), - funds: vec![], - }) - ); - } - #[test] fn without_any_events_in_between_is_equivalent_to_pledging_the_same_amount_immediately() { let mut test = TestSetup::new(); diff --git a/contracts/mixnet/src/interval/queries.rs b/contracts/mixnet/src/interval/queries.rs index d0b480848f..423d9a90ae 100644 --- a/contracts/mixnet/src/interval/queries.rs +++ b/contracts/mixnet/src/interval/queries.rs @@ -159,11 +159,8 @@ mod tests { } fn push_dummy_epoch_action(test: &mut TestSetup) { - let dummy_action = PendingEpochEventKind::Undelegate { - owner: Addr::unchecked("foomp"), - mix_id: test.rng.next_u32(), - proxy: None, - }; + let dummy_action = + PendingEpochEventKind::new_undelegate(Addr::unchecked("foomp"), test.rng.next_u32()); let env = test.env(); storage::push_new_epoch_event(test.deps_mut().storage, &env, dummy_action).unwrap(); } @@ -571,11 +568,8 @@ mod tests { ); // it exists - let dummy_action = PendingEpochEventKind::Undelegate { - owner: Addr::unchecked("foomp"), - mix_id: test.rng.next_u32(), - proxy: None, - }; + let dummy_action = + PendingEpochEventKind::new_undelegate(Addr::unchecked("foomp"), test.rng.next_u32()); let env = test.env(); storage::push_new_epoch_event(test.deps_mut().storage, &env, dummy_action.clone()).unwrap(); let expected = PendingEpochEventResponse { diff --git a/contracts/mixnet/src/interval/storage.rs b/contracts/mixnet/src/interval/storage.rs index 24666db431..33eddd4d4e 100644 --- a/contracts/mixnet/src/interval/storage.rs +++ b/contracts/mixnet/src/interval/storage.rs @@ -222,11 +222,10 @@ mod tests { let env = test.env(); for _ in 0..500 { - let dummy_action = PendingEpochEventKind::Undelegate { - owner: Addr::unchecked("foomp"), - mix_id: test.rng.next_u32(), - proxy: None, - }; + let dummy_action = PendingEpochEventKind::new_undelegate( + Addr::unchecked("foomp"), + test.rng.next_u32(), + ); let id = push_new_epoch_event(test.deps_mut().storage, &env, dummy_action).unwrap(); let expected = EPOCH_EVENT_ID_COUNTER.load(test.deps().storage).unwrap(); assert_eq!(expected, id); @@ -235,11 +234,10 @@ mod tests { test.execute_all_pending_events(); for _ in 0..10 { - let dummy_action = PendingEpochEventKind::Undelegate { - owner: Addr::unchecked("foomp"), - mix_id: test.rng.next_u32(), - proxy: None, - }; + let dummy_action = PendingEpochEventKind::new_undelegate( + Addr::unchecked("foomp"), + test.rng.next_u32(), + ); let id = push_new_epoch_event(test.deps_mut().storage, &env, dummy_action).unwrap(); let expected = EPOCH_EVENT_ID_COUNTER.load(test.deps().storage).unwrap(); assert_eq!(expected, id); diff --git a/contracts/mixnet/src/interval/transactions.rs b/contracts/mixnet/src/interval/transactions.rs index 0eae4fed7b..adc8faa9cc 100644 --- a/contracts/mixnet/src/interval/transactions.rs +++ b/contracts/mixnet/src/interval/transactions.rs @@ -5,11 +5,12 @@ use super::storage; use crate::interval::helpers::change_interval_config; use crate::interval::pending_events::ContractExecutableEvent; use crate::interval::storage::push_new_interval_event; +use crate::mixnet_contract_settings::storage::ADMIN; use crate::mixnodes::transactions::update_mixnode_layer; use crate::rewards; use crate::rewards::storage as rewards_storage; use crate::support::helpers::{ - ensure_can_advance_epoch, ensure_epoch_in_progress_state, ensure_is_authorized, ensure_is_owner, + ensure_can_advance_epoch, ensure_epoch_in_progress_state, ensure_is_authorized, }; use cosmwasm_std::{DepsMut, Env, MessageInfo, Order, Response, Storage}; use mixnet_contract_common::error::MixnetContractError; @@ -325,7 +326,7 @@ pub(crate) fn try_update_interval_config( epoch_duration_secs: u64, force_immediately: bool, ) -> Result { - ensure_is_owner(info.sender, deps.storage)?; + ADMIN.assert_admin(deps.as_ref(), &info.sender)?; if epochs_in_interval == 0 { return Err(MixnetContractError::EpochsInIntervalZero); @@ -373,18 +374,14 @@ mod tests { use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::Addr; use mixnet_contract_common::pending_events::PendingEpochEventKind; - use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; fn push_n_dummy_epoch_actions(test: &mut TestSetup, n: usize) { // if you attempt to undelegate non-existent delegation, // it will return an empty response, but will not fail let env = test.env(); for i in 0..n { - let dummy_action = PendingEpochEventKind::Undelegate { - owner: Addr::unchecked("foomp"), - mix_id: i as MixId, - proxy: None, - }; + let dummy_action = + PendingEpochEventKind::new_undelegate(Addr::unchecked("foomp"), i as MixId); storage::push_new_epoch_event(test.deps_mut().storage, &env, dummy_action).unwrap(); } } @@ -406,7 +403,7 @@ mod tests { mod performing_pending_epoch_actions { use super::*; use crate::support::tests::fixtures::TEST_COIN_DENOM; - use cosmwasm_std::{coin, coins, wasm_execute, BankMsg, Empty, SubMsg}; + use cosmwasm_std::{coin, coins, BankMsg, Empty, SubMsg}; use mixnet_contract_common::events::{ new_active_set_update_event, new_delegation_on_unbonded_node_event, new_undelegation_event, @@ -495,7 +492,6 @@ mod tests { #[test] fn catches_all_events_and_messages_from_executed_actions() { let mut test = TestSetup::new(); - let vesting_contract = test.vesting_contract(); let env = test.env(); let legit_mix = test.add_dummy_mixnode("mix-owner", None); @@ -509,17 +505,15 @@ mod tests { // delegate to node that doesn't exist, // we expect to receive BankMsg with tokens being returned, // and event regarding delegation - let non_existent_delegation = PendingEpochEventKind::Delegate { - owner: Addr::unchecked("foomp"), - mix_id: 123, - amount: coin(123, TEST_COIN_DENOM), - proxy: None, - }; + let non_existent_delegation = PendingEpochEventKind::new_delegate( + Addr::unchecked("foomp"), + 123, + coin(123, TEST_COIN_DENOM), + ); storage::push_new_epoch_event(test.deps_mut().storage, &env, non_existent_delegation) .unwrap(); expected_events.push(new_delegation_on_unbonded_node_event( &Addr::unchecked("foomp"), - &None, 123, )); expected_messages.push(SubMsg::new(BankMsg::Send { @@ -527,33 +521,6 @@ mod tests { amount: coins(123, TEST_COIN_DENOM), })); - // delegation to node that doesn't exist with vesting contract - // we expect the same as above PLUS TrackUndelegation message - let non_existent_delegation = PendingEpochEventKind::Delegate { - owner: Addr::unchecked("foomp2"), - mix_id: 123, - amount: coin(123, TEST_COIN_DENOM), - proxy: Some(vesting_contract.clone()), - }; - storage::push_new_epoch_event(test.deps_mut().storage, &env, non_existent_delegation) - .unwrap(); - expected_events.push(new_delegation_on_unbonded_node_event( - &Addr::unchecked("foomp2"), - &Some(vesting_contract.clone()), - 123, - )); - expected_messages.push(SubMsg::new(BankMsg::Send { - to_address: vesting_contract.clone().into_string(), - amount: coins(123, TEST_COIN_DENOM), - })); - let msg = VestingContractExecuteMsg::TrackUndelegation { - owner: "foomp2".to_string(), - mix_id: 123, - amount: coin(123, TEST_COIN_DENOM), - }; - let track_undelegate_message = wasm_execute(vesting_contract, &msg, vec![]).unwrap(); - expected_messages.push(SubMsg::new(track_undelegate_message)); - // updating active set should only emit events and no cosmos messages let action_with_event = PendingEpochEventKind::UpdateActiveSetSize { new_size: 50 }; storage::push_new_epoch_event(test.deps_mut().storage, &env, action_with_event) @@ -561,16 +528,12 @@ mod tests { expected_events.push(new_active_set_update_event(env.block.height, 50)); // undelegation just returns tokens and emits event - let legit_undelegate = PendingEpochEventKind::Undelegate { - owner: delegator.clone(), - mix_id: legit_mix, - proxy: None, - }; + let legit_undelegate = + PendingEpochEventKind::new_undelegate(delegator.clone(), legit_mix); storage::push_new_epoch_event(test.deps_mut().storage, &env, legit_undelegate).unwrap(); expected_events.push(new_undelegation_event( env.block.height, &delegator, - &None, legit_mix, )); expected_messages.push(SubMsg::new(BankMsg::Send { @@ -583,9 +546,9 @@ mod tests { let mut expected = Response::new().add_events(expected_events); expected.messages = expected_messages; assert_eq!(res, expected); - assert_eq!(executed, 4); + assert_eq!(executed, 3); assert_eq!( - 4, + 3, storage::LAST_PROCESSED_EPOCH_EVENT .load(test.deps().storage) .unwrap() @@ -1330,17 +1293,15 @@ mod tests { let mut expected_messages: Vec> = Vec::new(); // epoch event - let non_existent_delegation = PendingEpochEventKind::Delegate { - owner: Addr::unchecked("foomp"), - mix_id: 123, - amount: coin(123, TEST_COIN_DENOM), - proxy: None, - }; + let non_existent_delegation = PendingEpochEventKind::new_delegate( + Addr::unchecked("foomp"), + 123, + coin(123, TEST_COIN_DENOM), + ); storage::push_new_epoch_event(test.deps_mut().storage, &env, non_existent_delegation) .unwrap(); expected_events.push(new_delegation_on_unbonded_node_event( &Addr::unchecked("foomp"), - &None, 123, )); expected_messages.push(SubMsg::new(BankMsg::Send { @@ -1806,6 +1767,7 @@ mod tests { use super::*; use cosmwasm_std::testing::mock_info; use cosmwasm_std::Decimal; + use cw_controllers::AdminError::NotAdmin; use std::time::Duration; #[test] @@ -1871,11 +1833,11 @@ mod tests { 1000, false, ); - assert_eq!(res, Err(MixnetContractError::Unauthorized)); + assert_eq!(res, Err(MixnetContractError::Admin(NotAdmin {}))); let res = try_update_interval_config(test.deps_mut(), env.clone(), random, 100, 1000, false); - assert_eq!(res, Err(MixnetContractError::Unauthorized)); + assert_eq!(res, Err(MixnetContractError::Admin(NotAdmin {}))); let res = try_update_interval_config(test.deps_mut(), env, owner, 100, 1000, false); assert!(res.is_ok()) diff --git a/contracts/mixnet/src/lib.rs b/contracts/mixnet/src/lib.rs index 80e6822f89..985cffda57 100644 --- a/contracts/mixnet/src/lib.rs +++ b/contracts/mixnet/src/lib.rs @@ -19,3 +19,4 @@ mod support; #[cfg(feature = "contract-testing")] mod testing; +mod vesting_migration; diff --git a/contracts/mixnet/src/mixnet_contract_settings/queries.rs b/contracts/mixnet/src/mixnet_contract_settings/queries.rs index 1fbedf4d62..5d80fec78a 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/queries.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/queries.rs @@ -2,10 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 use super::storage; +use crate::mixnet_contract_settings::storage::ADMIN; use cosmwasm_std::{Deps, StdResult}; +use cw_controllers::AdminResponse; use mixnet_contract_common::{ContractBuildInformation, ContractState, ContractStateParams}; use nym_contracts_common::get_build_information; +pub(crate) fn query_admin(deps: Deps<'_>) -> StdResult { + ADMIN.query_admin(deps) +} + pub(crate) fn query_contract_state(deps: Deps<'_>) -> StdResult { storage::CONTRACT_STATE.load(deps.storage) } @@ -36,8 +42,9 @@ pub(crate) mod tests { fn query_for_contract_settings_works() { let mut deps = test_helpers::init_contract(); + #[allow(deprecated)] let dummy_state = ContractState { - owner: Addr::unchecked("someowner"), + owner: Some(Addr::unchecked("foomp")), rewarding_validator_address: Addr::unchecked("monitor"), vesting_contract_address: Addr::unchecked("foomp"), rewarding_denom: "unym".to_string(), @@ -45,6 +52,8 @@ pub(crate) mod tests { minimum_mixnode_delegation: None, minimum_mixnode_pledge: coin(123u128, "unym"), minimum_gateway_pledge: coin(456u128, "unym"), + profit_margin: Default::default(), + interval_operating_cost: Default::default(), }, }; diff --git a/contracts/mixnet/src/mixnet_contract_settings/storage.rs b/contracts/mixnet/src/mixnet_contract_settings/storage.rs index 4bb85c1025..0c8502089e 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/storage.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/storage.rs @@ -1,14 +1,16 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::constants::CONTRACT_STATE_KEY; -use cosmwasm_std::{Addr, Storage}; +use crate::constants::{ADMIN_STORAGE_KEY, CONTRACT_STATE_KEY}; +use cosmwasm_std::{Addr, DepsMut, Storage}; use cosmwasm_std::{Coin, StdResult}; +use cw_controllers::Admin; use cw_storage_plus::Item; use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::ContractState; +use mixnet_contract_common::{ContractState, OperatingCostRange, ProfitMarginRange}; pub(crate) const CONTRACT_STATE: Item<'_, ContractState> = Item::new(CONTRACT_STATE_KEY); +pub(crate) const ADMIN: Admin = Admin::new(ADMIN_STORAGE_KEY); pub fn rewarding_validator_address(storage: &dyn Storage) -> Result { Ok(CONTRACT_STATE @@ -28,6 +30,22 @@ pub(crate) fn minimum_gateway_pledge(storage: &dyn Storage) -> Result Result { + Ok(CONTRACT_STATE + .load(storage) + .map(|state| state.params.profit_margin)?) +} + +pub(crate) fn interval_oprating_cost_range( + storage: &dyn Storage, +) -> Result { + Ok(CONTRACT_STATE + .load(storage) + .map(|state| state.params.interval_operating_cost)?) +} + #[allow(unused)] pub(crate) fn minimum_delegation_stake( storage: &dyn Storage, @@ -50,8 +68,10 @@ pub(crate) fn vesting_contract_address(storage: &dyn Storage) -> Result, initial_state: ContractState, + initial_admin: Addr, ) -> StdResult<()> { - CONTRACT_STATE.save(storage, &initial_state) + CONTRACT_STATE.save(deps.storage, &initial_state)?; + ADMIN.set(deps, Some(initial_admin)) } diff --git a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs index 5aed1d45ea..b458f6fbbc 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs @@ -2,15 +2,36 @@ // SPDX-License-Identifier: Apache-2.0 use super::storage; -use cosmwasm_std::DepsMut; +use crate::mixnet_contract_settings::storage::ADMIN; use cosmwasm_std::MessageInfo; use cosmwasm_std::Response; +use cosmwasm_std::{DepsMut, StdResult}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ new_rewarding_validator_address_update_event, new_settings_update_event, }; use mixnet_contract_common::ContractStateParams; +pub fn try_update_contract_admin( + mut deps: DepsMut<'_>, + info: MessageInfo, + new_admin: String, +) -> Result { + let new_admin = deps.api.addr_validate(&new_admin)?; + + let res = ADMIN.execute_update_admin(deps.branch(), info, Some(new_admin.clone()))?; + + // SAFETY: we don't need to perform any authentication checks on the sender as it was performed + // during 'execute_update_admin' call + #[allow(deprecated)] + storage::CONTRACT_STATE.update(deps.storage, |mut state| -> StdResult<_> { + state.owner = Some(new_admin); + Ok(state) + })?; + + Ok(res) +} + pub fn try_update_rewarding_validator_address( deps: DepsMut<'_>, info: MessageInfo, @@ -18,9 +39,7 @@ pub fn try_update_rewarding_validator_address( ) -> Result { let mut state = storage::CONTRACT_STATE.load(deps.storage)?; - if info.sender != state.owner { - return Err(MixnetContractError::Unauthorized); - } + ADMIN.assert_admin(deps.as_ref(), &info.sender)?; let new_address = deps.api.addr_validate(&address)?; let old_address = state.rewarding_validator_address; @@ -43,10 +62,7 @@ pub(crate) fn try_update_contract_settings( ) -> Result { let mut state = storage::CONTRACT_STATE.load(deps.storage)?; - // check if this is executed by the owner, if not reject the transaction - if info.sender != state.owner { - return Err(MixnetContractError::Unauthorized); - } + ADMIN.assert_admin(deps.as_ref(), &info.sender)?; let response = Response::new().add_event(new_settings_update_event(&state.params, ¶ms)); @@ -65,6 +81,7 @@ pub mod tests { use crate::support::tests::test_helpers; use cosmwasm_std::testing::mock_info; use cosmwasm_std::{Addr, Coin, Uint128}; + use cw_controllers::AdminError::NotAdmin; #[test] fn update_contract_rewarding_validtor_address() { @@ -76,7 +93,7 @@ pub mod tests { info, "not-the-creator".to_string(), ); - assert_eq!(res, Err(MixnetContractError::Unauthorized)); + assert_eq!(res, Err(MixnetContractError::Admin(NotAdmin {}))); let info = mock_info("creator", &[]); let res = try_update_rewarding_validator_address( @@ -121,6 +138,8 @@ pub mod tests { denom, amount: INITIAL_GATEWAY_PLEDGE_AMOUNT + Uint128::new(1234), }, + profit_margin: Default::default(), + interval_operating_cost: Default::default(), }; let initial_params = storage::CONTRACT_STATE @@ -134,7 +153,7 @@ pub mod tests { // cannot be updated from non-owner account let info = mock_info("not-the-creator", &[]); let res = try_update_contract_settings(deps.as_mut(), info, new_params.clone()); - assert_eq!(res, Err(MixnetContractError::Unauthorized)); + assert_eq!(res, Err(MixnetContractError::Admin(NotAdmin {}))); // but works fine from the creator account let info = mock_info("creator", &[]); diff --git a/contracts/mixnet/src/mixnodes/helpers.rs b/contracts/mixnet/src/mixnodes/helpers.rs index 2650283b05..b8dd15ae6e 100644 --- a/contracts/mixnet/src/mixnodes/helpers.rs +++ b/contracts/mixnet/src/mixnodes/helpers.rs @@ -97,7 +97,6 @@ pub(crate) fn save_new_mixnode( mixnode: MixNode, cost_params: MixNodeCostParams, owner: Addr, - proxy: Option, pledge: Coin, ) -> Result<(MixId, Layer), MixnetContractError> { let layer = assign_layer(storage)?; @@ -105,15 +104,7 @@ pub(crate) fn save_new_mixnode( let current_epoch = interval_storage::current_interval(storage)?.current_epoch_absolute_id(); let mixnode_rewarding = MixNodeRewarding::initialise_new(cost_params, &pledge, current_epoch)?; - let mixnode_bond = MixNodeBond::new( - mix_id, - owner, - pledge, - layer, - mixnode, - proxy, - env.block.height, - ); + let mixnode_bond = MixNodeBond::new(mix_id, owner, pledge, layer, mixnode, env.block.height); // save mixnode bond data // note that this implicitly checks for uniqueness on identity key, sphinx key and owner @@ -411,7 +402,6 @@ pub(crate) mod tests { mixnode, cost_params.clone(), owner.clone(), - None, pledge.clone(), ) .unwrap(); @@ -444,7 +434,6 @@ pub(crate) mod tests { mixnode, cost_params.clone(), Addr::unchecked("different-owner"), - None, pledge.clone(), ); assert!(res.is_err()); @@ -457,7 +446,6 @@ pub(crate) mod tests { mixnode, cost_params.clone(), owner, - None, pledge.clone(), ); assert!(res.is_err()); @@ -471,7 +459,6 @@ pub(crate) mod tests { mixnode, cost_params, Addr::unchecked("different-owner"), - None, pledge, ); assert!(res.is_err()); diff --git a/contracts/mixnet/src/mixnodes/signature_helpers.rs b/contracts/mixnet/src/mixnodes/signature_helpers.rs index 2ccec88d81..04942aaff5 100644 --- a/contracts/mixnet/src/mixnodes/signature_helpers.rs +++ b/contracts/mixnet/src/mixnodes/signature_helpers.rs @@ -5,14 +5,16 @@ use crate::signing::storage as signing_storage; use crate::support::helpers::decode_ed25519_identity_key; use cosmwasm_std::{Addr, Coin, Deps}; use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::{construct_mixnode_bonding_sign_payload, MixNode, MixNodeCostParams}; +use mixnet_contract_common::{ + construct_legacy_mixnode_bonding_sign_payload, construct_mixnode_bonding_sign_payload, MixNode, + MixNodeCostParams, +}; use nym_contracts_common::signing::MessageSignature; use nym_contracts_common::signing::Verifier; pub(crate) fn verify_mixnode_bonding_signature( deps: Deps<'_>, sender: Addr, - proxy: Option, pledge: Coin, mixnode: MixNode, cost_params: MixNodeCostParams, @@ -21,14 +23,37 @@ pub(crate) fn verify_mixnode_bonding_signature( // recover the public key let public_key = decode_ed25519_identity_key(&mixnode.identity_key)?; - // reconstruct the payload + // reconstruct the payload, first try the current format, then attempt legacy let nonce = signing_storage::get_signing_nonce(deps.storage, sender.clone())?; - let msg = - construct_mixnode_bonding_sign_payload(nonce, sender, proxy, pledge, mixnode, cost_params); + let msg = construct_mixnode_bonding_sign_payload( + nonce, + sender.clone(), + pledge.clone(), + mixnode.clone(), + cost_params.clone(), + ); - if deps.api.verify_message(msg, signature, &public_key)? { + if deps + .api + .verify_message(msg, signature.clone(), &public_key)? + { Ok(()) } else { - Err(MixnetContractError::InvalidEd25519Signature) + // attempt to use legacy + let msg_legacy = construct_legacy_mixnode_bonding_sign_payload( + nonce, + sender, + pledge, + mixnode, + cost_params, + ); + if deps + .api + .verify_message(msg_legacy, signature, &public_key)? + { + Ok(()) + } else { + Err(MixnetContractError::InvalidEd25519Signature) + } } } diff --git a/contracts/mixnet/src/mixnodes/transactions.rs b/contracts/mixnet/src/mixnodes/transactions.rs index 032ef29bc9..59ebc432c0 100644 --- a/contracts/mixnet/src/mixnodes/transactions.rs +++ b/contracts/mixnet/src/mixnodes/transactions.rs @@ -1,8 +1,7 @@ -// Copyright 2021-2023 - Nym Technologies SA +// Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{coin, Addr, Coin, DepsMut, Env, MessageInfo, Response, Storage}; - +use cosmwasm_std::{coin, Coin, DepsMut, Env, MessageInfo, Response, Storage}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ new_mixnode_bonding_event, new_mixnode_config_update_event, @@ -25,8 +24,8 @@ use crate::mixnodes::signature_helpers::verify_mixnode_bonding_signature; use crate::signing::storage as signing_storage; use crate::support::helpers::{ ensure_bonded, ensure_epoch_in_progress_state, ensure_is_authorized, ensure_no_existing_bond, - ensure_no_pending_pledge_changes, ensure_proxy_match, ensure_sent_by_vesting_contract, - validate_pledge, + ensure_no_pending_pledge_changes, ensure_operating_cost_within_range, + ensure_profit_margin_within_range, validate_pledge, }; use super::storage; @@ -61,74 +60,30 @@ pub fn assign_mixnode_layer( Ok(Response::default()) } -pub fn try_add_mixnode( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - mix_node: MixNode, - cost_params: MixNodeCostParams, - owner_signature: MessageSignature, -) -> Result { - _try_add_mixnode( - deps, - env, - mix_node, - cost_params, - info.funds, - info.sender, - owner_signature, - None, - ) -} - -pub fn try_add_mixnode_on_behalf( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - mix_node: MixNode, - cost_params: MixNodeCostParams, - owner: String, - owner_signature: MessageSignature, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let proxy = info.sender; - let owner = deps.api.addr_validate(&owner)?; - _try_add_mixnode( - deps, - env, - mix_node, - cost_params, - info.funds, - owner, - owner_signature, - Some(proxy), - ) -} - -// I'm not entirely sure how to deal with this warning at the current moment -// // TODO: perhaps also require the user to explicitly provide what it thinks is the current nonce // so that we could return a better error message if it doesn't match? -#[allow(clippy::too_many_arguments)] -fn _try_add_mixnode( +pub(crate) fn try_add_mixnode( deps: DepsMut<'_>, env: Env, + info: MessageInfo, mixnode: MixNode, cost_params: MixNodeCostParams, - pledge: Vec, - owner: Addr, owner_signature: MessageSignature, - proxy: Option, ) -> Result { + // ensure the profit margin is within the defined range + ensure_profit_margin_within_range(deps.storage, cost_params.profit_margin_percent)?; + + // ensure the operating cost is within the defined range + ensure_operating_cost_within_range(deps.storage, &cost_params.interval_operating_cost)?; + // check if the pledge contains any funds of the appropriate denomination let minimum_pledge = mixnet_params_storage::minimum_mixnode_pledge(deps.storage)?; - let pledge = validate_pledge(pledge, minimum_pledge)?; + let pledge = validate_pledge(info.funds, minimum_pledge)?; // if the client has an active bonded mixnode or gateway, don't allow bonding // note that this has to be done explicitly as `UniqueIndex` constraint would not protect us // against attempting to use different node types (i.e. gateways and mixnodes) - ensure_no_existing_bond(&owner, deps.storage)?; + ensure_no_existing_bond(&info.sender, deps.storage)?; // there's no need to explicitly check whether there already exists mixnode with the same // identity or sphinx keys as this is going to be done implicitly when attempting to save @@ -137,8 +92,7 @@ fn _try_add_mixnode( // check if this sender actually owns the mixnode by checking the signature verify_mixnode_bonding_signature( deps.as_ref(), - owner.clone(), - proxy.clone(), + info.sender.clone(), pledge.clone(), mixnode.clone(), cost_params.clone(), @@ -146,7 +100,7 @@ fn _try_add_mixnode( )?; // update the signing nonce associated with this sender so that the future signature would be made on the new value - signing_storage::increment_signing_nonce(deps.storage, owner.clone())?; + signing_storage::increment_signing_nonce(deps.storage, info.sender.clone())?; let node_identity = mixnode.identity_key.clone(); let (node_id, layer) = save_new_mixnode( @@ -154,14 +108,12 @@ fn _try_add_mixnode( env, mixnode, cost_params, - owner.clone(), - proxy.clone(), + info.sender.clone(), pledge.clone(), )?; Ok(Response::new().add_event(new_mixnode_bonding_event( - &owner, - &proxy, + &info.sender, &pledge, &node_identity, node_id, @@ -174,43 +126,19 @@ pub fn try_increase_pledge( env: Env, info: MessageInfo, ) -> Result { - _try_increase_pledge(deps, env, info.funds, info.sender, None) -} - -pub fn try_increase_pledge_on_behalf( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - owner: String, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let proxy = info.sender; - let owner = deps.api.addr_validate(&owner)?; - _try_increase_pledge(deps, env, info.funds, owner, Some(proxy)) -} - -pub fn _try_increase_pledge( - deps: DepsMut<'_>, - env: Env, - increase: Vec, - owner: Addr, - proxy: Option, -) -> Result { - let mix_details = get_mixnode_details_by_owner(deps.storage, owner.clone())? - .ok_or(MixnetContractError::NoAssociatedMixNodeBond { owner })?; + let mix_details = get_mixnode_details_by_owner(deps.storage, info.sender.clone())? + .ok_or(MixnetContractError::NoAssociatedMixNodeBond { owner: info.sender })?; let mut pending_changes = mix_details.pending_changes; let mix_id = mix_details.mix_id(); // increasing pledge is only allowed if the epoch is currently not in the process of being advanced ensure_epoch_in_progress_state(deps.storage)?; - ensure_proxy_match(&proxy, &mix_details.bond_information.proxy)?; ensure_bonded(&mix_details.bond_information)?; ensure_no_pending_pledge_changes(&pending_changes)?; let rewarding_denom = rewarding_denom(deps.storage)?; - let pledge_increase = validate_pledge(increase, coin(1, rewarding_denom))?; + let pledge_increase = validate_pledge(info.funds, coin(1, rewarding_denom))?; let cosmos_event = new_pending_pledge_increase_event(mix_id, &pledge_increase); @@ -232,39 +160,14 @@ pub fn try_decrease_pledge( info: MessageInfo, decrease_by: Coin, ) -> Result { - _try_decrease_pledge(deps, env, decrease_by, info.sender, None) -} - -pub fn try_decrease_pledge_on_behalf( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - decrease_by: Coin, - owner: String, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let proxy = info.sender; - let owner = deps.api.addr_validate(&owner)?; - _try_decrease_pledge(deps, env, decrease_by, owner, Some(proxy)) -} - -pub fn _try_decrease_pledge( - deps: DepsMut<'_>, - env: Env, - decrease_by: Coin, - owner: Addr, - proxy: Option, -) -> Result { - let mix_details = get_mixnode_details_by_owner(deps.storage, owner.clone())? - .ok_or(MixnetContractError::NoAssociatedMixNodeBond { owner })?; + let mix_details = get_mixnode_details_by_owner(deps.storage, info.sender.clone())? + .ok_or(MixnetContractError::NoAssociatedMixNodeBond { owner: info.sender })?; let mut pending_changes = mix_details.pending_changes; let mix_id = mix_details.mix_id(); // decreasing pledge is only allowed if the epoch is currently not in the process of being advanced ensure_epoch_in_progress_state(deps.storage)?; - ensure_proxy_match(&proxy, &mix_details.bond_information.proxy)?; ensure_bonded(&mix_details.bond_information)?; ensure_no_pending_pledge_changes(&pending_changes)?; @@ -312,34 +215,12 @@ pub fn _try_decrease_pledge( Ok(Response::new().add_event(cosmos_event)) } -pub fn try_remove_mixnode_on_behalf( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - owner: String, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let proxy = info.sender; - let owner = deps.api.addr_validate(&owner)?; - _try_remove_mixnode(deps, env, owner, Some(proxy)) -} - -pub fn try_remove_mixnode( +pub(crate) fn try_remove_mixnode( deps: DepsMut<'_>, env: Env, info: MessageInfo, ) -> Result { - _try_remove_mixnode(deps, env, info.sender, None) -} - -pub(crate) fn _try_remove_mixnode( - deps: DepsMut<'_>, - env: Env, - owner: Addr, - proxy: Option, -) -> Result { - let existing_bond = must_get_mixnode_bond_by_owner(deps.storage, &owner)?; + let existing_bond = must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; let pending_changes = storage::PENDING_MIXNODE_CHANGES .may_load(deps.storage, existing_bond.mix_id)? .unwrap_or_default(); @@ -348,15 +229,12 @@ pub(crate) fn _try_remove_mixnode( ensure_epoch_in_progress_state(deps.storage)?; // see if the proxy matches - ensure_proxy_match(&proxy, &existing_bond.proxy)?; ensure_bonded(&existing_bond)?; // if there are any pending requests to change the pledge, wait for them to resolve before allowing the unbonding ensure_no_pending_pledge_changes(&pending_changes)?; // set `is_unbonding` field - // clippy beta 1.70.0-beta.1 false positive - #[allow(clippy::redundant_clone)] let mut updated_bond = existing_bond.clone(); updated_bond.is_unbonding = true; storage::mixnode_bonds().replace( @@ -375,7 +253,6 @@ pub(crate) fn _try_remove_mixnode( Ok( Response::new().add_event(new_pending_mixnode_unbonding_event( &existing_bond.owner, - &existing_bond.proxy, existing_bond.identity(), existing_bond.mix_id, )), @@ -387,39 +264,13 @@ pub(crate) fn try_update_mixnode_config( info: MessageInfo, new_config: MixNodeConfigUpdate, ) -> Result { - let owner = info.sender; - _try_update_mixnode_config(deps, new_config, owner, None) -} - -pub(crate) fn try_update_mixnode_config_on_behalf( - deps: DepsMut<'_>, - info: MessageInfo, - new_config: MixNodeConfigUpdate, - owner: String, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let owner = deps.api.addr_validate(&owner)?; - let proxy = info.sender; - _try_update_mixnode_config(deps, new_config, owner, Some(proxy)) -} - -pub(crate) fn _try_update_mixnode_config( - deps: DepsMut<'_>, - new_config: MixNodeConfigUpdate, - owner: Addr, - proxy: Option, -) -> Result { - let existing_bond = must_get_mixnode_bond_by_owner(deps.storage, &owner)?; + let existing_bond = must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; ensure_bonded(&existing_bond)?; - ensure_proxy_match(&proxy, &existing_bond.proxy)?; let cfg_update_event = - new_mixnode_config_update_event(existing_bond.mix_id, &owner, &proxy, &new_config); + new_mixnode_config_update_event(existing_bond.mix_id, &info.sender, &new_config); - // clippy beta 1.70.0-beta.1 false positive - #[allow(clippy::redundant_clone)] let mut updated_bond = existing_bond.clone(); updated_bond.mix_node.host = new_config.host; updated_bond.mix_node.mix_port = new_config.mix_port; @@ -442,45 +293,24 @@ pub(crate) fn try_update_mixnode_cost_params( env: Env, info: MessageInfo, new_costs: MixNodeCostParams, -) -> Result { - let owner = info.sender; - _try_update_mixnode_cost_params(deps, env, new_costs, owner, None) -} - -pub(crate) fn try_update_mixnode_cost_params_on_behalf( - deps: DepsMut, - env: Env, - info: MessageInfo, - new_costs: MixNodeCostParams, - owner: String, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let owner = deps.api.addr_validate(&owner)?; - let proxy = info.sender; - _try_update_mixnode_cost_params(deps, env, new_costs, owner, Some(proxy)) -} - -pub(crate) fn _try_update_mixnode_cost_params( - deps: DepsMut, - env: Env, - new_costs: MixNodeCostParams, - owner: Addr, - proxy: Option, ) -> Result { // see if the node still exists - let existing_bond = must_get_mixnode_bond_by_owner(deps.storage, &owner)?; + let existing_bond = must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; // changing cost params is only allowed if the epoch is currently not in the process of being advanced ensure_epoch_in_progress_state(deps.storage)?; - ensure_proxy_match(&proxy, &existing_bond.proxy)?; ensure_bonded(&existing_bond)?; + // ensure the profit margin is within the defined range + ensure_profit_margin_within_range(deps.storage, new_costs.profit_margin_percent)?; + + // ensure the operating cost is within the defined range + ensure_operating_cost_within_range(deps.storage, &new_costs.interval_operating_cost)?; + let cosmos_event = new_mixnode_pending_cost_params_update_event( existing_bond.mix_id, - &owner, - &proxy, + &info.sender, &new_costs, ); @@ -497,7 +327,7 @@ pub(crate) fn _try_update_mixnode_cost_params( #[cfg(test)] pub mod tests { use cosmwasm_std::testing::mock_info; - use cosmwasm_std::{Order, StdResult, Uint128}; + use cosmwasm_std::{Addr, Order, StdResult, Uint128}; use mixnet_contract_common::mixnode::PendingMixNodeChanges; use mixnet_contract_common::{EpochState, EpochStatus, ExecuteMsg, LayerDistribution, Percent}; @@ -680,38 +510,6 @@ pub mod tests { assert_eq!(res, Err(MixnetContractError::InvalidEd25519Signature)); } - #[test] - fn mixnode_add_with_illegal_proxy() { - let mut test = TestSetup::new(); - let env = test.env(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let owner = "alice"; - let (mixnode, sig, _) = test.mixnode_with_signature(owner, None); - let cost_params = fixtures::mix_node_cost_params_fixture(); - - let res = try_add_mixnode_on_behalf( - test.deps_mut(), - env, - mock_info(illegal_proxy.as_ref(), &good_mixnode_pledge()), - mixnode, - cost_params, - owner.to_string(), - sig, - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract, - } - ) - } - #[test] fn removing_mixnode_cant_be_performed_if_epoch_transition_is_in_progress() { let bad_states = vec![ @@ -761,23 +559,6 @@ pub mod tests { ); let mix_id = test.add_dummy_mixnode(owner, None); - let vesting_contract = test.vesting_contract(); - - // attempted to remove on behalf with invalid proxy (current is `None`) - let res = try_remove_mixnode_on_behalf( - test.deps_mut(), - env.clone(), - mock_info(vesting_contract.as_ref(), &[]), - owner.to_string(), - ); - - assert_eq!( - res, - Err(MixnetContractError::ProxyMismatch { - existing: "None".to_string(), - incoming: vesting_contract.into_string(), - }) - ); // "normal" unbonding succeeds and unbonding event is pushed to the pending epoch events let res = try_remove_mixnode(test.deps_mut(), env.clone(), info.clone()); @@ -799,35 +580,6 @@ pub mod tests { assert_eq!(res, Err(MixnetContractError::MixnodeIsUnbonding { mix_id })) } - #[test] - fn mixnode_remove_with_illegal_proxy() { - let mut test = TestSetup::new(); - let env = test.env(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let owner = "alice"; - - test.add_dummy_mixnode_with_illegal_proxy(owner, None, illegal_proxy.clone()); - - let res = try_remove_mixnode_on_behalf( - test.deps_mut(), - env, - mock_info(illegal_proxy.as_ref(), &[]), - owner.to_string(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract, - } - ) - } - #[test] fn mixnode_remove_is_not_allowed_if_there_are_pending_pledge_changes() { let mut test = TestSetup::new(); @@ -908,22 +660,7 @@ pub mod tests { ); let mix_id = test.add_dummy_mixnode(owner, None); - let vesting_contract = test.vesting_contract(); - // attempted to remove on behalf with invalid proxy (current is `None`) - let res = try_update_mixnode_config_on_behalf( - test.deps_mut(), - mock_info(vesting_contract.as_ref(), &[]), - update.clone(), - owner.to_string(), - ); - assert_eq!( - res, - Err(MixnetContractError::ProxyMismatch { - existing: "None".to_string(), - incoming: vesting_contract.into_string(), - }) - ); // "normal" update succeeds let res = try_update_mixnode_config(test.deps_mut(), info.clone(), update.clone()); assert!(res.is_ok()); @@ -943,41 +680,6 @@ pub mod tests { assert_eq!(res, Err(MixnetContractError::MixnodeIsUnbonding { mix_id })) } - #[test] - fn updating_mixnode_config_with_illegal_proxy() { - let mut test = TestSetup::new(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let owner = "alice"; - - test.add_dummy_mixnode_with_illegal_proxy(owner, None, illegal_proxy.clone()); - let update = MixNodeConfigUpdate { - host: "1.1.1.1:1234".to_string(), - mix_port: 1234, - verloc_port: 1235, - http_api_port: 1236, - version: "v1.2.3".to_string(), - }; - - let res = try_update_mixnode_config_on_behalf( - test.deps_mut(), - mock_info(illegal_proxy.as_ref(), &[]), - update, - owner.to_string(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract, - } - ) - } - #[test] fn mixnode_cost_params_cant_be_updated_when_epoch_transition_is_in_progress() { let bad_states = vec![ @@ -1042,23 +744,7 @@ pub mod tests { ); let mix_id = test.add_dummy_mixnode(owner, None); - let vesting_contract = test.vesting_contract(); - // attempted to remove on behalf with invalid proxy (current is `None`) - let res = try_update_mixnode_cost_params_on_behalf( - test.deps_mut(), - env.clone(), - mock_info(vesting_contract.as_ref(), &[]), - update.clone(), - owner.to_string(), - ); - assert_eq!( - res, - Err(MixnetContractError::ProxyMismatch { - existing: "None".to_string(), - incoming: vesting_contract.into_string(), - }) - ); // "normal" update succeeds let res = try_update_mixnode_cost_params( test.deps_mut(), @@ -1099,40 +785,6 @@ pub mod tests { assert_eq!(res, Err(MixnetContractError::MixnodeIsUnbonding { mix_id })) } - #[test] - fn updating_mixnode_cost_params_with_illegal_proxy() { - let mut test = TestSetup::new(); - let env = test.env(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let owner = "alice"; - - test.add_dummy_mixnode_with_illegal_proxy(owner, None, illegal_proxy.clone()); - let update = MixNodeCostParams { - profit_margin_percent: Percent::from_percentage_value(42).unwrap(), - interval_operating_cost: Coin::new(12345678, TEST_COIN_DENOM), - }; - - let res = try_update_mixnode_cost_params_on_behalf( - test.deps_mut(), - env, - mock_info(illegal_proxy.as_ref(), &[]), - update, - owner.to_string(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract, - } - ) - } - #[test] fn adding_mixnode_with_duplicate_sphinx_key_errors_out() { let mut test = TestSetup::new(); @@ -1240,69 +892,6 @@ pub mod tests { ) } - #[test] - fn is_not_allowed_if_theres_proxy_mismatch() { - let mut test = TestSetup::new(); - let env = test.env(); - - let owner_without_proxy = Addr::unchecked("no-proxy"); - let owner_with_proxy = Addr::unchecked("with-proxy"); - let proxy = Addr::unchecked("proxy"); - let wrong_proxy = Addr::unchecked("unrelated-proxy"); - - test.add_dummy_mixnode(owner_without_proxy.as_str(), None); - test.add_dummy_mixnode_with_illegal_proxy( - owner_with_proxy.as_str(), - None, - proxy.clone(), - ); - - let res = _try_increase_pledge( - test.deps_mut(), - env.clone(), - Vec::new(), - owner_without_proxy.clone(), - Some(proxy), - ); - assert_eq!( - res, - Err(MixnetContractError::ProxyMismatch { - existing: "None".to_string(), - incoming: "proxy".to_string(), - }) - ); - - let res = _try_increase_pledge( - test.deps_mut(), - env.clone(), - Vec::new(), - owner_with_proxy.clone(), - None, - ); - assert_eq!( - res, - Err(MixnetContractError::ProxyMismatch { - existing: "proxy".to_string(), - incoming: "None".to_string(), - }) - ); - - let res = _try_increase_pledge( - test.deps_mut(), - env, - Vec::new(), - owner_with_proxy.clone(), - Some(wrong_proxy), - ); - assert_eq!( - res, - Err(MixnetContractError::ProxyMismatch { - existing: "proxy".to_string(), - incoming: "unrelated-proxy".to_string(), - }) - ) - } - #[test] fn is_not_allowed_if_mixnode_has_unbonded_or_is_unbonding() { let mut test = TestSetup::new(); @@ -1457,35 +1046,6 @@ pub mod tests { } ); } - - #[test] - fn fails_for_illegal_proxy() { - let mut test = TestSetup::new(); - let env = test.env(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let owner = "alice"; - - test.add_dummy_mixnode_with_illegal_proxy(owner, None, illegal_proxy.clone()); - - let res = try_increase_pledge_on_behalf( - test.deps_mut(), - env, - mock_info(illegal_proxy.as_ref(), &[coin(123, TEST_COIN_DENOM)]), - owner.to_string(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract, - } - ) - } } #[cfg(test)] @@ -1546,73 +1106,6 @@ pub mod tests { ) } - #[test] - fn is_not_allowed_if_theres_proxy_mismatch() { - let mut test = TestSetup::new(); - let env = test.env(); - - let owner_without_proxy = Addr::unchecked("no-proxy"); - let owner_with_proxy = Addr::unchecked("with-proxy"); - let proxy = Addr::unchecked("proxy"); - let wrong_proxy = Addr::unchecked("unrelated-proxy"); - - // just to make sure that after decrease the value would still be above the minimum - let stake = Uint128::new(100_000_000_000); - let decrease = test.coin(1000); - - test.add_dummy_mixnode(owner_without_proxy.as_str(), Some(stake)); - test.add_dummy_mixnode_with_illegal_proxy( - owner_with_proxy.as_str(), - Some(stake), - proxy.clone(), - ); - - let res = _try_decrease_pledge( - test.deps_mut(), - env.clone(), - decrease.clone(), - owner_without_proxy.clone(), - Some(proxy), - ); - assert_eq!( - res, - Err(MixnetContractError::ProxyMismatch { - existing: "None".to_string(), - incoming: "proxy".to_string(), - }) - ); - - let res = _try_decrease_pledge( - test.deps_mut(), - env.clone(), - decrease.clone(), - owner_with_proxy.clone(), - None, - ); - assert_eq!( - res, - Err(MixnetContractError::ProxyMismatch { - existing: "proxy".to_string(), - incoming: "None".to_string(), - }) - ); - - let res = _try_decrease_pledge( - test.deps_mut(), - env, - decrease, - owner_with_proxy.clone(), - Some(wrong_proxy), - ); - assert_eq!( - res, - Err(MixnetContractError::ProxyMismatch { - existing: "proxy".to_string(), - incoming: "unrelated-proxy".to_string(), - }) - ) - } - #[test] fn is_not_allowed_if_mixnode_has_unbonded_or_is_unbonding() { let mut test = TestSetup::new(); @@ -1809,38 +1302,5 @@ pub mod tests { } ); } - - #[test] - fn fails_for_illegal_proxy() { - let mut test = TestSetup::new(); - let env = test.env(); - - let stake = Uint128::new(100_000_000_000); - let decrease = test.coin(1000); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let owner = "alice"; - - test.add_dummy_mixnode_with_illegal_proxy(owner, Some(stake), illegal_proxy.clone()); - - let res = try_decrease_pledge_on_behalf( - test.deps_mut(), - env, - mock_info(illegal_proxy.as_ref(), &[coin(123, TEST_COIN_DENOM)]), - decrease, - owner.to_string(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract, - } - ) - } } } diff --git a/contracts/mixnet/src/queued_migrations.rs b/contracts/mixnet/src/queued_migrations.rs index 8df3ef5d86..248a33d30e 100644 --- a/contracts/mixnet/src/queued_migrations.rs +++ b/contracts/mixnet/src/queued_migrations.rs @@ -1,2 +1,66 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 + +use crate::interval::storage as interval_storage; +use crate::mixnet_contract_settings::storage as mixnet_params_storage; +use cosmwasm_std::{DepsMut, Order, Storage}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::PendingEpochEventKind; + +fn ensure_no_pending_proxy_events(storage: &dyn Storage) -> Result<(), MixnetContractError> { + let last_executed = interval_storage::LAST_PROCESSED_EPOCH_EVENT.load(storage)?; + let last_inserted = interval_storage::EPOCH_EVENT_ID_COUNTER.load(storage)?; + + // no pending events + if last_executed == last_inserted { + return Ok(()); + } + + for maybe_event in + interval_storage::PENDING_EPOCH_EVENTS.range(storage, None, None, Order::Ascending) + { + let (id, event_data) = maybe_event?; + match event_data.kind { + PendingEpochEventKind::Delegate { proxy, .. } => { + if proxy.is_some() { + return Err(MixnetContractError::FailedMigration { + comment: format!( + "there is a pending vesting contract delegation with id {id}" + ), + }); + } + } + PendingEpochEventKind::Undelegate { proxy, .. } => { + if proxy.is_some() { + return Err(MixnetContractError::FailedMigration { + comment: format!( + "there is a pending vesting contract undelegation with id {id}" + ), + }); + } + } + _ => continue, + } + } + Ok(()) +} + +pub(crate) fn vesting_purge(deps: DepsMut) -> Result<(), MixnetContractError> { + ensure_no_pending_proxy_events(deps.storage)?; + + Ok(()) +} + +pub(crate) fn explicit_contract_admin(deps: DepsMut) -> Result<(), MixnetContractError> { + // we need to read the deprecated field to migrate it over + #[allow(deprecated)] + // SAFETY: this value should ALWAYS exist on the first execution of this migration; + // as a matter of fact, it should ALWAYS continue existing until another migration + #[allow(clippy::expect_used)] + let existing_admin = mixnet_params_storage::CONTRACT_STATE + .load(deps.storage)? + .owner + .expect("the contract state is corrupt - there's no admin set"); + mixnet_params_storage::ADMIN.set(deps, Some(existing_admin))?; + Ok(()) +} diff --git a/contracts/mixnet/src/rewards/transactions.rs b/contracts/mixnet/src/rewards/transactions.rs index 9aa417ae7c..f772dca800 100644 --- a/contracts/mixnet/src/rewards/transactions.rs +++ b/contracts/mixnet/src/rewards/transactions.rs @@ -1,8 +1,20 @@ -// Copyright 2021-2023 - Nym Technologies SA +// Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{wasm_execute, Addr, DepsMut, Env, MessageInfo, Response}; - +use super::storage; +use crate::delegations::storage as delegations_storage; +use crate::interval::storage as interval_storage; +use crate::interval::storage::{push_new_epoch_event, push_new_interval_event}; +use crate::mixnet_contract_settings::storage as mixnet_params_storage; +use crate::mixnet_contract_settings::storage::ADMIN; +use crate::mixnodes::helpers::get_mixnode_details_by_owner; +use crate::mixnodes::storage as mixnodes_storage; +use crate::rewards::helpers; +use crate::rewards::helpers::update_and_save_last_rewarded; +use crate::support::helpers::{ + ensure_bonded, ensure_can_advance_epoch, ensure_epoch_in_progress_state, AttachSendTokens, +}; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ new_active_set_update_event, new_mix_rewarding_event, @@ -16,22 +28,6 @@ use mixnet_contract_common::reward_params::{ IntervalRewardingParamsUpdate, NodeRewardParams, Performance, }; use mixnet_contract_common::{Delegation, EpochState, MixId}; -use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; - -use crate::delegations::storage as delegations_storage; -use crate::interval::storage as interval_storage; -use crate::interval::storage::{push_new_epoch_event, push_new_interval_event}; -use crate::mixnet_contract_settings::storage as mixnet_params_storage; -use crate::mixnodes::helpers::get_mixnode_details_by_owner; -use crate::mixnodes::storage as mixnodes_storage; -use crate::rewards::helpers; -use crate::rewards::helpers::update_and_save_last_rewarded; -use crate::support::helpers::{ - ensure_bonded, ensure_can_advance_epoch, ensure_epoch_in_progress_state, ensure_is_owner, - ensure_proxy_match, ensure_sent_by_vesting_contract, send_to_proxy_or_owner, -}; - -use super::storage; pub(crate) fn try_reward_mixnode( deps: DepsMut<'_>, @@ -111,6 +107,14 @@ pub(crate) fn try_reward_mixnode( ); } + // make sure node's profit margin is within the allowed range, + // if not adjust it accordingly + let params = mixnet_params_storage::CONTRACT_STATE + .load(deps.storage)? + .params; + mix_rewarding.normalise_profit_margin(params.profit_margin); + mix_rewarding.normalise_operating_cost(params.interval_operating_cost); + let rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; let node_reward_params = NodeRewardParams::new(node_performance, node_status.is_active()); @@ -140,37 +144,16 @@ pub(crate) fn try_withdraw_operator_reward( deps: DepsMut<'_>, info: MessageInfo, ) -> Result { - _try_withdraw_operator_reward(deps, info.sender, None) -} - -pub(crate) fn try_withdraw_operator_reward_on_behalf( - deps: DepsMut<'_>, - info: MessageInfo, - owner: String, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let proxy = info.sender; - let owner = deps.api.addr_validate(&owner)?; - _try_withdraw_operator_reward(deps, owner, Some(proxy)) -} - -pub(crate) fn _try_withdraw_operator_reward( - deps: DepsMut<'_>, - owner: Addr, - proxy: Option, -) -> Result { - // we need to grab all of the node's details so we'd known original pledge alongside + // we need to grab all of the node's details, so we'd known original pledge alongside // all the earned rewards (and obviously to know if this node even exists and is still // in the bonded state) - let mix_details = get_mixnode_details_by_owner(deps.storage, owner.clone())?.ok_or( + let mix_details = get_mixnode_details_by_owner(deps.storage, info.sender.clone())?.ok_or( MixnetContractError::NoAssociatedMixNodeBond { - owner: owner.clone(), + owner: info.sender.clone(), }, )?; let mix_id = mix_details.mix_id(); - ensure_proxy_match(&proxy, &mix_details.bond_information.proxy)?; ensure_bonded(&mix_details.bond_information)?; let reward = helpers::withdraw_operator_reward(deps.storage, mix_details)?; @@ -178,26 +161,13 @@ pub(crate) fn _try_withdraw_operator_reward( // if the reward is zero, don't track or send anything - there's no point if !reward.amount.is_zero() { - let return_tokens = send_to_proxy_or_owner(&proxy, &owner, vec![reward.clone()]); - response = response.add_message(return_tokens); - - if let Some(proxy) = &proxy { - // we can only attempt to send the message to the vesting contract if the proxy IS the vesting contract - // otherwise, we don't care - let vesting_contract = mixnet_params_storage::vesting_contract_address(deps.storage)?; - if proxy == vesting_contract { - let msg = VestingContractExecuteMsg::TrackReward { - amount: reward.clone(), - address: owner.clone().into_string(), - }; - let track_reward_message = wasm_execute(proxy, &msg, vec![])?; - response = response.add_message(track_reward_message); - } - } + response = response.send_tokens(&info.sender, reward.clone()) } Ok(response.add_event(new_withdraw_operator_reward_event( - &owner, &proxy, reward, mix_id, + &info.sender, + reward, + mix_id, ))) } @@ -205,37 +175,15 @@ pub(crate) fn try_withdraw_delegator_reward( deps: DepsMut<'_>, info: MessageInfo, mix_id: MixId, -) -> Result { - _try_withdraw_delegator_reward(deps, mix_id, info.sender, None) -} - -pub(crate) fn try_withdraw_delegator_reward_on_behalf( - deps: DepsMut<'_>, - info: MessageInfo, - mix_id: MixId, - owner: String, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let proxy = info.sender; - let owner = deps.api.addr_validate(&owner)?; - _try_withdraw_delegator_reward(deps, mix_id, owner, Some(proxy)) -} - -pub(crate) fn _try_withdraw_delegator_reward( - deps: DepsMut<'_>, - mix_id: MixId, - owner: Addr, - proxy: Option, ) -> Result { // see if the delegation even exists - let storage_key = Delegation::generate_storage_key(mix_id, &owner, proxy.as_ref()); + let storage_key = Delegation::generate_storage_key(mix_id, &info.sender, None); let delegation = match delegations_storage::delegations().may_load(deps.storage, storage_key)? { None => { return Err(MixnetContractError::NoMixnodeDelegationFound { mix_id, - address: owner.into_string(), - proxy: proxy.map(Addr::into_string), + address: info.sender.into_string(), + proxy: None, }); } Some(delegation) => delegation, @@ -257,33 +205,18 @@ pub(crate) fn _try_withdraw_delegator_reward( _ => (), }; - ensure_proxy_match(&proxy, &delegation.proxy)?; - let reward = helpers::withdraw_delegator_reward(deps.storage, delegation, mix_rewarding)?; let mut response = Response::new(); // if the reward is zero, don't track or send anything - there's no point if !reward.amount.is_zero() { - let return_tokens = send_to_proxy_or_owner(&proxy, &owner, vec![reward.clone()]); - response = response.add_message(return_tokens); - - if let Some(proxy) = &proxy { - // we can only attempt to send the message to the vesting contract if the proxy IS the vesting contract - // otherwise, we don't care - let vesting_contract = mixnet_params_storage::vesting_contract_address(deps.storage)?; - if proxy == vesting_contract { - let msg = VestingContractExecuteMsg::TrackReward { - amount: reward.clone(), - address: owner.clone().into_string(), - }; - let track_reward_message = wasm_execute(proxy, &msg, vec![])?; - response = response.add_message(track_reward_message); - } - } + response = response.send_tokens(&info.sender, reward.clone()) } Ok(response.add_event(new_withdraw_delegator_reward_event( - &owner, &proxy, reward, mix_id, + &info.sender, + reward, + mix_id, ))) } @@ -294,7 +227,7 @@ pub(crate) fn try_update_active_set_size( active_set_size: u32, force_immediately: bool, ) -> Result { - ensure_is_owner(info.sender, deps.storage)?; + ADMIN.assert_admin(deps.as_ref(), &info.sender)?; let mut rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; if active_set_size == 0 { @@ -340,7 +273,7 @@ pub(crate) fn try_update_rewarding_params( updated_params: IntervalRewardingParamsUpdate, force_immediately: bool, ) -> Result { - ensure_is_owner(info.sender, deps.storage)?; + ADMIN.assert_admin(deps.as_ref(), &info.sender)?; if !updated_params.contains_updates() { return Err(MixnetContractError::EmptyParamsChangeMsg); @@ -1405,13 +1338,10 @@ pub mod tests { #[cfg(test)] mod withdrawing_delegator_reward { - use cosmwasm_std::{coin, BankMsg, CosmosMsg, Decimal, Uint128}; - - use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; - use crate::interval::pending_events; - use crate::support::tests::fixtures::TEST_COIN_DENOM; use crate::support::tests::test_helpers::{assert_eq_with_leeway, TestSetup}; + use cosmwasm_std::{BankMsg, CosmosMsg, Decimal, Uint128}; + use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; use super::*; @@ -1742,60 +1672,14 @@ pub mod tests { let accumulated_actual = truncate_reward_amount(accumulated_quad); assert_eq_with_leeway(total_claimed, accumulated_actual, Uint128::new(6)); } - - #[test] - fn fails_for_illegal_proxy() { - let test = TestSetup::new(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let mut test = TestSetup::new(); - let mix_id = - test.add_dummy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); - - let delegator = "delegator"; - - test.add_immediate_delegation_with_illegal_proxy( - delegator, - 100_000_000u128, - mix_id, - illegal_proxy.clone(), - ); - - // reward the node - test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); - test.start_epoch_transition(); - test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); - - let res = try_withdraw_delegator_reward_on_behalf( - test.deps_mut(), - mock_info(illegal_proxy.as_ref(), &[coin(123, TEST_COIN_DENOM)]), - mix_id, - delegator.to_string(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract, - } - ) - } } #[cfg(test)] mod withdrawing_operator_reward { - use cosmwasm_std::{coin, BankMsg, CosmosMsg, Uint128}; - - use crate::interval::pending_events; - use crate::support::tests::fixtures::TEST_COIN_DENOM; - use crate::support::tests::test_helpers::TestSetup; - use super::*; + use crate::interval::pending_events; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::{Addr, BankMsg, CosmosMsg, Uint128}; #[test] fn can_only_be_done_if_bond_exists() { @@ -1908,46 +1792,11 @@ pub mod tests { }) ); } - - #[test] - fn fails_for_illegal_proxy() { - let mut test = TestSetup::new(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let owner = "mix-owner1"; - let mix_id = test.add_dummy_mixnode_with_illegal_proxy( - owner, - Some(Uint128::new(1_000_000_000_000)), - illegal_proxy.clone(), - ); - - // reward the node - test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); - test.start_epoch_transition(); - test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); - - let res = try_withdraw_operator_reward_on_behalf( - test.deps_mut(), - mock_info(illegal_proxy.as_ref(), &[coin(123, TEST_COIN_DENOM)]), - owner.to_string(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract, - } - ) - } } #[cfg(test)] mod updating_active_set { + use cw_controllers::AdminError::NotAdmin; use mixnet_contract_common::EpochStatus; use crate::support::tests::test_helpers::TestSetup; @@ -2010,10 +1859,10 @@ pub mod tests { 42, false, ); - assert_eq!(res, Err(MixnetContractError::Unauthorized)); + assert_eq!(res, Err(MixnetContractError::Admin(NotAdmin {}))); let res = try_update_active_set_size(test.deps_mut(), env.clone(), random, 42, false); - assert_eq!(res, Err(MixnetContractError::Unauthorized)); + assert_eq!(res, Err(MixnetContractError::Admin(NotAdmin {}))); let res = try_update_active_set_size(test.deps_mut(), env, owner, 42, false); assert!(res.is_ok()) @@ -2154,6 +2003,7 @@ pub mod tests { #[cfg(test)] mod updating_rewarding_params { use cosmwasm_std::Decimal; + use cw_controllers::AdminError::NotAdmin; use mixnet_contract_common::EpochStatus; @@ -2237,11 +2087,11 @@ pub mod tests { update, false, ); - assert_eq!(res, Err(MixnetContractError::Unauthorized)); + assert_eq!(res, Err(MixnetContractError::Admin(NotAdmin {}))); let res = try_update_rewarding_params(test.deps_mut(), env.clone(), random, update, false); - assert_eq!(res, Err(MixnetContractError::Unauthorized)); + assert_eq!(res, Err(MixnetContractError::Admin(NotAdmin {}))); let res = try_update_rewarding_params(test.deps_mut(), env, owner, update, false); assert!(res.is_ok()) diff --git a/contracts/mixnet/src/support/helpers.rs b/contracts/mixnet/src/support/helpers.rs index 12c02a675a..19ddf2bb6e 100644 --- a/contracts/mixnet/src/support/helpers.rs +++ b/contracts/mixnet/src/support/helpers.rs @@ -4,11 +4,11 @@ use crate::gateways::storage as gateways_storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::mixnodes::storage as mixnodes_storage; -use cosmwasm_std::{wasm_execute, Addr, BankMsg, Coin, CosmosMsg, MessageInfo, Response, Storage}; +use cosmwasm_std::{Addr, BankMsg, Coin, CosmosMsg, Response, Storage}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::mixnode::PendingMixNodeChanges; -use mixnet_contract_common::{EpochState, EpochStatus, IdentityKeyRef, MixId, MixNodeBond}; -use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; +use mixnet_contract_common::{EpochState, EpochStatus, IdentityKeyRef, MixNodeBond}; +use nym_contracts_common::Percent; // helper trait to attach `Msg` to a response if it's provided #[allow(dead_code)] @@ -26,131 +26,16 @@ impl AttachOptionalMessage for Response { } } -// another helper trait to remove some duplicate code and consolidate comments regarding -// possible epoch progression halting behaviour -pub(crate) trait VestingTracking -where - Self: Sized, -{ - fn maybe_add_track_vesting_undelegation_message( - self, - storage: &dyn Storage, - proxy: Option, - owner: String, - mix_id: MixId, - amount: Coin, - ) -> Result; - - fn maybe_add_track_vesting_unbond_mixnode_message( - self, - storage: &dyn Storage, - proxy: Option, - owner: String, - amount: Coin, - ) -> Result; - - fn maybe_add_track_vesting_decrease_mixnode_pledge( - self, - storage: &dyn Storage, - proxy: Option, - owner: String, - amount: Coin, - ) -> Result; +pub(crate) trait AttachSendTokens { + fn send_tokens(self, to: impl AsRef, amount: Coin) -> Self; } -impl VestingTracking for Response { - fn maybe_add_track_vesting_undelegation_message( - self, - storage: &dyn Storage, - proxy: Option, - owner: String, - mix_id: MixId, - amount: Coin, - ) -> Result { - // if there's a proxy set (i.e. the vesting contract), send the track message - if let Some(proxy) = proxy { - let vesting_contract = mixnet_params_storage::vesting_contract_address(storage)?; - - // Note: this can INTENTIONALLY cause epoch progression halt if the proxy is not the vesting contract - // But this is fine, since this situation should have NEVER occurred in the first place - // (as all 'on_behalf' methods, including 'DelegateToMixnodeOnBehalf' that got us here, - // explicitly require the proxy to be the vesting contract) - // 'fixing' it would require manually inspecting the problematic event, investigating - // it's cause and manually (presumably via migration) clearing it. - if proxy != vesting_contract { - return Err(MixnetContractError::ProxyIsNotVestingContract { - received: proxy, - vesting_contract, - }); - } - - let msg = VestingContractExecuteMsg::TrackUndelegation { - owner, - mix_id, - amount, - }; - - let track_undelegate_message = wasm_execute(proxy, &msg, vec![])?; - Ok(self.add_message(track_undelegate_message)) - } else { - // there's no proxy so nothing to do - Ok(self) - } - } - - fn maybe_add_track_vesting_unbond_mixnode_message( - self, - storage: &dyn Storage, - proxy: Option, - owner: String, - amount: Coin, - ) -> Result { - // if there's a proxy set (i.e. the vesting contract), send the track message - if let Some(proxy) = proxy { - let vesting_contract = mixnet_params_storage::vesting_contract_address(storage)?; - - // exactly the same possible halting behaviour as in `maybe_add_track_vesting_undelegation_message`. - if proxy != vesting_contract { - return Err(MixnetContractError::ProxyIsNotVestingContract { - received: proxy, - vesting_contract, - }); - } - - let msg = VestingContractExecuteMsg::TrackUnbondMixnode { owner, amount }; - let track_unbond_message = wasm_execute(proxy, &msg, vec![])?; - Ok(self.add_message(track_unbond_message)) - } else { - // there's no proxy so nothing to do - Ok(self) - } - } - - fn maybe_add_track_vesting_decrease_mixnode_pledge( - self, - storage: &dyn Storage, - proxy: Option, - owner: String, - amount: Coin, - ) -> Result { - if let Some(proxy) = proxy { - let vesting_contract = mixnet_params_storage::vesting_contract_address(storage)?; - - // exactly the same possible halting behaviour as in `maybe_add_track_vesting_undelegation_message`. - if proxy != vesting_contract { - return Err(MixnetContractError::ProxyIsNotVestingContract { - received: proxy, - vesting_contract, - }); - } - - let msg = VestingContractExecuteMsg::TrackDecreasePledge { owner, amount }; - let track_decrease_pledge_message = wasm_execute(proxy, &msg, vec![])?; - Ok(self.add_message(track_decrease_pledge_message)) - } else { - // there's no proxy so nothing to do - Ok(self) - } +impl AttachSendTokens for Response { + fn send_tokens(self, to: impl AsRef, amount: Coin) -> Self { + self.add_message(BankMsg::Send { + to_address: to.as_ref().to_string(), + amount: vec![amount], + }) } } @@ -158,20 +43,6 @@ impl VestingTracking for Response { // api.debug(&*format!("\n\n\n=========================================\n{}\n=========================================\n\n\n", msg.into())); // } -/// Attempts to construct a `BankMsg` to send specified tokens to the provided -/// proxy address. If that's unavailable, the `BankMsg` will use the "owner" as the -/// "to_address". -pub(crate) fn send_to_proxy_or_owner( - proxy: &Option, - owner: &Addr, - amount: Vec, -) -> BankMsg { - BankMsg::Send { - to_address: proxy.as_ref().unwrap_or(owner).to_string(), - amount, - } -} - pub(crate) fn validate_pledge( mut pledge: Vec, minimum_pledge: Coin, @@ -323,53 +194,6 @@ pub(crate) fn ensure_can_advance_epoch( Ok(epoch_status) } -pub(crate) fn ensure_is_owner( - sender: Addr, - storage: &dyn Storage, -) -> Result<(), MixnetContractError> { - if sender - != crate::mixnet_contract_settings::storage::CONTRACT_STATE - .load(storage)? - .owner - { - return Err(MixnetContractError::Unauthorized); - } - Ok(()) -} - -pub(crate) fn ensure_proxy_match( - actual: &Option, - expected: &Option, -) -> Result<(), MixnetContractError> { - if actual != expected { - return Err(MixnetContractError::ProxyMismatch { - existing: expected - .as_ref() - .map_or_else(|| "None".to_string(), |a| a.as_str().to_string()), - incoming: actual - .as_ref() - .map_or_else(|| "None".to_string(), |a| a.as_str().to_string()), - }); - } - Ok(()) -} - -pub(crate) fn ensure_sent_by_vesting_contract( - info: &MessageInfo, - storage: &dyn Storage, -) -> Result<(), MixnetContractError> { - let vesting_contract_address = - crate::mixnet_contract_settings::storage::vesting_contract_address(storage)?; - if info.sender != vesting_contract_address { - Err(MixnetContractError::SenderIsNotVestingContract { - received: info.sender.clone(), - vesting_contract: vesting_contract_address, - }) - } else { - Ok(()) - } -} - pub(crate) fn ensure_bonded(bond: &MixNodeBond) -> Result<(), MixnetContractError> { if bond.is_unbonding { return Err(MixnetContractError::MixnodeIsUnbonding { @@ -431,3 +255,34 @@ pub(crate) fn decode_ed25519_identity_key( Ok(public_key) } + +pub(crate) fn ensure_profit_margin_within_range( + storage: &dyn Storage, + profit_margin: Percent, +) -> Result<(), MixnetContractError> { + let range = mixnet_params_storage::profit_margin_range(storage)?; + if !range.within_range(profit_margin) { + return Err(MixnetContractError::ProfitMarginOutsideRange { + provided: profit_margin, + range, + }); + } + + Ok(()) +} + +pub fn ensure_operating_cost_within_range( + storage: &dyn Storage, + operating_cost: &Coin, +) -> Result<(), MixnetContractError> { + let range = mixnet_params_storage::interval_oprating_cost_range(storage)?; + if !range.within_range(operating_cost.amount) { + return Err(MixnetContractError::OperatingCostOutsideRange { + denom: operating_cost.denom.clone(), + provided: operating_cost.amount, + range, + }); + } + + Ok(()) +} diff --git a/contracts/mixnet/src/support/tests/messages.rs b/contracts/mixnet/src/support/tests/messages.rs index a1bf5ab37e..61a0815d91 100644 --- a/contracts/mixnet/src/support/tests/messages.rs +++ b/contracts/mixnet/src/support/tests/messages.rs @@ -22,7 +22,7 @@ pub(crate) fn valid_bond_gateway_msg( ..tests::fixtures::gateway_fixture() }; - let msg = gateway_bonding_sign_payload(deps, sender, None, gateway.clone(), stake); + let msg = gateway_bonding_sign_payload(deps, sender, gateway.clone(), stake); let owner_signature = ed25519_sign_message(msg, keypair.private_key()); let identity_key = keypair.public_key().to_base58_string(); diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index 2bd98eb175..dee121b2f0 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -14,8 +14,7 @@ pub mod test_helpers { use crate::delegations::storage as delegations_storage; use crate::delegations::transactions::try_delegate_to_mixnode; use crate::families::transactions::{try_create_family, try_join_family}; - use crate::gateways::storage as gateways_storage; - use crate::gateways::transactions::{try_add_gateway, try_add_gateway_on_behalf}; + use crate::gateways::transactions::try_add_gateway; use crate::interval::transactions::{ perform_pending_epoch_actions, perform_pending_interval_actions, try_begin_epoch_transition, }; @@ -27,9 +26,7 @@ pub mod test_helpers { }; use crate::mixnodes::storage as mixnodes_storage; use crate::mixnodes::storage::mixnode_bonds; - use crate::mixnodes::transactions::{ - try_add_mixnode, try_add_mixnode_on_behalf, try_remove_mixnode, - }; + use crate::mixnodes::transactions::{try_add_mixnode, try_remove_mixnode}; use crate::rewards::queries::{ query_pending_delegator_reward, query_pending_mixnode_operator_reward, }; @@ -45,7 +42,7 @@ pub mod test_helpers { use cosmwasm_std::testing::mock_info; use cosmwasm_std::testing::MockApi; use cosmwasm_std::testing::MockQuerier; - use cosmwasm_std::{coin, coins, Addr, Api, BankMsg, CosmosMsg, Storage}; + use cosmwasm_std::{coin, coins, Addr, BankMsg, CosmosMsg, Storage}; use cosmwasm_std::{Coin, Order}; use cosmwasm_std::{Decimal, Empty, MemoryStorage}; use cosmwasm_std::{Deps, OwnedDeps}; @@ -108,10 +105,11 @@ pub mod test_helpers { let deps = init_contract(); let rewarding_validator_address = rewarding_validator_address(deps.as_ref().storage).unwrap(); - let owner = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_ref().storage) + let owner = mixnet_params_storage::ADMIN + .query_admin(deps.as_ref()) .unwrap() - .owner; + .admin + .unwrap(); TestSetup { deps, @@ -148,13 +146,6 @@ pub mod test_helpers { self.owner.clone() } - pub fn vesting_contract(&self) -> Addr { - mixnet_params_storage::CONTRACT_STATE - .load(self.deps().storage) - .unwrap() - .vesting_contract_address - } - pub fn coin(&self, amount: u128) -> Coin { coin(amount, rewarding_denom(self.deps().storage).unwrap()) } @@ -178,7 +169,6 @@ pub mod test_helpers { &mut self, family_owner_keys: &identity::KeyPair, member_node: IdentityKeyRef, - vesting: bool, ) -> MessageSignature { let identity = family_owner_keys.public_key().to_base58_string(); @@ -195,14 +185,7 @@ pub mod test_helpers { let nonce = signing_storage::get_signing_nonce(self.deps().storage, owner).unwrap(); - let proxy = if vesting { - Some(self.vesting_contract()) - } else { - None - }; - - let msg = - construct_family_join_permit(nonce, family_head, proxy, member_node.to_owned()); + let msg = construct_family_join_permit(nonce, family_head, member_node.to_owned()); let sig_bytes = family_owner_keys .private_key() @@ -217,13 +200,11 @@ pub mod test_helpers { member: &str, member_keys: &identity::KeyPair, head_keys: &identity::KeyPair, - vesting: bool, ) { let member_identity = member_keys.public_key().to_base58_string(); let head_identity = head_keys.public_key().to_base58_string(); - let join_permit = - self.generate_family_join_permit(head_keys, &member_identity, vesting); + let join_permit = self.generate_family_join_permit(head_keys, &member_identity); let family_head = FamilyHead::new(head_identity); try_join_family( @@ -235,12 +216,13 @@ pub mod test_helpers { .unwrap(); } + #[allow(dead_code)] pub fn create_dummy_mixnode_with_new_family( &mut self, head: &str, label: &str, ) -> (MixId, identity::KeyPair) { - let (mix_id, keys) = self.add_dummy_mixnode_with_proxy_and_keypair(head, None); + let (mix_id, keys) = self.add_dummy_mixnode_with_keypair(head, None); try_create_family(self.deps_mut(), mock_info(head, &[]), label.to_string()).unwrap(); (mix_id, keys) @@ -338,7 +320,7 @@ pub mod test_helpers { stake: Option, ) -> MessageSignature { let stake = self.make_mix_pledge(stake); - let msg = mixnode_bonding_sign_payload(self.deps(), owner, None, mixnode, stake); + let msg = mixnode_bonding_sign_payload(self.deps(), owner, mixnode, stake); ed25519_sign_message(msg, key) } @@ -359,13 +341,8 @@ pub mod test_helpers { ..tests::fixtures::mix_node_fixture() }; - let msg = mixnode_bonding_sign_payload( - self.deps(), - owner, - None, - mixnode.clone(), - stake.clone(), - ); + let msg = + mixnode_bonding_sign_payload(self.deps(), owner, mixnode.clone(), stake.clone()); let owner_signature = ed25519_sign_message(msg, keypair.private_key()); let info = mock_info(owner, &stake); @@ -389,156 +366,6 @@ pub mod test_helpers { (current_id_counter + 1, keypair) } - pub fn add_dummy_mixnode_with_proxy_and_keypair( - &mut self, - owner: &str, - stake: Option, - ) -> (MixId, identity::KeyPair) { - let stake = self.make_mix_pledge(stake); - - let proxy = self.vesting_contract(); - - let keypair = identity::KeyPair::new(&mut self.rng); - let identity_key = keypair.public_key().to_base58_string(); - let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut self.rng); - - let mixnode = MixNode { - identity_key, - sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), - ..tests::fixtures::mix_node_fixture() - }; - - let msg = mixnode_bonding_sign_payload( - self.deps(), - owner, - Some(proxy.clone()), - mixnode.clone(), - stake.clone(), - ); - let owner_signature = ed25519_sign_message(msg, keypair.private_key()); - - let info = mock_info(proxy.as_str(), &stake); - let current_id_counter = mixnodes_storage::MIXNODE_ID_COUNTER - .may_load(self.deps().storage) - .unwrap() - .unwrap_or_default(); - - let env = self.env(); - try_add_mixnode_on_behalf( - self.deps_mut(), - env, - info, - mixnode, - tests::fixtures::mix_node_cost_params_fixture(), - owner.to_string(), - owner_signature, - ) - .unwrap(); - - // newly added mixnode gets assigned the current counter + 1 - (current_id_counter + 1, keypair) - } - - pub fn add_dummy_mixnode_with_legal_proxy( - &mut self, - owner: &str, - stake: Option, - ) -> MixId { - self.add_dummy_mixnode_with_proxy_and_keypair(owner, stake) - .0 - } - - pub fn set_illegal_mixnode_proxy(&mut self, mix_id: MixId, proxy: Addr) { - let mut bond_details = mixnodes_storage::mixnode_bonds() - .load(self.deps().storage, mix_id) - .unwrap(); - bond_details.proxy = Some(proxy); - mixnodes_storage::mixnode_bonds() - .save(self.deps_mut().storage, mix_id, &bond_details) - .unwrap(); - } - - pub fn add_dummy_gateway_with_illegal_proxy( - &mut self, - owner: &str, - stake: Option, - proxy: Addr, - ) -> IdentityKey { - let gateway_identity = self.add_dummy_gateway_with_legal_proxy(owner, stake); - self.set_illegal_gateway_proxy(&gateway_identity, proxy); - gateway_identity - } - - pub fn set_illegal_gateway_proxy(&mut self, gateway_id: &str, proxy: Addr) { - let mut gateway = gateways_storage::gateways() - .load(self.deps().storage, gateway_id) - .unwrap(); - gateway.proxy = Some(proxy); - gateways_storage::gateways() - .save(self.deps_mut().storage, gateway_id, &gateway) - .unwrap(); - } - - pub fn add_dummy_gateway_with_legal_proxy( - &mut self, - owner: &str, - stake: Option, - ) -> IdentityKey { - let stake = match stake { - Some(amount) => { - let denom = rewarding_denom(self.deps().storage).unwrap(); - Coin { denom, amount } - } - None => minimum_mixnode_pledge(self.deps.as_ref().storage).unwrap(), - }; - - let keypair = identity::KeyPair::new(&mut self.rng); - let identity_key = keypair.public_key().to_base58_string(); - let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut self.rng); - - let proxy = self.vesting_contract(); - - let gateway = Gateway { - identity_key, - sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), - ..tests::fixtures::gateway_fixture() - }; - - let msg = gateway_bonding_sign_payload( - self.deps(), - owner, - Some(proxy.clone()), - gateway.clone(), - vec![stake.clone()], - ); - let owner_signature = ed25519_sign_message(msg, keypair.private_key()); - - let env = self.env(); - let info = mock_info(proxy.as_ref(), &[stake]); - - try_add_gateway_on_behalf( - self.deps_mut(), - env, - info, - gateway, - owner.to_string(), - owner_signature, - ) - .unwrap(); - keypair.public_key().to_base58_string() - } - - pub fn add_dummy_mixnode_with_illegal_proxy( - &mut self, - owner: &str, - stake: Option, - proxy: Addr, - ) -> MixId { - let mix_id = self.add_dummy_mixnode_with_legal_proxy(owner, stake); - self.set_illegal_mixnode_proxy(mix_id, proxy); - mix_id - } - pub fn mixnode_with_signature( &mut self, sender: &str, @@ -555,8 +382,7 @@ pub mod test_helpers { sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), ..tests::fixtures::mix_node_fixture() }; - let msg = - mixnode_bonding_sign_payload(self.deps(), sender, None, mixnode.clone(), stake); + let msg = mixnode_bonding_sign_payload(self.deps(), sender, mixnode.clone(), stake); let owner_signature = ed25519_sign_message(msg, keypair.private_key()); (mixnode, owner_signature, keypair) @@ -579,8 +405,7 @@ pub mod test_helpers { ..tests::fixtures::gateway_fixture() }; - let msg = - gateway_bonding_sign_payload(self.deps(), sender, None, gateway.clone(), stake); + let msg = gateway_bonding_sign_payload(self.deps(), sender, gateway.clone(), stake); let owner_signature = ed25519_sign_message(msg, keypair.private_key()); (gateway, owner_signature) @@ -625,87 +450,10 @@ pub mod test_helpers { Addr::unchecked(delegator), target, amount, - None, ) .unwrap(); } - pub fn add_immediate_delegation_with_legal_proxy( - &mut self, - delegator: &str, - amount: impl Into, - target: MixId, - ) { - let denom = rewarding_denom(self.deps().storage).unwrap(); - let amount = Coin { - denom, - amount: amount.into(), - }; - let env = self.env(); - let proxy = self.vesting_contract(); - pending_events::delegate( - self.deps_mut(), - &env, - env.block.height, - Addr::unchecked(delegator), - target, - amount, - Some(proxy), - ) - .unwrap(); - } - - // to set illegal proxy we have to bypass "normal" flow and put the value - // directly into the storage - pub fn add_immediate_delegation_with_illegal_proxy( - &mut self, - delegator: &str, - amount: impl Into, - target: MixId, - proxy: Addr, - ) { - let denom = rewarding_denom(self.deps().storage).unwrap(); - let amount = Coin { - denom, - amount: amount.into(), - }; - - let owner = self.deps.api.addr_validate(delegator).unwrap(); - let storage_key = Delegation::generate_storage_key(target, &owner, Some(&proxy)); - - let mut mix_rewarding = self.mix_rewarding(target); - - let mut stored_delegation_amount = amount; - - if let Some(existing_delegation) = delegations_storage::delegations() - .may_load(&self.deps.storage, storage_key.clone()) - .unwrap() - { - let og_with_reward = mix_rewarding.undelegate(&existing_delegation).unwrap(); - stored_delegation_amount.amount += og_with_reward.amount; - } - - mix_rewarding - .add_base_delegation(stored_delegation_amount.amount) - .unwrap(); - - let delegation = Delegation::new( - owner, - target, - mix_rewarding.total_unit_reward, - stored_delegation_amount, - self.env.block.height, - Some(proxy), - ); - - delegations_storage::delegations() - .save(&mut self.deps.storage, storage_key, &delegation) - .unwrap(); - rewards_storage::MIXNODE_REWARDING - .save(&mut self.deps.storage, target, &mix_rewarding) - .unwrap(); - } - #[allow(unused)] pub fn add_delegation( &mut self, @@ -724,14 +472,8 @@ pub mod test_helpers { pub fn remove_immediate_delegation(&mut self, delegator: &str, target: MixId) { let height = self.env.block.height; - pending_events::undelegate( - self.deps_mut(), - height, - Addr::unchecked(delegator), - target, - None, - ) - .unwrap(); + pending_events::undelegate(self.deps_mut(), height, Addr::unchecked(delegator), target) + .unwrap(); } pub fn start_epoch_transition(&mut self) { @@ -1109,7 +851,6 @@ pub mod test_helpers { Addr::unchecked(format!("owner{}", i)), mix_id, tests::fixtures::good_mixnode_pledge().pop().unwrap(), - None, ) .unwrap(); } @@ -1205,7 +946,6 @@ pub mod test_helpers { pub fn mixnode_bonding_sign_payload( deps: Deps<'_>, owner: &str, - proxy: Option, mixnode: MixNode, stake: Vec, ) -> SignableMixNodeBondingMsg { @@ -1214,14 +954,13 @@ pub mod test_helpers { signing_storage::get_signing_nonce(deps.storage, Addr::unchecked(owner)).unwrap(); let payload = MixnodeBondingPayload::new(mixnode, cost_params); - let content = ContractMessageContent::new(Addr::unchecked(owner), proxy, stake, payload); + let content = ContractMessageContent::new(Addr::unchecked(owner), stake, payload); SignableMixNodeBondingMsg::new(nonce, content) } pub fn gateway_bonding_sign_payload( deps: Deps<'_>, owner: &str, - proxy: Option, gateway: Gateway, stake: Vec, ) -> SignableGatewayBondingMsg { @@ -1229,7 +968,7 @@ pub mod test_helpers { signing_storage::get_signing_nonce(deps.storage, Addr::unchecked(owner)).unwrap(); let payload = GatewayBondingPayload::new(gateway); - let content = ContractMessageContent::new(Addr::unchecked(owner), proxy, stake, payload); + let content = ContractMessageContent::new(Addr::unchecked(owner), stake, payload); SignableGatewayBondingMsg::new(nonce, content) } @@ -1258,6 +997,8 @@ pub mod test_helpers { epochs_in_interval: 720, epoch_duration: Duration::from_secs(60 * 60), initial_rewarding_params: initial_rewarding_params(), + profit_margin: Default::default(), + interval_operating_cost: Default::default(), }; let env = mock_env(); let info = mock_info("creator", &[]); diff --git a/contracts/mixnet/src/vesting_migration.rs b/contracts/mixnet/src/vesting_migration.rs new file mode 100644 index 0000000000..3faf826ec0 --- /dev/null +++ b/contracts/mixnet/src/vesting_migration.rs @@ -0,0 +1,95 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::delegations::storage as delegations_storage; +use crate::mixnet_contract_settings::storage as mixnet_params_storage; +use crate::mixnodes::helpers::get_mixnode_details_by_owner; +use crate::mixnodes::storage as mixnodes_storage; +use crate::support::helpers::{ + ensure_bonded, ensure_epoch_in_progress_state, ensure_no_pending_pledge_changes, +}; +use cosmwasm_std::{wasm_execute, DepsMut, MessageInfo, Response}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::{Delegation, MixId}; +use vesting_contract_common::messages::ExecuteMsg as VestingExecuteMsg; + +pub(crate) fn try_migrate_vested_mixnode( + deps: DepsMut<'_>, + info: MessageInfo, +) -> Result { + let mix_details = get_mixnode_details_by_owner(deps.storage, info.sender.clone())?.ok_or( + MixnetContractError::NoAssociatedMixNodeBond { + owner: info.sender.clone(), + }, + )?; + let mix_id = mix_details.mix_id(); + + ensure_epoch_in_progress_state(deps.storage)?; + ensure_no_pending_pledge_changes(&mix_details.pending_changes)?; + ensure_bonded(&mix_details.bond_information)?; + + let Some(proxy) = &mix_details.bond_information.proxy else { + return Err(MixnetContractError::NotAVestingMixnode); + }; + + let vesting_contract = mixnet_params_storage::vesting_contract_address(deps.storage)?; + if proxy != vesting_contract { + return Err(MixnetContractError::ProxyIsNotVestingContract { + received: proxy.clone(), + vesting_contract, + }); + } + + let mut updated_bond = mix_details.bond_information.clone(); + updated_bond.proxy = None; + mixnodes_storage::mixnode_bonds().replace( + deps.storage, + mix_id, + Some(&updated_bond), + Some(&mix_details.bond_information), + )?; + + Ok(Response::new().add_message(wasm_execute( + vesting_contract, + &VestingExecuteMsg::TrackMigratedMixnode { + owner: info.sender.into_string(), + }, + vec![], + )?)) +} + +pub(crate) fn try_migrate_vested_delegation( + deps: DepsMut<'_>, + info: MessageInfo, + mix_id: MixId, +) -> Result { + ensure_epoch_in_progress_state(deps.storage)?; + + let vesting_contract = mixnet_params_storage::vesting_contract_address(deps.storage)?; + + let storage_key = + Delegation::generate_storage_key(mix_id, &info.sender, Some(&vesting_contract)); + let Some(mut delegation) = + delegations_storage::delegations().may_load(deps.storage, storage_key.clone())? + else { + return Err(MixnetContractError::NotAVestingDelegation); + }; + + // sanity check that's meant to blow up the contract + assert_eq!(delegation.proxy, Some(vesting_contract.clone())); + + // update the delegation and save it under the correct storage key + delegation.proxy = None; + let updated_storage_key = Delegation::generate_storage_key(mix_id, &info.sender, None); + delegations_storage::delegations().remove(deps.storage, storage_key)?; + delegations_storage::delegations().save(deps.storage, updated_storage_key, &delegation)?; + + Ok(Response::new().add_message(wasm_execute( + vesting_contract, + &VestingExecuteMsg::TrackMigratedDelegation { + owner: info.sender.into_string(), + mix_id, + }, + vec![], + )?)) +} diff --git a/contracts/name-service/Cargo.toml b/contracts/name-service/Cargo.toml deleted file mode 100644 index 5e60860761..0000000000 --- a/contracts/name-service/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "nym-name-service" -version = "0.1.0" -edition = "2021" - -[[bin]] -name = "schema" -required-features = ["schema-gen"] - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -bs58 = { workspace = true } -cosmwasm-schema = { workspace = true, optional = true } -cosmwasm-std = { workspace = true } -cw-controllers = { workspace = true } -cw-storage-plus = { workspace = true } -cw-utils = { workspace = true } -cw2 = { workspace = true } -nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", version = "0.5.0" } -nym-name-service-common = { path = "../../common/cosmwasm-smart-contracts/name-service" } -serde = { version = "1.0.155", default-features = false, features = ["derive"] } -thiserror = { workspace = true } - -[dev-dependencies] -anyhow = "1.0.40" -cw-multi-test = { workspace = true } -nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } -nym-sphinx-addressing = { path = "../../common/nymsphinx/addressing" } -rand = "0.8.5" -rand_chacha = "0.3" -rstest = "0.17.0" - -[features] -schema-gen = ["nym-name-service-common/schema", "cosmwasm-schema"] diff --git a/contracts/service-provider-directory/Cargo.toml b/contracts/service-provider-directory/Cargo.toml deleted file mode 100644 index 95c001f85b..0000000000 --- a/contracts/service-provider-directory/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "nym-service-provider-directory" -version = "0.1.0" -edition = "2021" - -[[bin]] -name = "schema" -required-features = ["schema-gen"] - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -bs58 = { workspace = true } -cosmwasm-schema = { workspace = true, optional = true } -cosmwasm-std = { workspace = true } -cw-controllers = { workspace = true } -cw-storage-plus = { workspace = true } -cw-utils = { workspace = true } -cw2 = { workspace = true } -nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", version = "0.5.0" } -nym-service-provider-directory-common = { path = "../../common/cosmwasm-smart-contracts/service-provider-directory" } -serde = { version = "1.0.155", default-features = false, features = ["derive"] } -thiserror = { workspace = true } - -[dev-dependencies] -anyhow = "1.0.40" -cw-multi-test = { workspace = true } -nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } -rand_chacha = "0.3" -rstest = "0.17.0" - -[features] -schema-gen = ["nym-service-provider-directory-common/schema", "cosmwasm-schema"] diff --git a/contracts/vesting/schema/nym-vesting-contract.json b/contracts/vesting/schema/nym-vesting-contract.json index 7d016b8e68..1a7b292784 100644 --- a/contracts/vesting/schema/nym-vesting-contract.json +++ b/contracts/vesting/schema/nym-vesting-contract.json @@ -691,6 +691,54 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "track_migrated_mixnode" + ], + "properties": { + "track_migrated_mixnode": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "track_migrated_delegation" + ], + "properties": { + "track_migrated_delegation": { + "type": "object", + "required": [ + "mix_id", + "owner" + ], + "properties": { + "mix_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": { diff --git a/contracts/vesting/schema/raw/execute.json b/contracts/vesting/schema/raw/execute.json index be6d6a73ff..9723294d67 100644 --- a/contracts/vesting/schema/raw/execute.json +++ b/contracts/vesting/schema/raw/execute.json @@ -669,6 +669,54 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "track_migrated_mixnode" + ], + "properties": { + "track_migrated_mixnode": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "track_migrated_delegation" + ], + "properties": { + "track_migrated_delegation": { + "type": "object", + "required": [ + "mix_id", + "owner" + ], + "properties": { + "mix_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": { diff --git a/contracts/vesting/src/contract.rs b/contracts/vesting/src/contract.rs index a9689e3390..a9ef9401a6 100644 --- a/contracts/vesting/src/contract.rs +++ b/contracts/vesting/src/contract.rs @@ -70,55 +70,12 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { - ExecuteMsg::CreateFamily { label } => try_create_family(info, deps, label), - ExecuteMsg::JoinFamily { - join_permit, - family_head, - } => try_join_family(info, deps, join_permit, family_head), - ExecuteMsg::LeaveFamily { family_head } => try_leave_family(info, deps, family_head), - ExecuteMsg::KickFamilyMember { member } => try_kick_family_member(info, deps, member), - ExecuteMsg::UpdateLockedPledgeCap { address, cap } => { - try_update_locked_pledge_cap(address, cap, info, deps) - } ExecuteMsg::TrackReward { amount, address } => { try_track_reward(deps, info, amount, &address) } - ExecuteMsg::ClaimOperatorReward {} => try_claim_operator_reward(deps, info), - ExecuteMsg::ClaimDelegatorReward { mix_id } => { - try_claim_delegator_reward(deps, info, mix_id) - } - ExecuteMsg::UpdateMixnodeConfig { new_config } => { - try_update_mixnode_config(new_config, info, deps) - } - ExecuteMsg::UpdateMixnodeCostParams { new_costs } => { - try_update_mixnode_cost_params(new_costs, info, deps) - } ExecuteMsg::UpdateMixnetAddress { address } => { try_update_mixnet_address(address, info, deps) } - ExecuteMsg::DelegateToMixnode { - mix_id, - amount, - on_behalf_of, - } => try_delegate_to_mixnode(mix_id, amount, on_behalf_of, info, env, deps), - ExecuteMsg::UndelegateFromMixnode { - mix_id, - on_behalf_of, - } => try_undelegate_from_mixnode(mix_id, on_behalf_of, info, deps), - ExecuteMsg::CreateAccount { - owner_address, - staking_address, - vesting_spec, - cap, - } => try_create_periodic_vesting_account( - &owner_address, - staking_address, - vesting_spec, - cap, - info, - env, - deps, - ), ExecuteMsg::WithdrawVestedCoins { amount } => { try_withdraw_vested_coins(amount, env, info, deps) } @@ -127,47 +84,22 @@ pub fn execute( mix_id, amount, } => try_track_undelegation(&owner, mix_id, amount, info, deps), - ExecuteMsg::BondMixnode { - mix_node, - cost_params, - owner_signature, - amount, - } => try_bond_mixnode( - mix_node, - cost_params, - owner_signature, - amount, - info, - env, - deps, - ), - ExecuteMsg::PledgeMore { amount } => try_pledge_more(deps, env, info, amount), - ExecuteMsg::DecreasePledge { amount } => try_decrease_pledge(deps, info, amount), - ExecuteMsg::UnbondMixnode {} => try_unbond_mixnode(info, deps), ExecuteMsg::TrackUnbondMixnode { owner, amount } => { try_track_unbond_mixnode(&owner, amount, info, deps) } ExecuteMsg::TrackDecreasePledge { owner, amount } => { try_track_decrease_mixnode_pledge(&owner, amount, info, deps) } - ExecuteMsg::BondGateway { - gateway, - owner_signature, - amount, - } => try_bond_gateway(gateway, owner_signature, amount, info, env, deps), - ExecuteMsg::UnbondGateway {} => try_unbond_gateway(info, deps), ExecuteMsg::TrackUnbondGateway { owner, amount } => { try_track_unbond_gateway(&owner, amount, info, deps) } - ExecuteMsg::UpdateGatewayConfig { new_config } => { - try_update_gateway_config(new_config, info, deps) - } - ExecuteMsg::TransferOwnership { to_address } => { - try_transfer_ownership(to_address, info, deps) - } - ExecuteMsg::UpdateStakingAddress { to_address } => { - try_update_staking_address(to_address, info, deps) + ExecuteMsg::TrackMigratedMixnode { owner } => try_track_migrate_mixnode(&owner, info, deps), + ExecuteMsg::TrackMigratedDelegation { owner, mix_id } => { + try_track_migrate_delegation(&owner, mix_id, info, deps) } + _ => Err(VestingContractError::Other { + message: "the contract has been disabled".to_string(), + }), } } diff --git a/contracts/vesting/src/traits/bonding_account.rs b/contracts/vesting/src/traits/bonding_account.rs index e5e415e30c..8e4c7c6a62 100644 --- a/contracts/vesting/src/traits/bonding_account.rs +++ b/contracts/vesting/src/traits/bonding_account.rs @@ -61,6 +61,10 @@ pub trait MixnodeBondingAccount { new_costs: MixNodeCostParams, storage: &mut dyn Storage, ) -> Result; + fn try_track_migrated_mixnode( + &self, + storage: &mut dyn Storage, + ) -> Result<(), VestingContractError>; } pub trait GatewayBondingAccount { diff --git a/contracts/vesting/src/traits/delegating_account.rs b/contracts/vesting/src/traits/delegating_account.rs index 629cfee050..2e7b5a65d3 100644 --- a/contracts/vesting/src/traits/delegating_account.rs +++ b/contracts/vesting/src/traits/delegating_account.rs @@ -44,4 +44,9 @@ pub trait DelegatingAccount { amount: Coin, storage: &mut dyn Storage, ) -> Result<(), VestingContractError>; + fn track_migrated_delegation( + &self, + mix_id: MixId, + storage: &mut dyn Storage, + ) -> Result<(), VestingContractError>; } diff --git a/contracts/vesting/src/transactions.rs b/contracts/vesting/src/transactions.rs index adbfe39436..a51f084862 100644 --- a/contracts/vesting/src/transactions.rs +++ b/contracts/vesting/src/transactions.rs @@ -18,8 +18,9 @@ use mixnet_contract_common::{ use vesting_contract_common::events::{ new_ownership_transfer_event, new_periodic_vesting_account_event, new_staking_address_update_event, new_track_gateway_unbond_event, - new_track_mixnode_pledge_decrease_event, new_track_mixnode_unbond_event, - new_track_reward_event, new_track_undelegation_event, new_vested_coins_withdraw_event, + new_track_migrate_mixnode_event, new_track_mixnode_pledge_decrease_event, + new_track_mixnode_unbond_event, new_track_reward_event, new_track_undelegation_event, + new_vested_coins_withdraw_event, }; use vesting_contract_common::{Account, PledgeCap, VestingContractError, VestingSpecification}; @@ -255,6 +256,35 @@ pub fn try_track_unbond_gateway( Ok(Response::new().add_event(new_track_gateway_unbond_event())) } +/// Track vesting mixnode being converted into the usage of liquid tokens. invoked by the mixnet contract after successful migration. +pub fn try_track_migrate_mixnode( + owner: &str, + info: MessageInfo, + deps: DepsMut<'_>, +) -> Result { + if info.sender != MIXNET_CONTRACT_ADDRESS.load(deps.storage)? { + return Err(VestingContractError::NotMixnetContract(info.sender)); + } + let account = account_from_address(owner, deps.storage, deps.api)?; + account.try_track_migrated_mixnode(deps.storage)?; + Ok(Response::new().add_event(new_track_migrate_mixnode_event())) +} + +/// Track vesting delegation being converted into the usage of liquid tokens. invoked by the mixnet contract after successful migration. +pub fn try_track_migrate_delegation( + owner: &str, + mix_id: MixId, + info: MessageInfo, + deps: DepsMut<'_>, +) -> Result { + if info.sender != MIXNET_CONTRACT_ADDRESS.load(deps.storage)? { + return Err(VestingContractError::NotMixnetContract(info.sender)); + } + let account = account_from_address(owner, deps.storage, deps.api)?; + account.track_migrated_delegation(mix_id, deps.storage)?; + Ok(Response::new().add_event(new_track_migrate_mixnode_event())) +} + /// Bond a mixnode, sends [mixnet_contract_common::ExecuteMsg::BondMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. pub fn try_bond_mixnode( mix_node: MixNode, diff --git a/contracts/vesting/src/vesting/account/delegating_account.rs b/contracts/vesting/src/vesting/account/delegating_account.rs index 0371bc7cc3..d01bea6789 100644 --- a/contracts/vesting/src/vesting/account/delegating_account.rs +++ b/contracts/vesting/src/vesting/account/delegating_account.rs @@ -125,4 +125,25 @@ impl DelegatingAccount for Account { self.save_balance(new_balance, storage)?; Ok(()) } + + fn track_migrated_delegation( + &self, + mix_id: MixId, + storage: &mut dyn Storage, + ) -> Result<(), VestingContractError> { + let delegation = self.total_delegations_for_mix(mix_id, storage)?; + if delegation.is_zero() { + return Err(VestingContractError::NoSuchDelegation( + self.owner_address.clone(), + mix_id, + )); + } + + // treat the tokens that were used for delegation as 'withdrawn' + let current_withdrawn = self.load_withdrawn(storage)?; + self.save_withdrawn(current_withdrawn + delegation, storage)?; + + // remove the delegation data since it no longer belongs to the vesting contract + self.remove_delegations_for_mix(mix_id, storage) + } } diff --git a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs index 29ee780ff9..ee14207d3e 100644 --- a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs +++ b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs @@ -221,4 +221,24 @@ impl MixnodeBondingAccount for Account { .add_message(update_mixnode_costs_msg) .add_event(new_vesting_update_mixnode_cost_params_event())) } + + fn try_track_migrated_mixnode( + &self, + storage: &mut dyn Storage, + ) -> Result<(), VestingContractError> { + let Some(pledge) = self.load_mixnode_pledge(storage)? else { + return Err(VestingContractError::NoBondFound( + self.owner_address().as_str().to_string(), + )); + }; + + // treat the tokens that were used for bonding as 'withdrawn' + let current_withdrawn = self.load_withdrawn(storage)?; + self.save_withdrawn(current_withdrawn + pledge.amount.amount, storage)?; + + // don't change the balance as the tokens are left in the mixnet contract + + // remove the pledge data since it no longer belongs to the vesting account + self.remove_mixnode_pledge(storage) + } } diff --git a/contracts/vesting/src/vesting/mod.rs b/contracts/vesting/src/vesting/mod.rs index b16efbdd23..d8da405008 100644 --- a/contracts/vesting/src/vesting/mod.rs +++ b/contracts/vesting/src/vesting/mod.rs @@ -13,14 +13,14 @@ pub fn populate_vesting_periods( #[cfg(test)] mod tests { use crate::contract::*; - use crate::storage::*; + use crate::support::tests::helpers::vesting_account_percent_fixture; use crate::support::tests::helpers::{ init_contract, vesting_account_mid_fixture, vesting_account_new_fixture, TEST_COIN_DENOM, }; use crate::traits::DelegatingAccount; + use crate::traits::GatewayBondingAccount; use crate::traits::VestingAccount; - use crate::traits::{GatewayBondingAccount, MixnodeBondingAccount}; use crate::vesting::account::StorableVestingAccountExt; use crate::vesting::populate_vesting_periods; use contracts_common::signing::MessageSignature; @@ -36,162 +36,56 @@ mod tests { fn test_account_creation() { let mut deps = init_contract(); let env = mock_env(); - let info = mock_info("not_admin", &coins(1_000_000_000_000, TEST_COIN_DENOM)); + let msg = ExecuteMsg::CreateAccount { owner_address: "owner".to_string(), staking_address: Some("staking".to_string()), vesting_spec: None, cap: Some(PledgeCap::Absolute(Uint128::from(100_000_000_000u128))), }; - // Try creating an account when not admin - let response = execute(deps.as_mut(), env.clone(), info, msg.clone()); - assert!(response.is_err()); let info = mock_info("admin", &coins(1_000_000_000_000, TEST_COIN_DENOM)); - let _response = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()); - let created_account = load_account(Addr::unchecked("owner"), &deps.storage) - .unwrap() - .unwrap(); - + let response = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()); assert_eq!( - created_account.load_balance(&deps.storage).unwrap(), - // One was liquidated - Uint128::new(1_000_000_000_000) + response, + Err(VestingContractError::Other { + message: "the contract has been disabled".to_string() + }) ); - - // nothing is saved for "staking" account! - let created_account_test_by_staking = - load_account(Addr::unchecked("staking"), &deps.storage).unwrap(); - assert!(created_account_test_by_staking.is_none()); - - // but we can stake on its behalf! - let stake_msg = ExecuteMsg::DelegateToMixnode { - on_behalf_of: Some("owner".to_string()), - mix_id: 42, - amount: coin(500, TEST_COIN_DENOM), - }; - - let response = execute( - deps.as_mut(), - env.clone(), - mock_info("staking", &[]), - stake_msg, - ); - assert!(response.is_ok()); - - assert_eq!( - created_account.load_balance(&deps.storage).unwrap(), - // One was liquidated - Uint128::new(999_999_999_500) - ); - - // Try create the same account again - let response = execute(deps.as_mut(), env.clone(), info, msg); - assert!(response.is_err()); - - let account_again = vesting_account_new_fixture(&mut deps.storage, &env); - assert_eq!(created_account.storage_key(), 1); - assert_ne!(created_account.storage_key(), account_again.storage_key()); } #[test] fn test_ownership_transfer() { let mut deps = init_contract(); - let mut env = mock_env(); + let env = mock_env(); let info = mock_info("owner", &[]); - let account = vesting_account_new_fixture(&mut deps.storage, &env); - let staker = account.staking_address().unwrap(); let msg = ExecuteMsg::TransferOwnership { to_address: "new_owner".to_string(), }; - let _response = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()).unwrap(); - let new_owner_account = load_account(Addr::unchecked("new_owner"), &deps.storage) - .unwrap() - .unwrap(); + let response = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()); assert_eq!( - new_owner_account.load_balance(&deps.storage), - account.load_balance(&deps.storage) + response, + Err(VestingContractError::Other { + message: "the contract has been disabled".to_string() + }) ); - - // Check old account is gone - let old_owner_account = load_account(Addr::unchecked("owner"), &deps.storage).unwrap(); - assert!(old_owner_account.is_none()); - - // Not the owner - let response = execute(deps.as_mut(), env.clone(), info, msg); - assert!(response.is_err()); - - // can't stake on behalf of the original owner anymore, but we can do it for the new one! - let stake_msg = ExecuteMsg::DelegateToMixnode { - on_behalf_of: Some("owner".to_string()), - mix_id: 42, - amount: coin(500, TEST_COIN_DENOM), - }; - let response = execute( - deps.as_mut(), - env.clone(), - mock_info(staker.as_ref(), &[]), - stake_msg, - ); - assert!(response.is_err()); - - let new_stake_msg = ExecuteMsg::DelegateToMixnode { - on_behalf_of: Some("new_owner".to_string()), - mix_id: 42, - amount: coin(500, TEST_COIN_DENOM), - }; - let response = execute( - deps.as_mut(), - env.clone(), - mock_info(staker.as_ref(), &[]), - new_stake_msg, - ); - assert!(response.is_ok()); - - let info = mock_info("new_owner", &[]); - let msg = ExecuteMsg::UpdateStakingAddress { - to_address: Some("new_staking".to_string()), - }; - let _response = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); - - let msg = ExecuteMsg::WithdrawVestedCoins { - amount: Coin { - amount: Uint128::new(1), - denom: TEST_COIN_DENOM.to_string(), - }, - }; - let info = mock_info("new_owner", &[]); - env.block.time = Timestamp::from_nanos(env.block.time.nanos() + 100_000_000_000_000_000); - let response = execute(deps.as_mut(), env.clone(), info, msg.clone()); - assert!(response.is_ok()); - - let info = mock_info("owner", &[]); - let response = execute(deps.as_mut(), env.clone(), info, msg); - assert!(response.is_err()); } #[test] fn test_staking_account() { let mut deps = init_contract(); - let mut env = mock_env(); + let env = mock_env(); let info = mock_info("staking", &[]); let msg = ExecuteMsg::TransferOwnership { to_address: "new_owner".to_string(), }; let response = execute(deps.as_mut(), env.clone(), info.clone(), msg); - // Only owner can transfer - assert!(response.is_err()); - - let msg = ExecuteMsg::WithdrawVestedCoins { - amount: Coin { - amount: Uint128::new(1), - denom: "nym".to_string(), - }, - }; - env.block.time = Timestamp::from_nanos(env.block.time.nanos() + 100_000_000_000_000_000); - let response = execute(deps.as_mut(), env, info, msg); - // Only owner can withdraw - assert!(response.is_err()); + assert_eq!( + response, + Err(VestingContractError::Other { + message: "the contract has been disabled".to_string() + }) + ); } #[test] @@ -213,31 +107,12 @@ mod tests { mock_info(original_staker.as_ref(), &[]), stake_msg.clone(), ); - assert!(response.is_ok()); - - let info = mock_info("owner", &[]); - let msg = ExecuteMsg::UpdateStakingAddress { - to_address: Some("new_staking".to_string()), - }; - let _response = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); - - // the old staking account can't do any staking anymore! - let response = execute( - deps.as_mut(), - env.clone(), - mock_info(original_staker.as_ref(), &[]), - stake_msg.clone(), + assert_eq!( + response, + Err(VestingContractError::Other { + message: "the contract has been disabled".to_string() + }) ); - assert!(response.is_err()); - - // but the new one can - let response = execute( - deps.as_mut(), - env.clone(), - mock_info("new_staking", &[]), - stake_msg, - ); - assert!(response.is_ok()); } #[test] @@ -245,66 +120,27 @@ mod tests { let mut deps = init_contract(); let env = mock_env(); - let amount1 = coin(1000000000, "unym"); - let amount2 = coin(100, "unym"); + let amount = coin(1000000000, "unym"); // create the accounts - let msg1 = ExecuteMsg::CreateAccount { + let msg = ExecuteMsg::CreateAccount { owner_address: "vesting1".to_string(), staking_address: None, vesting_spec: None, cap: None, }; - let res1 = execute( + let response = execute( deps.as_mut(), env.clone(), - mock_info("admin", &[amount1.clone()]), - msg1, - ); - assert!(res1.is_ok()); - - let msg2 = ExecuteMsg::CreateAccount { - owner_address: "vesting2".to_string(), - staking_address: None, - vesting_spec: None, - cap: None, - }; - let res2 = execute( - deps.as_mut(), - env.clone(), - mock_info("admin", &[amount2.clone()]), - msg2, - ); - assert!(res2.is_ok()); - - let vesting1 = try_get_vesting_coins("vesting1", None, env.clone(), deps.as_ref()).unwrap(); - assert_eq!(vesting1, amount1); - - let vesting2 = try_get_vesting_coins("vesting2", None, env.clone(), deps.as_ref()).unwrap(); - assert_eq!(vesting2, amount2); - - let staking_address_change = ExecuteMsg::UpdateStakingAddress { - to_address: Some("vesting1".to_string()), - }; - let res = execute( - deps.as_mut(), - env.clone(), - mock_info("vesting2", &[]), - staking_address_change, + mock_info("admin", &[amount.clone()]), + msg, ); assert_eq!( - Err(VestingContractError::StakingAccountExists( - "vesting1".to_string() - )), - res + response, + Err(VestingContractError::Other { + message: "the contract has been disabled".to_string() + }) ); - - // ensure nothing has changed! - let vesting1 = try_get_vesting_coins("vesting1", None, env.clone(), deps.as_ref()).unwrap(); - assert_eq!(vesting1, amount1); - - let vesting2 = try_get_vesting_coins("vesting2", None, env, deps.as_ref()).unwrap(); - assert_eq!(vesting2, amount2); } #[test] @@ -566,63 +402,13 @@ mod tests { }; let info = mock_info("admin", &coins(1_000_000_000_000, TEST_COIN_DENOM)); - let _response = execute(deps.as_mut(), env.clone(), info, msg); - let account = load_account(Addr::unchecked("owner"), &deps.storage) - .unwrap() - .unwrap(); - - // Try delegating too much - let err = account.try_delegate_to_mixnode( - 1, - Coin { - amount: Uint128::new(1_000_000_000_001), - denom: TEST_COIN_DENOM.to_string(), - }, - &env, - &mut deps.storage, + let response = execute(deps.as_mut(), env.clone(), info, msg); + assert_eq!( + response, + Err(VestingContractError::Other { + message: "the contract has been disabled".to_string() + }) ); - assert!(err.is_err()); - - let ok = account.try_delegate_to_mixnode( - 1, - Coin { - amount: Uint128::new(90_000_000_000), - denom: TEST_COIN_DENOM.to_string(), - }, - &env, - &mut deps.storage, - ); - assert!(ok.is_ok()); - - // Fails due to delegation locked delegation cap - let ok = account.try_delegate_to_mixnode( - 1, - Coin { - amount: Uint128::new(20_000_000_000), - denom: TEST_COIN_DENOM.to_string(), - }, - &env, - &mut deps.storage, - ); - assert!(ok.is_err()); - - let balance = account.load_balance(&deps.storage).unwrap(); - assert_eq!(balance, Uint128::new(910000000000)); - - // Try delegating too much againcalca - let err = account.try_delegate_to_mixnode( - 1, - Coin { - amount: Uint128::new(500_000_000_001), - denom: TEST_COIN_DENOM.to_string(), - }, - &env, - &mut deps.storage, - ); - assert!(err.is_err()); - - let total_delegations = account.total_delegations_for_mix(1, &deps.storage).unwrap(); - assert_eq!(Uint128::new(90_000_000_000), total_delegations); } #[test] @@ -649,52 +435,24 @@ mod tests { amount: Uint128::new(40), }, }; - // Try delegating too much - let err = account.try_bond_mixnode( - mix_node.clone(), - cost_params.clone(), - MessageSignature::from(vec![1, 2, 3]), - Coin { - amount: Uint128::new(1_000_000_000_001), - denom: TEST_COIN_DENOM.to_string(), - }, - &env, - &mut deps.storage, - ); - assert!(err.is_err()); - let ok = account.try_bond_mixnode( - mix_node.clone(), - cost_params.clone(), - MessageSignature::from(vec![1, 2, 3]), - Coin { + let msg = ExecuteMsg::BondMixnode { + mix_node, + cost_params, + owner_signature: vec![1, 2, 3, 4].into(), + amount: Coin { amount: Uint128::new(90_000_000_000), denom: TEST_COIN_DENOM.to_string(), }, - &env, - &mut deps.storage, + }; + let info = mock_info(account.owner_address.as_str(), &[]); + let response = execute(deps.as_mut(), env.clone(), info, msg); + assert_eq!( + response, + Err(VestingContractError::Other { + message: "the contract has been disabled".to_string() + }) ); - assert!(ok.is_ok()); - - let balance = account.load_balance(&deps.storage).unwrap(); - assert_eq!(balance, Uint128::new(910_000_000_000)); - - // Try delegating too much again - let err = account.try_bond_mixnode( - mix_node, - cost_params, - MessageSignature::from(vec![1, 2, 3]), - Coin { - amount: Uint128::new(10_000_000_001), - denom: TEST_COIN_DENOM.to_string(), - }, - &env, - &mut deps.storage, - ); - assert!(err.is_err()); - - let pledge = account.load_mixnode_pledge(&deps.storage).unwrap().unwrap(); - assert_eq!(Uint128::new(90_000_000_000), pledge.amount().amount); } #[test] diff --git a/deny.toml b/deny.toml index 812b1e6e1d..317455647c 100644 --- a/deny.toml +++ b/deny.toml @@ -74,9 +74,9 @@ notice = "warn" # A list of advisory IDs to ignore. Note that ignored advisories will still # output a note when they are encountered. ignore = [ - "RUSTSEC-2020-0159", - "RUSTSEC-2020-0071", - "RUSTSEC-2021-0145", + "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 @@ -115,6 +115,7 @@ allow = [ "Unicode-DFS-2016", "OpenSSL", "Zlib", + "Unicode-3.0", ] # List of explicitly disallowed licenses # See https://spdx.org/licenses/ for list of possible licenses @@ -217,7 +218,7 @@ deny = [ # Wrapper crates can optionally be specified to allow the crate when it # is a direct dependency of the otherwise banned crate #{ name = "ansi_term", version = "=0.11.0", wrappers = [] }, - { name = "openssl"}, + { name = "openssl" }, ] # List of features to allow/deny diff --git a/documentation/dev-portal/themes/index.hbs b/documentation/dev-portal/themes/index.hbs index 02e0a4ffeb..d0bb54d0c3 100644 --- a/documentation/dev-portal/themes/index.hbs +++ b/documentation/dev-portal/themes/index.hbs @@ -164,10 +164,9 @@ - + - diff --git a/documentation/docs/themes/index.hbs b/documentation/docs/themes/index.hbs index 02e0a4ffeb..d0bb54d0c3 100644 --- a/documentation/docs/themes/index.hbs +++ b/documentation/docs/themes/index.hbs @@ -164,10 +164,9 @@ - + - diff --git a/documentation/operators/src/nodes/vps-setup.md b/documentation/operators/src/nodes/vps-setup.md index a6166b497c..1594d6a147 100644 --- a/documentation/operators/src/nodes/vps-setup.md +++ b/documentation/operators/src/nodes/vps-setup.md @@ -58,6 +58,10 @@ To install a full node from scratch, refer to the [validator setup guide](valida Before node or validator setup, the VPS needs to be configured and tested, to verify your connectivity and make sure that your provider wasn't dishonest with the offered services. +```admonish info +The commands listed in this chapter must be executed with a prefix `sudo` or from a root shell. +``` + ### Install Dependencies SSH to your server as `root` or become one running `sudo -i` or `su`. If you prefer to administrate your VPS from a user environment, supply the commands with prefix `sudo`. @@ -96,13 +100,13 @@ ufw status 2. Open all needed ports to have your firewall working correctly: ```sh # for nym-node -ufw allow 1789,1790,8000,9000,9001,22/tcp +ufw allow 1789,1790,8080,9000,9001,22/tcp # in case of planning to setup a WSS (for Gateway functionality) ufw allow 9001/tcp # in case of reverse proxy for the swagger page (for Gateway optionality) -ufw allow 8080,80,443/tcp +ufw allow 80,443/tcp # for validator ufw allow 1317,26656,26660,22,80,443/tcp @@ -223,7 +227,7 @@ All node-specific port configuration can be found in `$HOME/.nym//Academic Papers - + - diff --git a/envs/canary.env b/envs/canary.env index 1c38b89a1a..c4aac15082 100644 --- a/envs/canary.env +++ b/envs/canary.env @@ -2,7 +2,7 @@ CONFIGURED=true RUST_LOG=info RUST_BACKTRACE=1 -NETWORK_NAME=sandbox +NETWORK_NAME=canary BECH32_PREFIX=n MIX_DENOM=unym MIX_DENOM_DISPLAY=nym @@ -12,7 +12,8 @@ DENOMS_EXPONENT=6 REWARDING_VALIDATOR_ADDRESS=n1duuyj2th2y0z4u4f4wtljpdz9s3pxtu0xx6zdz MIXNET_CONTRACT_ADDRESS=n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g -COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1mf6ptkssddfmxvhdx0ech0k03ktp6kf9yk59renau2gvht3nq2gqt5tdrk +VESTING_CONTRACT_ADDRESS=n1780emnrt7v9uqx5txhpxc0z8zfawq0czjmtd4q2maz83cckwjlfsx45u6c +ECASH_CONTRACT_ADDRESS=n14jf8r2kxlymgjkp3xj8xuyud7h974648qdn9tsgsj0pascmntt9s3lvlwm GROUP_CONTRACT_ADDRESS=n1qg5ega6dykkxc307y25pecuufrjkxkaggkkxh7nad0vhyhtuhw3sa07c47 MULTISIG_CONTRACT_ADDRESS=n1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqx5a364 COCONUT_DKG_CONTRACT_ADDRESS=n1aakfpghcanxtc45gpqlx8j3rq0zcpyf49qmhm9mdjrfx036h4z5sy2vfh9 diff --git a/envs/local.env b/envs/local.env index 11338f12b4..09dd5ef227 100644 --- a/envs/local.env +++ b/envs/local.env @@ -13,7 +13,7 @@ STAKE_DENOM_DISPLAY=nyx DENOMS_EXPONENT=6 MIXNET_CONTRACT_ADDRESS=n1xsml6tm6pnx8nsuz3a54cznh55g6nuwu9ug9t92ucjkq5ewp5w9syxeqsv VESTING_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav -COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1lp5zex6685kd22agzskhqsylpnssxnweyuvsz4edr4p4ta92qf3q3dhmsx +ECASH_CONTRACT_ADDRESS=n1lp5zex6685kd22agzskhqsylpnssxnweyuvsz4edr4p4ta92qf3q3dhmsx GROUP_CONTRACT_ADDRESS=n1rw8fw2mpcpzzq3jpa4e52ufawnmj5a4u68p35umvgskewuw0nlzsaa5w4m MULTISIG_CONTRACT_ADDRESS=n1rw8fw2mpcpzzq3jpa4e52ufawnmj5a4u68p35umvgskewuw0nlzsaa5w4m COCONUT_DKG_CONTRACT_ADDRESS=n1vwtgazgpancsfel04y7syc95ausmat47cjtldqzkdmx6phyrwx2qvkv32p diff --git a/envs/mainnet.env b/envs/mainnet.env index 04aa7f1a49..e40e54a471 100644 --- a/envs/mainnet.env +++ b/envs/mainnet.env @@ -15,6 +15,7 @@ DENOMS_EXPONENT=6 MIXNET_CONTRACT_ADDRESS=n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr VESTING_CONTRACT_ADDRESS=n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw GROUP_CONTRACT_ADDRESS=n1e2zq4886zzewpvpucmlw8v9p7zv692f6yck4zjzxh699dkcmlrfqk2knsr +ECASH_CONTRACT_ADDRESS=n1r7s6aksyc6pqardx88k3rkgfagwvj4z4zum9mmz2sfk3zm2mha0sd4dnun MULTISIG_CONTRACT_ADDRESS=n1txayqfz5g9qww3rlflpg025xd26m9payz96u54x4fe3s2ktz39xqk67gzx COCONUT_DKG_CONTRACT_ADDRESS=n19604yflqggs9mk2z26mqygq43q2kr3n932egxx630svywd5mpxjsztfpvx @@ -24,3 +25,4 @@ NYXD="https://rpc.nymtech.net" NYM_API="https://validator.nymtech.net/api/" NYXD_WS="wss://rpc.nymtech.net/websocket" EXPLORER_API="https://explorer.nymtech.net/api/" +NYM_VPN_API="https://nymvpn.com/api" diff --git a/envs/qa.env b/envs/qa.env index f889c60b81..781d4ef19f 100644 --- a/envs/qa.env +++ b/envs/qa.env @@ -11,7 +11,7 @@ STAKE_DENOM_DISPLAY=nyx DENOMS_EXPONENT=6 MIXNET_CONTRACT_ADDRESS=n1hm4y6fzgxgu688jgf7ek66px6xkrtmn3gyk8fax3eawhp68c2d5qujz296 -COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n14y2x8a60knc5jjfeztt84kw8x8l5pwdgnqg256v0p9v4p7t2q6eswxyusw +ECASH_CONTRACT_ADDRESS=n14y2x8a60knc5jjfeztt84kw8x8l5pwdgnqg256v0p9v4p7t2q6eswxyusw GROUP_CONTRACT_ADDRESS=n1qp35fcj0v9u3trhaps5v9q0lc42t4m6aty2wryss75ee8zuqnsqqdcreyq MULTISIG_CONTRACT_ADDRESS=n1qa4hswlcjmttulj0q9qa46jf64f93pecl6tydcsjldfe0hy5ju0sdmwzya COCONUT_DKG_CONTRACT_ADDRESS=n1ayrk6wp6w5lf6njtnfjwljmtcc9vevv5sxwkz7uq24rp2pw67t0qhmmxdd diff --git a/envs/sandbox.env b/envs/sandbox.env index 90dc8d7e7c..4763269a6f 100644 --- a/envs/sandbox.env +++ b/envs/sandbox.env @@ -13,11 +13,10 @@ DENOMS_EXPONENT=6 REWARDING_VALIDATOR_ADDRESS=n1pefc2utwpy5w78p2kqdsfmpjxfwmn9d39k5mqa MIXNET_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav VESTING_CONTRACT_ADDRESS=n1unyuj8qnmygvzuex3dwmg9yzt9alhvyeat0uu0jedg2wj33efl5qackslz -COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1ljlwey4xdj0zs7zueepc48nkr033fca6fjgvurfvttqegm8dvsrswsul70 -GROUP_CONTRACT_ADDRESS=n10v3rjnq4cjyccfykyams68ztce337gksuu6f0lvtl4meuwvkewaqru4uav -MULTISIG_CONTRACT_ADDRESS=n1cemnu8as0ls45v3caunpesl8jlsfw2ff9rlwnltlecp7zrxct4dsqc2y42 -COCONUT_DKG_CONTRACT_ADDRESS=n1zx96qgd88vqlzcxkpwzks7kqs5ctrx36xtzfc58p7q6c4ng9anlqzc4nh8 - +GROUP_CONTRACT_ADDRESS=n1ewmwz97xm0h8rdk8sw7h9mwn866qkx9hl9zlmagqfkhuzvwk5hhq844ue9 +MULTISIG_CONTRACT_ADDRESS=n1tz0setr8vkh9udp8xyxgpqc89ns27k4d0jx2h942hr0ax63yjhmqz6xct8 +COCONUT_DKG_CONTRACT_ADDRESS=n1v3n2ly2dp3a9ng3ff6rh26yfkn0pc5hed7w2shc5u9ca5c865utqj5elvh +ECASH_CONTRACT_ADDRESS=n1v3vydvs2ued84yv3khqwtgldmgwn0elljsdh08dr5s2j9x4rc5fs9jlwz9 STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0" EXPLORER_API=https://sandbox-explorer.nymtech.net/api diff --git a/explorer-api/.env.dev b/explorer-api/.env.dev index 61113ab13d..f332cff85e 100644 --- a/explorer-api/.env.dev +++ b/explorer-api/.env.dev @@ -13,11 +13,10 @@ DENOMS_EXPONENT=6 REWARDING_VALIDATOR_ADDRESS=n1pefc2utwpy5w78p2kqdsfmpjxfwmn9d39k5mqa MIXNET_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav VESTING_CONTRACT_ADDRESS=n1unyuj8qnmygvzuex3dwmg9yzt9alhvyeat0uu0jedg2wj33efl5qackslz -BANDWIDTH_CLAIM_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 -COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n16a32stm6kknhq5cc8rx77elr66pygf2hfszw7wvpq746x3uffylqkjar4l -GROUP_CONTRACT_ADDRESS=n1pd7kfgvr5tpcv0xnlv46c4jsq9jg2r799xxrcwqdm4l2jhq2pjwqrmz5ju -MULTISIG_CONTRACT_ADDRESS=n14ph4e660eyqz0j36zlkaey4zgzexm5twkmjlqaequxr2cjm9eprqsmad6k -COCONUT_DKG_CONTRACT_ADDRESS=n1ahg0erc2fs6xx3j5m8sfx3ryuzdjh6kf6qm9plsf865fltekyrfsesac6a +ECASH_CONTRACT_ADDRESS=n1v3vydvs2ued84yv3khqwtgldmgwn0elljsdh08dr5s2j9x4rc5fs9jlwz9 +GROUP_CONTRACT_ADDRESS=n1ewmwz97xm0h8rdk8sw7h9mwn866qkx9hl9zlmagqfkhuzvwk5hhq844ue9 +MULTISIG_CONTRACT_ADDRESS=n1tz0setr8vkh9udp8xyxgpqc89ns27k4d0jx2h942hr0ax63yjhmqz6xct8 +COCONUT_DKG_CONTRACT_ADDRESS=n1v3n2ly2dp3a9ng3ff6rh26yfkn0pc5hed7w2shc5u9ca5c865utqj5elvh STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0" diff --git a/explorer-api/Cargo.toml b/explorer-api/Cargo.toml index 2a33ae5fc8..691c88b67a 100644 --- a/explorer-api/Cargo.toml +++ b/explorer-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "explorer-api" -version = "1.1.38" +version = "1.1.39" edition = "2021" license.workspace = true diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index cc2c6c37d1..4a62c70bee 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -11,34 +11,44 @@ authors = [ ] description = "Implementation of the Nym Mixnet Gateway" edition = "2021" -rust-version = "1.70" +rust-version = "1.76" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +path = "src/lib.rs" + +[[bin]] +name = "nym-gateway" +required-features = ["bin-deps"] +path = "src/main.rs" + [dependencies] anyhow = { workspace = true } async-trait = { workspace = true } bip39 = { workspace = true } bs58 = { workspace = true } -clap = { workspace = true, features = ["cargo", "derive"] } +clap = { workspace = true, features = ["cargo", "derive"], optional = true } colored = { workspace = true } +cosmwasm-std = { workspace = true } +cw-utils = { workspace = true } dashmap = { workspace = true } dirs = { workspace = true } dotenvy = { workspace = true } futures = { workspace = true } humantime-serde = { workspace = true } ipnetwork = { workspace = true } -log = { workspace = true } once_cell = { workspace = true } rand = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +si-scale = { workspace = true } sqlx = { workspace = true, features = [ "runtime-tokio-rustls", "sqlite", "macros", "migrate", - "time" + "time", ] } subtle-encoding = { workspace = true, features = ["bech32-preview"] } thiserror = { workspace = true } @@ -52,20 +62,25 @@ tokio = { workspace = true, features = [ tokio-stream = { workspace = true, features = ["fs"] } tokio-tungstenite = { workspace = true } tokio-util = { workspace = true, features = ["codec"] } +tracing = { workspace = true } url = { workspace = true, features = ["serde"] } time = { workspace = true } zeroize = { workspace = true } +bloomfilter = { workspace = true } # internal nym-authenticator = { path = "../service-providers/authenticator" } nym-api-requests = { path = "../nym-api/nym-api-requests" } -nym-bin-common = { path = "../common/bin-common", features = ["output_format"] } +nym-bin-common = { path = "../common/bin-common" } nym-config = { path = "../common/config" } nym-credentials = { path = "../common/credentials" } nym-credentials-interface = { path = "../common/credentials-interface" } nym-crypto = { path = "../common/crypto" } -nym-gateway-requests = { path = "gateway-requests" } +nym-ecash-contract-common = { path = "../common/cosmwasm-smart-contracts/ecash-contract" } +nym-ecash-double-spending = { path = "../common/ecash-double-spending" } +nym-gateway-storage = { path = "../common/gateway-storage" } +nym-gateway-requests = { path = "../common/gateway-requests" } nym-mixnet-client = { path = "../common/client-libs/mixnet-client" } nym-mixnode-common = { path = "../common/mixnode-common" } nym-network-defaults = { path = "../common/network-defaults" } @@ -78,10 +93,10 @@ nym-types = { path = "../common/types" } nym-validator-client = { path = "../common/client-libs/validator-client" } nym-ip-packet-router = { path = "../service-providers/ip-packet-router" } -nym-wireguard = { path = "../common/wireguard", optional = true } +nym-wireguard = { path = "../common/wireguard" } nym-wireguard-types = { path = "../common/wireguard-types", default-features = false } -defguard_wireguard_rs = { workspace = true, optional = true } +defguard_wireguard_rs = { workspace = true } [build-dependencies] @@ -94,7 +109,7 @@ sqlx = { workspace = true, features = [ ] } [features] -wireguard = ["nym-wireguard", "defguard_wireguard_rs"] +bin-deps = ["clap", 'nym-bin-common/output_format'] [package.metadata.deb] name = "nym-gateway" diff --git a/gateway/gateway-requests/src/models.rs b/gateway/gateway-requests/src/models.rs deleted file mode 100644 index 339715bbf7..0000000000 --- a/gateway/gateway-requests/src/models.rs +++ /dev/null @@ -1,336 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::GatewayRequestsError; -use nym_credentials::coconut::bandwidth::CredentialSpendingData; -use nym_credentials_interface::{CoconutError, VerifyCredentialRequest}; -use serde::{Deserialize, Serialize}; - -// reimplements old coconut-interface::Credential for backwards compatibility sake -// (so that 'new' gateways could still understand those requests) -#[derive(Debug, PartialEq, Eq)] -pub struct OldV1Credential { - pub n_params: u32, - - pub theta: VerifyCredentialRequest, - - pub voucher_value: u64, - - pub voucher_info: String, - - pub epoch_id: u64, -} - -// attempt to convert the old request type into the new variant -impl TryFrom for CredentialSpendingRequest { - type Error = GatewayRequestsError; - - fn try_from(value: OldV1Credential) -> Result { - if value.n_params <= 2 { - return Err(GatewayRequestsError::InvalidNumberOfEmbededParameters( - value.n_params, - )); - } - let embedded_private_attributes = value.n_params as usize - 2; - let typ = value.voucher_info.parse()?; - let public_attributes_plain = vec![value.voucher_value.to_string(), value.voucher_info]; - - Ok(CredentialSpendingRequest { - data: CredentialSpendingData { - embedded_private_attributes, - verify_credential_request: value.theta, - public_attributes_plain, - typ, - epoch_id: value.epoch_id, - }, - }) - } -} - -impl OldV1Credential { - pub fn as_bytes(&self) -> Vec { - let n_params_bytes = self.n_params.to_be_bytes(); - let theta_bytes = self.theta.to_bytes(); - let theta_bytes_len = theta_bytes.len(); - let voucher_value_bytes = self.voucher_value.to_be_bytes(); - let epoch_id_bytes = self.epoch_id.to_be_bytes(); - let voucher_info_bytes = self.voucher_info.as_bytes(); - let voucher_info_len = voucher_info_bytes.len(); - - let mut bytes = Vec::with_capacity(28 + theta_bytes_len + voucher_info_len); - bytes.extend_from_slice(&n_params_bytes); - bytes.extend_from_slice(&(theta_bytes_len as u64).to_be_bytes()); - bytes.extend_from_slice(&theta_bytes); - bytes.extend_from_slice(&voucher_value_bytes); - bytes.extend_from_slice(&epoch_id_bytes); - bytes.extend_from_slice(voucher_info_bytes); - - bytes - } - - pub fn from_bytes(bytes: &[u8]) -> Result { - if bytes.len() < 28 { - return Err(CoconutError::Deserialization(String::from( - "To few bytes in credential", - ))); - } - let mut four_byte = [0u8; 4]; - let mut eight_byte = [0u8; 8]; - - four_byte.copy_from_slice(&bytes[..4]); - let n_params = u32::from_be_bytes(four_byte); - eight_byte.copy_from_slice(&bytes[4..12]); - let theta_len = u64::from_be_bytes(eight_byte); - if bytes.len() < 28 + theta_len as usize { - return Err(CoconutError::Deserialization(String::from( - "To few bytes in credential", - ))); - } - let theta = VerifyCredentialRequest::from_bytes(&bytes[12..12 + theta_len as usize]) - .map_err(|e| CoconutError::Deserialization(e.to_string()))?; - eight_byte.copy_from_slice(&bytes[12 + theta_len as usize..20 + theta_len as usize]); - let voucher_value = u64::from_be_bytes(eight_byte); - eight_byte.copy_from_slice(&bytes[20 + theta_len as usize..28 + theta_len as usize]); - let epoch_id = u64::from_be_bytes(eight_byte); - let voucher_info = String::from_utf8(bytes[28 + theta_len as usize..].to_vec()) - .map_err(|e| CoconutError::Deserialization(e.to_string()))?; - - Ok(OldV1Credential { - n_params, - theta, - voucher_value, - voucher_info, - epoch_id, - }) - } -} - -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct CredentialSpendingRequest { - /// The cryptographic material required for spending the underlying credential. - pub data: CredentialSpendingData, -} - -// just a helper macro for checking required length and advancing the buffer -macro_rules! ensure_len_and_advance { - ($b:expr, $n:expr) => {{ - if $b.len() < $n { - return Err(GatewayRequestsError::CredentialDeserializationFailureEOF); - } - // create binding to the desired range - let bytes = &$b[..$n]; - - // update the initial binding - - $b = &$b[$n..]; - - bytes - }}; -} - -impl CredentialSpendingRequest { - pub fn new(data: CredentialSpendingData) -> Self { - CredentialSpendingRequest { data } - } - - pub fn matches_blinded_serial_number( - &self, - blinded_serial_number_bs58: &str, - ) -> Result { - self.data - .verify_credential_request - .has_blinded_serial_number(blinded_serial_number_bs58) - } - - pub fn unchecked_voucher_value(&self) -> u64 { - self.data - .get_bandwidth_attribute() - .expect("failed to extract bandwidth attribute") - .parse() - .expect("failed to parse voucher value") - } - - pub fn to_bytes(&self) -> Vec { - // simple length prefixed serialization - // TODO: change it to a standard format instead - let mut bytes = Vec::new(); - - let embedded_private = (self.data.embedded_private_attributes as u32).to_be_bytes(); - let theta = self.data.verify_credential_request.to_bytes(); - let theta_len = (theta.len() as u32).to_be_bytes(); - - let public = (self.data.public_attributes_plain.len() as u32).to_be_bytes(); - let typ = self.data.typ.to_string(); - let typ_bytes = typ.as_bytes(); - let typ_len = (typ_bytes.len() as u32).to_be_bytes(); - - bytes.extend_from_slice(&embedded_private); - bytes.extend_from_slice(&theta_len); - bytes.extend_from_slice(&theta); - bytes.extend_from_slice(&public); - - for pub_element in &self.data.public_attributes_plain { - let bytes_el = pub_element.as_bytes(); - let len = (bytes_el.len() as u32).to_be_bytes(); - - bytes.extend_from_slice(&len); - bytes.extend_from_slice(bytes_el); - } - - bytes.extend_from_slice(&typ_len); - bytes.extend_from_slice(typ_bytes); - bytes.extend_from_slice(&self.data.epoch_id.to_be_bytes()); - - bytes - } - - pub fn try_from_bytes(raw: &[u8]) -> Result { - // initial binding - let mut b = raw; - let embedded_private_bytes = ensure_len_and_advance!(b, 4); - let embedded_private_attributes = - u32::from_be_bytes(embedded_private_bytes.try_into().unwrap()) as usize; - - let theta_len_bytes = ensure_len_and_advance!(b, 4); - let theta_len = u32::from_be_bytes(theta_len_bytes.try_into().unwrap()) as usize; - - let theta_bytes = ensure_len_and_advance!(b, theta_len); - let theta = VerifyCredentialRequest::from_bytes(theta_bytes) - .map_err(GatewayRequestsError::CredentialDeserializationFailureMalformedTheta)?; - - let public_bytes = ensure_len_and_advance!(b, 4); - let public = u32::from_be_bytes(public_bytes.try_into().unwrap()) as usize; - - let mut public_attributes_plain = Vec::with_capacity(public); - for _ in 0..public { - let element_len_bytes = ensure_len_and_advance!(b, 4); - let element_len = u32::from_be_bytes(element_len_bytes.try_into().unwrap()) as usize; - - let element_bytes = ensure_len_and_advance!(b, element_len); - let element = String::from_utf8(element_bytes.to_vec())?; - public_attributes_plain.push(element); - } - - let typ_len_bytes = ensure_len_and_advance!(b, 4); - let typ_len = u32::from_be_bytes(typ_len_bytes.try_into().unwrap()) as usize; - - let typ_bytes = ensure_len_and_advance!(b, typ_len); - let raw_typ = String::from_utf8(typ_bytes.to_vec())?; - let typ = raw_typ.parse()?; - - // tell the linter to chill out in for this last iteration - #[allow(unused_assignments)] - let epoch_id_bytes = ensure_len_and_advance!(b, 8); - let epoch_id = u64::from_be_bytes(epoch_id_bytes.try_into().unwrap()); - - Ok(CredentialSpendingRequest { - data: CredentialSpendingData { - embedded_private_attributes, - verify_credential_request: theta, - public_attributes_plain, - typ, - epoch_id, - }, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use nym_credentials::coconut::bandwidth::bandwidth_credential_params; - use nym_credentials::IssuanceBandwidthCredential; - use nym_credentials_interface::{ - blind_sign, hash_to_scalar, prove_bandwidth_credential, Attribute, Base58, Parameters, - Signature, VerificationKey, - }; - - #[test] - fn old_v1_coconut_credential_roundtrip() { - let voucher_value = 1000000u64; - let voucher_info = String::from("BandwidthVoucher"); - let serial_number = - Attribute::try_from_bs58("7Rp3imcuNX3w9se9wm5th8gSvc2czsnMrGsdt5HsrycA").unwrap(); - let binding_number = - Attribute::try_from_bs58("Auf8yVEgyEAWNHaXUZmimS4n9g5YiYnNYqp6F9BtBe9E").unwrap(); - let signature = Signature::try_from_bs58( - "ta3pM9ffj5T6YGbwjSBp2W118rcwyP9PXStc\ - 7ssb91g5GQYMQHhuTNajbdZcjxUFBFL5rhED8EHpRzE8r432ss3qbPBfpNev4CdkfMkQ3wepyM7hy7q1W6Rn9WmFoZL\ - ZR9j", - ) - .unwrap(); - let params = Parameters::new(4).unwrap(); - let verification_key = VerificationKey::try_from_bs58("8CFtVVXdwLy4WHMQPE4\ - woe89q3DRHoNxBSchftrEjSBPWA4r4xZv4Y9qSvS5x5bMmFtp7BX6ikECAnuXr5EjXWSsgjirZJmpS5XDUynVfht1cD\ - FWGDvy2XFrRCuoCMotNXi3PoF6wYqdTR9Rqcfoj3i2H5Nid422WBaLtVoC9QNobvpvaqq6vX5PbsSyPayvU8HCXFxM6\ - JjScYpbRTxQtdwefWLrk3LmXyJQBWi7c2VAhSxu9msp7VTBycqdwQNgxHETStZuwXsozxaGQ2KssVUCaaoYPR4g2RqK\ - UAvtWwA7pMiAQNcbkXcbsjCgVjWaCpMWC37XA31cLcFf3zbjHD9e5tXjAcqa4M89fbFhuvvSXxowSAZ5NoWrN32kd5d\ - wxJm1JW3Tt2h6yDDBe84oMy71462dZn7N78DVk2mFNGwBCibrZWA7oUzRBMfYxiQrksoFcou7QfLLd58zoNYmPQPt84\ - 1VpQopEBfdQ7Nf9zoXxBt3zMy7g5NsFGvzh7KTbDUyeeXrdkKJPQBs6dqaizr9sS8CPPmR4uk96vDTRh8CJ5FbSsmb8\ - nP71dRvvwRZJHGzwYirMo6SXS3ZYxFuiA3mkxYuqDHCwkTWDuRCcAaztrDYRZg7VCMo4Q446AaEso5eqpeWpHZQt53E\ - ZRpqmNYKASGwMhTeEHPSLgSmtoAAUcaRWpGRzYfd6kzEma8tdGLwyP4rLXgvSvtDLP37dU7YgF3LEXbGAz57U9ATy46\ - 6sroLpHPdaCWB8RF11wvB6Tu196JnJd2KyQBP1iUWP3rtZs3GhAF1QVcxquh8BqDZzAcpQ6wCS1P9c5GxKgww77FVF5\ - Kp83XtoxSrw3GaYVyKTGxNh3vcKPR31txCjTxPaN2fg7TaPLhoQJX4YaAroFSXqrqbbRsisuHhhCeUP2YwDjHedes9y") - .unwrap(); - let theta = prove_bandwidth_credential( - ¶ms, - &verification_key, - &signature, - &serial_number, - &binding_number, - ) - .unwrap(); - - let credential = OldV1Credential { - n_params: 4, - theta, - voucher_value, - voucher_info, - epoch_id: 42, - }; - - let serialized_credential = credential.as_bytes(); - let deserialized_credential = OldV1Credential::from_bytes(&serialized_credential).unwrap(); - - assert_eq!(credential, deserialized_credential); - } - - #[test] - fn credential_roundtrip() { - // make valid request - let params = bandwidth_credential_params(); - let keypair = nym_credentials_interface::keygen(params); - - let issuance = IssuanceBandwidthCredential::new_freepass(None); - let sig_req = issuance.prepare_for_signing(); - let pub_attrs_hashed = sig_req - .public_attributes_plain - .iter() - .map(hash_to_scalar) - .collect::>(); - let pub_attrs = pub_attrs_hashed.iter().collect::>(); - let blind_sig = blind_sign( - params, - keypair.secret_key(), - &sig_req.blind_sign_request, - &pub_attrs, - ) - .unwrap(); - let sig = blind_sig.unblind( - keypair.verification_key(), - &sig_req.pedersen_commitments_openings, - ); - - let issued = issuance.into_issued_credential(sig, 42); - let spending = issued - .prepare_for_spending(keypair.verification_key()) - .unwrap(); - - let with_epoch = CredentialSpendingRequest { data: spending }; - - let bytes = with_epoch.to_bytes(); - let recovered = CredentialSpendingRequest::try_from_bytes(&bytes).unwrap(); - - assert_eq!(with_epoch, recovered); - } -} diff --git a/gateway/src/commands/helpers.rs b/gateway/src/commands/helpers.rs index c8551cc445..9711ab1583 100644 --- a/gateway/src/commands/helpers.rs +++ b/gateway/src/commands/helpers.rs @@ -1,6 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::commands::upgrade_helpers; use log::{error, info}; use nym_config::{save_formatted_config_to_file, OptionalSet}; use nym_crypto::asymmetric::identity; @@ -17,6 +18,7 @@ use nym_gateway::helpers::{ use nym_network_defaults::mainnet; use nym_network_defaults::var_names::NYXD; use nym_network_defaults::var_names::{BECH32_PREFIX, NYM_API}; +use tracing::{error, info}; use nym_network_requester::{ generate_new_client_keys, set_active_gateway, setup_fs_gateways_storage, setup_gateway, diff --git a/gateway/src/commands/mod.rs b/gateway/src/commands/mod.rs index d2100efab6..1a08e249a0 100644 --- a/gateway/src/commands/mod.rs +++ b/gateway/src/commands/mod.rs @@ -5,10 +5,10 @@ use crate::Cli; use anyhow::bail; use clap::CommandFactory; use clap::Subcommand; -use log::{error, warn}; use nym_bin_common::completions::{fig_generate, ArgShell}; use std::io::IsTerminal; use std::time::Duration; +use tracing::{error, warn}; pub(crate) mod build_info; pub(crate) mod helpers; diff --git a/gateway/src/commands/run.rs b/gateway/src/commands/run.rs index ceb30fcb4c..e9b4fb8d1b 100644 --- a/gateway/src/commands/run.rs +++ b/gateway/src/commands/run.rs @@ -4,13 +4,13 @@ use crate::commands::helpers::{try_load_current_config, try_override_config, OverrideConfig}; use anyhow::bail; use clap::Args; -use log::warn; use nym_bin_common::output_format::OutputFormat; use nym_config::helpers::SPECIAL_ADDRESSES; use nym_gateway::helpers::{OverrideIpPacketRouterConfig, OverrideNetworkRequesterConfig}; use nym_gateway::GatewayError; use std::net::IpAddr; use std::path::PathBuf; +use tracing::warn; #[derive(Args, Clone)] pub struct Run { diff --git a/gateway/src/commands/setup_network_requester.rs b/gateway/src/commands/setup_network_requester.rs index c489983424..3611392a56 100644 --- a/gateway/src/commands/setup_network_requester.rs +++ b/gateway/src/commands/setup_network_requester.rs @@ -3,12 +3,12 @@ use crate::commands::helpers::{initialise_local_network_requester, try_load_current_config}; use clap::Args; -use log::warn; use nym_bin_common::output_format::OutputFormat; use nym_gateway::helpers::{load_public_key, OverrideNetworkRequesterConfig}; use std::io::IsTerminal; use std::path::PathBuf; use std::time::Duration; +use tracing::warn; #[derive(Args, Clone)] pub struct CmdArgs { diff --git a/gateway/src/config/mod.rs b/gateway/src/config/mod.rs index 91d1e3b92e..7605987a79 100644 --- a/gateway/src/config/mod.rs +++ b/gateway/src/config/mod.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::config::template::CONFIG_TEMPLATE; -use log::{debug, warn}; use nym_bin_common::logging::LoggingSettings; use nym_config::defaults::{DEFAULT_CLIENT_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT}; use nym_config::helpers::inaddr_any; @@ -11,12 +10,13 @@ use nym_config::{ must_get_home, read_config_from_toml_file, save_formatted_config_to_file, NymConfigTemplate, DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, DEFAULT_DATA_DIR, NYM_DIR, }; -use nym_network_defaults::{mainnet, DEFAULT_NYM_NODE_HTTP_PORT}; +use nym_network_defaults::{mainnet, DEFAULT_NYM_NODE_HTTP_PORT, TICKETBOOK_VALIDITY_DAYS}; use serde::{Deserialize, Serialize}; use std::io; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::{Path, PathBuf}; use std::time::Duration; +use tracing::{debug, warn}; use url::Url; use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -67,7 +67,7 @@ pub fn default_data_directory>(id: P) -> PathBuf { .join(DEFAULT_DATA_DIR) } -#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Deserialize, Serialize)] #[serde(deny_unknown_fields)] pub struct Config { // additional metadata holding on-disk location of this config file @@ -220,7 +220,6 @@ impl Config { self.gateway.only_coconut_credentials = only_coconut_credentials; self } - pub fn with_custom_nym_apis(mut self, nym_api_urls: Vec) -> Self { self.gateway.nym_api_urls = nym_api_urls; self @@ -415,7 +414,7 @@ impl Default for IpPacketRouter { } } -#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Deserialize, Serialize)] #[serde(default)] pub struct Debug { /// Initial value of an exponential backoff to reconnect to dropped TCP connection when @@ -459,6 +458,9 @@ pub struct Debug { // existing nodes whilst everyone else is upgrading and getting the code for handling the new field. // It shall be disabled in the subsequent releases. pub use_legacy_framed_packet_version: bool, + + #[serde(default)] + pub zk_nym_tickets: ZkNymTicketHandlerDebug, } impl Default for Debug { @@ -475,6 +477,72 @@ impl Default for Debug { client_bandwidth_max_delta_flushing_amount: DEFAULT_CLIENT_BANDWIDTH_MAX_DELTA_FLUSHING_AMOUNT, use_legacy_framed_packet_version: false, + zk_nym_tickets: Default::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ZkNymTicketHandlerDebug { + /// Specifies the multiplier for revoking a malformed/double-spent ticket + /// (if it has to go all the way to the nym-api for verification) + /// e.g. if one ticket grants 100Mb and `revocation_bandwidth_penalty` is set to 1.5, + /// the client will lose 150Mb + pub revocation_bandwidth_penalty: f32, + + /// Specifies the interval for attempting to resolve any failed, pending operations, + /// such as ticket verification or redemption. + #[serde(with = "humantime_serde")] + pub pending_poller: Duration, + + pub minimum_api_quorum: f32, + + /// Specifies the minimum number of tickets this gateway will attempt to redeem. + pub minimum_redemption_tickets: usize, + + /// Specifies the maximum time between two subsequent tickets redemptions. + /// That's required as nym-apis will purge all ticket information for tickets older than maximum validity. + #[serde(with = "humantime_serde")] + pub maximum_time_between_redemption: Duration, +} + +impl ZkNymTicketHandlerDebug { + pub const DEFAULT_REVOCATION_BANDWIDTH_PENALTY: f32 = 10.0; + pub const DEFAULT_PENDING_POLLER: Duration = Duration::from_secs(300); + pub const DEFAULT_MINIMUM_API_QUORUM: f32 = 0.8; + pub const DEFAULT_MINIMUM_REDEMPTION_TICKETS: usize = 100; + + // use min(4/5 of max validity, validity - 1), but making sure it's no greater than 1 day + // ASSUMPTION: our validity period is AT LEAST 2 days + // + // this could have been a constant, but it's more readable as a function + pub const fn default_maximum_time_between_redemption() -> Duration { + let desired_secs = TICKETBOOK_VALIDITY_DAYS * (86400 * 4) / 5; + let desired_secs_alt = (TICKETBOOK_VALIDITY_DAYS - 1) * 86400; + + // can't use `min` in const context + let target_secs = if desired_secs < desired_secs_alt { + desired_secs + } else { + desired_secs_alt + }; + + assert!( + target_secs > 86400, + "the maximum time between redemption can't be lower than 1 day!" + ); + Duration::from_secs(target_secs as u64) + } +} + +impl Default for ZkNymTicketHandlerDebug { + fn default() -> Self { + ZkNymTicketHandlerDebug { + revocation_bandwidth_penalty: Self::DEFAULT_REVOCATION_BANDWIDTH_PENALTY, + pending_poller: Self::DEFAULT_PENDING_POLLER, + minimum_api_quorum: Self::DEFAULT_MINIMUM_API_QUORUM, + minimum_redemption_tickets: Self::DEFAULT_MINIMUM_REDEMPTION_TICKETS, + maximum_time_between_redemption: Self::default_maximum_time_between_redemption(), } } } diff --git a/gateway/src/error.rs b/gateway/src/error.rs index edf34b3a95..4d9f8fc9b4 100644 --- a/gateway/src/error.rs +++ b/gateway/src/error.rs @@ -1,8 +1,8 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node::storage::error::StorageError; use nym_authenticator::error::AuthenticatorError; +use nym_gateway_storage::error::StorageError; use nym_ip_packet_router::error::IpPacketRouterError; use nym_network_requester::error::{ClientCoreError, NetworkRequesterError}; use nym_validator_client::nyxd::error::NyxdError; @@ -14,6 +14,7 @@ use std::path::PathBuf; use thiserror::Error; pub use crate::node::client_handling::websocket::connection_handler::authenticated::RequestHandlingError; +use crate::node::client_handling::websocket::connection_handler::ecash::error::EcashTicketError; #[derive(Debug, Error)] pub enum GatewayError { @@ -169,6 +170,12 @@ pub enum GatewayError { source: RequestHandlingError, }, + #[error("ecash related failure: {source}")] + EcashFailure { + #[from] + source: EcashTicketError, + }, + #[error("failed to catch an interrupt: {source}")] ShutdownFailure { source: Box, @@ -189,13 +196,14 @@ pub enum GatewayError { source: ipnetwork::IpNetworkError, }, - #[cfg(all(feature = "wireguard", target_os = "linux"))] + #[error("the current multisig contract is not using 'AbsolutePercentage' threshold!")] + InvalidMultisigThreshold, + #[error("failed to remove wireguard interface: {0}")] WireguardInterfaceError(#[from] defguard_wireguard_rs::error::WireguardInterfaceError), - #[cfg(all(feature = "wireguard", target_os = "linux"))] - #[error("wireguard not set")] - WireguardNotSet, + #[error("internal wireguard error {0}")] + InternalWireguardError(String), #[error("failed to start authenticator: {source}")] AuthenticatorStartError { diff --git a/gateway/src/helpers.rs b/gateway/src/helpers.rs index 7a00000a2f..7b17da7170 100644 --- a/gateway/src/helpers.rs +++ b/gateway/src/helpers.rs @@ -13,6 +13,7 @@ use nym_types::gateway::{ GatewayIpPacketRouterDetails, GatewayNetworkRequesterDetails, GatewayNodeDetailsResponse, }; use std::path::Path; +use tracing::info; pub use crate::node::helpers::{load_ip_packet_router_config, load_network_requester_config}; @@ -72,7 +73,7 @@ pub fn override_ip_packet_router_config( // disable poisson rate in the BASE client if the IPR option is enabled if cfg.ip_packet_router.disable_poisson_rate { - log::info!("Disabling poisson rate for ip packet router"); + info!("Disabling poisson rate for ip packet router"); cfg.set_no_poisson_process(); } diff --git a/gateway/src/http/mod.rs b/gateway/src/http/mod.rs index 23bc8d8693..72d0aa499d 100644 --- a/gateway/src/http/mod.rs +++ b/gateway/src/http/mod.rs @@ -4,7 +4,6 @@ use crate::config::Config; use crate::error::GatewayError; use crate::helpers::load_public_key; -use log::{debug, error, warn}; use nym_bin_common::bin_info_owned; use nym_crypto::asymmetric::{encryption, identity}; use nym_network_requester::RequestFilter; @@ -15,6 +14,7 @@ use nym_node_http_api::NymNodeHttpError; use nym_sphinx::addressing::clients::Recipient; use nym_task::TaskClient; use std::sync::Arc; +use tracing::{debug, error, warn}; fn load_gateway_details( config: &Config, @@ -272,19 +272,3 @@ impl<'a> HttpApiBuilder<'a> { Ok(()) } } - -// pub(crate) fn start_http_api( -// gateway_config: &Config, -// network_requester_config: Option<&nym_network_requester::Config>, -// client_registry: Arc, -// identity_keypair: &identity::KeyPair, -// // TODO: this should be a wg specific key and not re-used sphinx -// sphinx_keypair: Arc, -// -// task_client: TaskClient, -// ) -> Result<(), GatewayError> { -// HttpApiBuilder::new(gateway_config, identity_keypair, sphinx_keypair) -// .with_wireguard_client_registry(client_registry) -// .with_network_requester(network_requester_config) -// .start(task_client) -// } diff --git a/gateway/src/main.rs b/gateway/src/main.rs index 3c5109817b..6759180420 100644 --- a/gateway/src/main.rs +++ b/gateway/src/main.rs @@ -6,12 +6,12 @@ use clap::{crate_name, crate_version, Parser}; use colored::Colorize; -use log::error; use nym_bin_common::bin_info; use nym_bin_common::logging::{maybe_print_banner, setup_logging}; use nym_network_defaults::setup_env; use std::io::IsTerminal; use std::sync::OnceLock; +use tracing::error; mod commands; diff --git a/gateway/src/node/client_handling/active_clients.rs b/gateway/src/node/client_handling/active_clients.rs index df88d16213..962765067e 100644 --- a/gateway/src/node/client_handling/active_clients.rs +++ b/gateway/src/node/client_handling/active_clients.rs @@ -4,9 +4,9 @@ use super::websocket::message_receiver::{IsActiveRequestSender, MixMessageSender}; use crate::node::client_handling::embedded_clients::LocalEmbeddedClientHandle; use dashmap::DashMap; -use log::warn; use nym_sphinx::DestinationAddressBytes; use std::sync::Arc; +use tracing::warn; enum ActiveClient { /// Handle to a remote client connected via a network socket. diff --git a/gateway/src/node/client_handling/bandwidth.rs b/gateway/src/node/client_handling/bandwidth.rs index d4ce709c96..ebfb91b0c1 100644 --- a/gateway/src/node/client_handling/bandwidth.rs +++ b/gateway/src/node/client_handling/bandwidth.rs @@ -1,21 +1,17 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use log::{error, warn}; -use nym_credentials::coconut::bandwidth::CredentialType; +use nym_network_defaults::TicketTypeRepr; use std::num::ParseIntError; use thiserror::Error; use time::error::ComponentRange; -use time::OffsetDateTime; +use tracing::error; #[derive(Debug, Error)] pub enum BandwidthError { #[error("Provided bandwidth credential asks for more bandwidth than it is supported to add at once (credential value: {0}, supported: {}). Try to split it before attempting again", i64::MAX)] UnsupportedBandwidthValue(u64), - #[error("the provided free pass has already expired (expiry was on {expiry_date})")] - ExpiredFreePass { expiry_date: OffsetDateTime }, - #[error("failed to parse the bandwidth voucher value: {source}")] VoucherValueParsingFailure { #[source] @@ -46,51 +42,10 @@ impl Bandwidth { Bandwidth { value } } - pub fn new(bandwidth_value: u64) -> Result { - if bandwidth_value > i64::MAX as u64 { - // note that this would have represented more than 1 exabyte, - // which is like 125,000 worth of hard drives, so I don't think we have - // to worry about it for now... - warn!("Somehow we received bandwidth value higher than 9223372036854775807. We don't really want to deal with this now"); - return Err(BandwidthError::UnsupportedBandwidthValue(bandwidth_value)); + pub fn ticket_amount(typ: TicketTypeRepr) -> Self { + Bandwidth { + value: typ.bandwidth_value(), } - - Ok(Bandwidth { - value: bandwidth_value, - }) - } - - pub(crate) fn parse_raw_bandwidth( - value: &str, - typ: CredentialType, - ) -> Result<(u64, Option), BandwidthError> { - let (bandwidth_value, freepass_expiration) = - match typ { - CredentialType::Voucher => { - let token_value: u64 = value - .parse() - .map_err(|source| BandwidthError::VoucherValueParsingFailure { source })?; - (token_value * nym_network_defaults::BYTES_PER_UTOKEN, None) - } - CredentialType::FreePass => { - let expiry_timestamp: i64 = value - .parse() - .map_err(|source| BandwidthError::ExpiryDateParsingFailure { source })?; - - let expiry_date = OffsetDateTime::from_unix_timestamp(expiry_timestamp) - .map_err(|source| BandwidthError::InvalidExpiryDate { - unix_timestamp: expiry_timestamp, - source, - })?; - let now = OffsetDateTime::now_utc(); - - if expiry_date < now { - return Err(BandwidthError::ExpiredFreePass { expiry_date }); - } - (nym_network_defaults::BYTES_PER_FREEPASS, Some(expiry_date)) - } - }; - Ok((bandwidth_value, freepass_expiration)) } pub fn value(&self) -> u64 { diff --git a/gateway/src/node/client_handling/embedded_clients/mod.rs b/gateway/src/node/client_handling/embedded_clients/mod.rs index a7264f0985..7974bb9c41 100644 --- a/gateway/src/node/client_handling/embedded_clients/mod.rs +++ b/gateway/src/node/client_handling/embedded_clients/mod.rs @@ -5,11 +5,11 @@ use crate::node::client_handling::websocket::message_receiver::{ MixMessageReceiver, MixMessageSender, }; use futures::StreamExt; -use log::{debug, error}; use nym_network_requester::{GatewayPacketRouter, PacketRouter}; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::DestinationAddressBytes; use nym_task::TaskClient; +use tracing::{debug, error, trace}; #[derive(Debug)] pub(crate) struct LocalEmbeddedClientHandle { @@ -71,12 +71,12 @@ impl MessageRouter { messages = self.mix_receiver.next() => match messages { Some(messages) => self.handle_received_messages(messages), None => { - log::trace!("embedded_clients::MessageRouter: Stopping since channel closed"); + trace!("embedded_clients::MessageRouter: Stopping since channel closed"); break; } }, _ = shutdown.recv_with_delay() => { - log::trace!("embedded_clients::MessageRouter: Received shutdown"); + trace!("embedded_clients::MessageRouter: Received shutdown"); debug_assert!(shutdown.is_shutdown()); break } diff --git a/gateway/src/node/client_handling/websocket/common_state.rs b/gateway/src/node/client_handling/websocket/common_state.rs index e112997e57..3a2a206368 100644 --- a/gateway/src/node/client_handling/websocket/common_state.rs +++ b/gateway/src/node/client_handling/websocket/common_state.rs @@ -1,15 +1,16 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node::client_handling::websocket::connection_handler::coconut::CoconutVerifier; +use crate::node::client_handling::websocket::connection_handler::ecash::EcashManager; use crate::node::client_handling::websocket::connection_handler::BandwidthFlushingBehaviourConfig; use nym_crypto::asymmetric::identity; use std::sync::Arc; // I can see this being possible expanded with say storage or client store #[derive(Clone)] -pub(crate) struct CommonHandlerState { - pub(crate) coconut_verifier: Arc, +pub(crate) struct CommonHandlerState { + pub(crate) ecash_verifier: Arc>, + pub(crate) storage: S, pub(crate) local_identity: Arc, pub(crate) only_coconut_credentials: bool, pub(crate) bandwidth_cfg: BandwidthFlushingBehaviourConfig, diff --git a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs index 249f60997f..51117b2879 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -1,43 +1,41 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node::client_handling::bandwidth::BandwidthError; -use crate::node::client_handling::websocket::connection_handler::ClientBandwidth; -use crate::node::{ - client_handling::{ - bandwidth::Bandwidth, - websocket::{ - connection_handler::{ClientDetails, FreshHandler}, - message_receiver::{ - IsActive, IsActiveRequestReceiver, IsActiveResultSender, MixMessageReceiver, - }, +use crate::node::client_handling::{ + bandwidth::{Bandwidth, BandwidthError}, + websocket::{ + connection_handler::{ + ecash::error::EcashTicketError, ClientBandwidth, ClientDetails, FreshHandler, + }, + message_receiver::{ + IsActive, IsActiveRequestReceiver, IsActiveResultSender, MixMessageReceiver, }, - FREE_TESTNET_BANDWIDTH_VALUE, }, - storage::{error::StorageError, Storage}, + FREE_TESTNET_BANDWIDTH_VALUE, }; use futures::{ future::{FusedFuture, OptionFuture}, FutureExt, StreamExt, }; -use log::*; -use nym_credentials::coconut::bandwidth::{bandwidth_credential_params, CredentialType}; -use nym_credentials_interface::{Base58, CoconutError}; +use nym_credentials::ecash::utils::{ecash_today, EcashTime}; +use nym_credentials_interface::{ClientTicket, CredentialSpendingData}; use nym_gateway_requests::models::CredentialSpendingRequest; use nym_gateway_requests::{ - iv::{IVConversionError, IV}, types::{BinaryRequest, ServerResponse}, - ClientControlRequest, GatewayRequestsError, + ClientControlRequest, GatewayRequestsError, SimpleGatewayRequestsError, }; +use nym_gateway_storage::{error::StorageError, Storage}; use nym_sphinx::forwarding::packet::MixPacket; use nym_task::TaskClient; -use nym_validator_client::coconut::CoconutApiError; +use nym_validator_client::coconut::EcashApiError; use rand::{CryptoRng, Rng}; +use si_scale::helpers::bibytes2; use std::{process, time::Duration}; use thiserror::Error; -use time::OffsetDateTime; +use time::{Date, OffsetDateTime}; use tokio::io::{AsyncRead, AsyncWrite}; use tokio_tungstenite::tungstenite::{protocol::Message, Error as WsError}; +use tracing::*; #[derive(Debug, Error)] pub enum RequestHandlingError { @@ -49,9 +47,6 @@ pub enum RequestHandlingError { )] MissingClientBandwidthEntry { client_address: String }, - #[error("Provided bandwidth IV is malformed - {0}")] - MalformedIV(#[from] IVConversionError), - #[error("Provided binary request was malformed - {0}")] InvalidBinaryRequest(#[from] GatewayRequestsError), @@ -61,8 +56,13 @@ pub enum RequestHandlingError { #[error("The received request is not valid in the current context: {additional_context}")] IllegalRequest { additional_context: String }, - #[error("Provided bandwidth credential did not verify correctly on {0}")] - InvalidBandwidthCredential(String), + #[error("credential has been rejected by the validators")] + RejectedProposal, + + #[error( + "the provided credential has an invalid spending date. got {got} but expected {expected}" + )] + InvalidCredentialSpendingDate { got: Date, expected: Date }, #[error("the provided bandwidth credential has already been spent before at this gateway")] BandwidthCredentialAlreadySpent, @@ -76,46 +76,62 @@ pub enum RequestHandlingError { #[error("Validator API error - {0}")] APIError(#[from] nym_validator_client::ValidatorClientError), - #[error("Not enough nym API endpoints provided. Needed {needed}, received {received}")] - NotEnoughNymAPIs { received: usize, needed: usize }, - #[error("There was a problem with the proposal id: {reason}")] ProposalIdError { reason: String }, - #[error("coconut failure: {0}")] - CoconutError(#[from] CoconutError), + #[error("compact ecash error: {0}")] + CompactEcashError(#[from] nym_credentials_interface::CompactEcashError), #[error("coconut api query failure: {0}")] - CoconutApiError(#[from] CoconutApiError), + CoconutApiError(#[from] EcashApiError), #[error("Credential error - {0}")] CredentialError(#[from] nym_credentials::error::Error), + #[error("Internal error")] + InternalError, + #[error("failed to recover bandwidth value: {0}")] BandwidthRecoveryFailure(#[from] BandwidthError), - #[error("the provided credential did not contain a valid type attribute")] - InvalidTypeAttribute, - #[error("insufficient bandwidth available to process the request. required: {required}B, available: {available}B")] OutOfBandwidth { required: i64, available: i64 }, - #[error("the provided credential did not have a bandwidth attribute")] - MissingBandwidthAttribute, + #[error(transparent)] + EcashFailure(EcashTicketError), - #[error("attempted to claim a bandwidth voucher for an account using a free pass (it expires on {expiration})")] - BandwidthVoucherForFreePassAccount { expiration: OffsetDateTime }, - - #[error("attempted to claim another free pass for the account while another free pass is still active (it expires on {expiration})")] - PreexistingFreePass { expiration: OffsetDateTime }, - - #[error("the DKG contract is unavailable")] - UnavailableDkgContract, + #[error( + "the received payment contained more than a single ticket. that's currently not supported" + )] + MultipleTickets, } impl RequestHandlingError { fn into_error_message(self) -> Message { - ServerResponse::new_error(self.to_string()).into() + let server_response = match self { + RequestHandlingError::OutOfBandwidth { + required, + available, + } => ServerResponse::TypedError { + error: SimpleGatewayRequestsError::OutOfBandwidth { + required, + available, + }, + }, + other => ServerResponse::new_error(other.to_string()), + }; + server_response.into() + } +} + +impl From for RequestHandlingError { + fn from(err: EcashTicketError) -> Self { + // don't expose storage issue details to the user + if let EcashTicketError::InternalStorageFailure { source } = err { + RequestHandlingError::StorageError(source) + } else { + RequestHandlingError::EcashFailure(err) + } } } @@ -159,7 +175,7 @@ impl AuthenticatedHandler where // TODO: those trait bounds here don't really make sense.... R: Rng + CryptoRng, - St: Storage, + St: Storage + Clone + 'static, { /// Upgrades `FreshHandler` into the Authenticated variant implying the client is now authenticated /// and thus allowed to perform more actions with the gateway, such as redeeming bandwidth or @@ -181,8 +197,9 @@ where // so in theory we could just unwrap the value here, but since we're returning a Result anyway, // we might as well return a failure response instead let bandwidth = fresh + .shared_state .storage - .get_available_bandwidth(client.address) + .get_available_bandwidth(client.id) .await? .ok_or(RequestHandlingError::MissingClientBandwidthEntry { client_address: client.address.as_base58_string(), @@ -205,10 +222,11 @@ where .disconnect(self.client.address) } - async fn expire_freepass(&mut self) -> Result<(), RequestHandlingError> { + async fn expire_bandwidth(&mut self) -> Result<(), RequestHandlingError> { + self.inner.expire_bandwidth(self.client.id).await?; self.client_bandwidth.bandwidth = Default::default(); - self.client_bandwidth.update_flush_data(); - Ok(self.inner.expire_freepass(self.client.address).await?) + self.client_bandwidth.update_sync_data(); + Ok(()) } /// Increases the amount of available bandwidth of the connected client by the specified value. @@ -216,28 +234,20 @@ where /// # Arguments /// /// * `amount`: amount to increase the available bandwidth by. + /// * `expiration` : the expiration date of that bandwidth async fn increase_bandwidth( &mut self, bandwidth: Bandwidth, + expiration: OffsetDateTime, ) -> Result<(), RequestHandlingError> { self.client_bandwidth.bandwidth.bytes += bandwidth.value() as i64; + self.client_bandwidth.bytes_delta_since_sync += bandwidth.value() as i64; + self.client_bandwidth.bandwidth.expiration = expiration; // any increases to bandwidth should get flushed immediately // (we don't want to accidentally miss somebody claiming a gigabyte voucher) - self.flush_bandwidth().await - } - - async fn set_freepass_expiration( - &mut self, - expiration: OffsetDateTime, - ) -> Result<(), RequestHandlingError> { - self.client_bandwidth.bandwidth.freepass_expiration = Some(expiration); - self.inner - .storage - .set_freepass_expiration(self.client.address, expiration) - .await?; - self.client_bandwidth.update_flush_data(); - Ok(()) + self.sync_expiration().await?; + self.sync_bandwidth().await } /// Decreases the amount of available bandwidth of the connected client by the specified value. @@ -247,14 +257,15 @@ where /// * `amount`: amount to decrease the available bandwidth by. async fn consume_bandwidth(&mut self, amount: i64) -> Result<(), RequestHandlingError> { self.client_bandwidth.bandwidth.bytes -= amount; + self.client_bandwidth.bytes_delta_since_sync -= amount; // since we're going to be operating on a fair use policy anyway, even if we crash and let extra few packets // through, that's completely fine if self .client_bandwidth - .should_flush(self.inner.shared_state.bandwidth_cfg) + .should_sync(self.inner.shared_state.bandwidth_cfg) { - self.flush_bandwidth().await?; + self.sync_bandwidth().await?; } Ok(()) @@ -272,148 +283,107 @@ where } } - async fn handle_bandwidth_request( - &mut self, - credential: CredentialSpendingRequest, - ) -> Result { - // check if the credential hasn't been spent before - let serial_number = credential.data.blinded_serial_number(); - trace!("processing credential {}", serial_number.to_bs58()); + async fn check_local_db_for_double_spending( + &self, + serial_number: &[u8], + ) -> Result<(), RequestHandlingError> { + trace!("checking local db for double spending..."); - // if we already have had received a free pass (that's not expired, don't accept any additional bandwidth) - if self.client_bandwidth.bandwidth.freepass_expired() { - // the free pass we used before has expired -> reset our state and handle the request as normal - self.expire_freepass().await?; - } else if let Some(expiration) = self.client_bandwidth.bandwidth.freepass_expiration { - // the free pass is still valid -> return error - return match credential.data.typ { - CredentialType::Voucher => { - Err(RequestHandlingError::BandwidthVoucherForFreePassAccount { expiration }) - } - CredentialType::FreePass => { - Err(RequestHandlingError::PreexistingFreePass { expiration }) - } - }; - } - - let already_spent = self + let spent = self .inner + .shared_state .storage - .contains_credential(&serial_number) + .contains_ticket(serial_number) .await?; - if already_spent { - trace!("the credential has already been spent before"); + if spent { + trace!("the credential has already been spent before at this gateway"); return Err(RequestHandlingError::BandwidthCredentialAlreadySpent); } - - trace!( - "attempting to obtain aggregate verification key for epoch {}", - credential.data.epoch_id - ); - - if !credential.data.validate_type_attribute() { - trace!("mismatch in the type attribute"); - return Err(RequestHandlingError::InvalidTypeAttribute); - } - - let Some(bandwidth_attribute) = credential.data.get_bandwidth_attribute() else { - trace!("missing bandwidth attribute"); - return Err(RequestHandlingError::MissingBandwidthAttribute); - }; - - // this will extract token amounts out of bandwidth vouchers and validate expiry of free passes - let (raw_bandwidth, freepass_expiration) = - Bandwidth::parse_raw_bandwidth(bandwidth_attribute, credential.data.typ)?; - - let bandwidth = Bandwidth::new(raw_bandwidth)?; - - trace!("embedded bandwidth: {bandwidth:?}"); - - // locally verify the credential - { - let aggregated_verification_key = self - .inner - .shared_state - .coconut_verifier - .verification_key(credential.data.epoch_id) - .await?; - - let params = bandwidth_credential_params(); - if !credential.data.verify(params, &aggregated_verification_key) { - trace!("the credential did not verify correctly"); - return Err(RequestHandlingError::InvalidBandwidthCredential( - String::from("local credential verification has failed"), - )); - } - } - - match credential.data.typ { - CredentialType::Voucher => { - trace!("the credential is a bandwidth voucher. attempting to release the funds"); - let api_clients = self - .inner - .shared_state - .coconut_verifier - .api_clients(credential.data.epoch_id) - .await?; - - self.inner - .shared_state - .coconut_verifier - .release_bandwidth_voucher_funds(&api_clients, credential) - .await?; - } - CredentialType::FreePass => { - // no need to do anything special here, we already extracted the bandwidth amount and checked expiry - info!("received a free pass credential"); - } - } - - // technically this is not atomic, i.e. checking for the spending and then marking as spent, - // but because we have the `UNIQUE` constraint on the database table - // if somebody attempts to spend the same credential in another, parallel request, - // one of them will fail - // - // mark the credential as spent - // TODO: technically this should be done under a storage transaction so that if we experience any - // failures later on, it'd get reverted - trace!("storing serial number information"); - self.inner - .storage - .insert_spent_credential( - serial_number, - freepass_expiration.is_some(), - self.client.address, - ) - .await?; - - trace!("increasing client bandwidth"); - self.increase_bandwidth(bandwidth).await?; - // set free pass expiration - if let Some(expiration) = freepass_expiration { - self.set_freepass_expiration(expiration).await?; - } - - let available_total = self.client_bandwidth.bandwidth.bytes; - - Ok(ServerResponse::Bandwidth { available_total }) + Ok(()) } - async fn handle_bandwidth_v1( - &mut self, - enc_credential: Vec, - iv: Vec, - ) -> Result { - debug!("handling v1 bandwidth request"); + async fn check_bloomfilter(&self, serial_number: &Vec) -> Result<(), RequestHandlingError> { + trace!("checking the bloomfilter..."); - let iv = IV::try_from_bytes(&iv)?; - let credential = ClientControlRequest::try_from_enc_coconut_bandwidth_credential_v1( - enc_credential, - &self.client.shared_keys, - iv, - )?; + let spent = self + .inner + .shared_state + .ecash_verifier + .check_double_spend(serial_number) + .await; - self.handle_bandwidth_request(credential.try_into()?).await + if spent { + trace!("the credential has already been spent before at some gateway before (bloomfilter failure)"); + return Err(RequestHandlingError::BandwidthCredentialAlreadySpent); + } + Ok(()) + } + + fn check_credential_spending_date( + &self, + proposed: Date, + today: Date, + ) -> Result<(), RequestHandlingError> { + trace!("checking ticket spending date..."); + + if today != proposed { + trace!("invalid credential spending date. received {proposed}"); + return Err(RequestHandlingError::InvalidCredentialSpendingDate { + got: proposed, + expected: today, + }); + } + Ok(()) + } + + async fn cryptographically_verify_ticket( + &self, + credential: &CredentialSpendingRequest, + ) -> Result<(), RequestHandlingError> { + trace!("attempting to perform ticket verification..."); + + let aggregated_verification_key = self + .inner + .shared_state + .ecash_verifier + .verification_key(credential.data.epoch_id) + .await?; + + self.inner + .shared_state + .ecash_verifier + .check_payment(&credential.data, &aggregated_verification_key) + .await?; + Ok(()) + } + + fn async_verify_ticket(&self, ticket: CredentialSpendingData, ticket_id: i64) { + let client_ticket = ClientTicket::new(ticket, ticket_id); + + self.inner + .shared_state + .ecash_verifier + .async_verify(client_ticket); + } + + async fn store_received_ticket( + &self, + ticket_data: &CredentialSpendingRequest, + received_at: OffsetDateTime, + ) -> Result { + trace!("storing received ticket"); + let ticket_id = self + .inner + .shared_state + .storage + .insert_received_ticket( + self.client.id, + received_at, + ticket_data.encoded_serial_number(), + ticket_data.to_bytes(), + ) + .await?; + Ok(ticket_id) } /// Tries to handle the received bandwidth request by checking correctness of the received data @@ -421,23 +391,55 @@ where /// /// # Arguments /// - /// * `enc_credential`: raw encrypted bandwidth credential to verify. + /// * `enc_credential`: raw encrypted credential to verify. /// * `iv`: fresh iv used for the credential. - async fn handle_bandwidth_v2( + async fn handle_ecash_bandwidth( &mut self, enc_credential: Vec, iv: Vec, ) -> Result { - debug!("handling v2 bandwidth request"); + let received_at = OffsetDateTime::now_utc(); + // TODO: change it into a span field instead once we move to tracing + debug!( + "handling e-cash bandwidth request from {}", + self.client.address + ); - let iv = IV::try_from_bytes(&iv)?; - let credential = ClientControlRequest::try_from_enc_coconut_bandwidth_credential_v2( + let credential = ClientControlRequest::try_from_enc_ecash_credential( enc_credential, &self.client.shared_keys, iv, )?; + let spend_date = ecash_today(); - self.handle_bandwidth_request(credential).await + // check if the credential hasn't been spent before + let serial_number = credential.data.encoded_serial_number(); + + if credential.data.payment.spend_value != 1 { + return Err(RequestHandlingError::MultipleTickets); + } + + self.check_credential_spending_date(credential.data.spend_date, spend_date.ecash_date())?; + self.check_bloomfilter(&serial_number).await?; + self.check_local_db_for_double_spending(&serial_number) + .await?; + + // TODO: do we HAVE TO do it? + self.cryptographically_verify_ticket(&credential).await?; + + let ticket_id = self.store_received_ticket(&credential, received_at).await?; + self.async_verify_ticket(credential.data, ticket_id); + + // TODO: double storing? + // self.store_spent_credential(serial_number_bs58).await?; + + let bandwidth = Bandwidth::ticket_amount(Default::default()); + + self.increase_bandwidth(bandwidth, spend_date).await?; + + let available_total = self.client_bandwidth.bandwidth.bytes; + + Ok(ServerResponse::Bandwidth { available_total }) } async fn handle_claim_testnet_bandwidth( @@ -449,29 +451,47 @@ where return Err(RequestHandlingError::OnlyCoconutCredentials); } - self.increase_bandwidth(FREE_TESTNET_BANDWIDTH_VALUE) + self.increase_bandwidth(FREE_TESTNET_BANDWIDTH_VALUE, ecash_today()) .await?; let available_total = self.client_bandwidth.bandwidth.bytes; Ok(ServerResponse::Bandwidth { available_total }) } - async fn flush_bandwidth(&mut self) -> Result<(), RequestHandlingError> { - trace!("flushing client bandwidth to the underlying storage"); + async fn sync_expiration(&mut self) -> Result<(), RequestHandlingError> { self.inner + .shared_state .storage - .set_bandwidth(self.client.address, self.client_bandwidth.bandwidth.bytes) + .set_expiration(self.client.id, self.client_bandwidth.bandwidth.expiration) .await?; - self.client_bandwidth.update_flush_data(); Ok(()) } + #[instrument(level = "trace", skip_all)] + async fn sync_bandwidth(&mut self) -> Result<(), RequestHandlingError> { + trace!("syncing client bandwidth with the underlying storage"); + let updated = self + .inner + .shared_state + .storage + .increase_bandwidth(self.client.id, self.client_bandwidth.bytes_delta_since_sync) + .await?; + + trace!(updated); + + self.client_bandwidth.bandwidth.bytes = updated; + + self.client_bandwidth.update_sync_data(); + Ok(()) + } + + #[instrument(skip_all)] async fn try_use_bandwidth( &mut self, required_bandwidth: i64, ) -> Result { - if self.client_bandwidth.bandwidth.freepass_expired() { - self.expire_freepass().await?; + if self.client_bandwidth.bandwidth.expired() { + self.expire_bandwidth().await?; } let available_bandwidth = self.client_bandwidth.bandwidth.bytes; @@ -482,8 +502,12 @@ where }); } + let available_bi2 = bibytes2(available_bandwidth as f64); + let required_bi2 = bibytes2(required_bandwidth as f64); + debug!(available = available_bi2, required = required_bi2); + self.consume_bandwidth(required_bandwidth).await?; - Ok(self.client_bandwidth.bandwidth.bytes) + Ok(available_bandwidth) } /// Tries to handle request to forward sphinx packet into the network. The request can only succeed @@ -494,6 +518,7 @@ where /// # Arguments /// /// * `mix_packet`: packet received from the client that should get forwarded into the network. + #[instrument(skip_all)] async fn handle_forward_sphinx( &mut self, mix_packet: MixPacket, @@ -543,14 +568,22 @@ where match ClientControlRequest::try_from(raw_request) { Err(e) => RequestHandlingError::InvalidTextRequest(e).into_error_message(), Ok(request) => match request { - ClientControlRequest::BandwidthCredential { enc_credential, iv } => self - .handle_bandwidth_v1(enc_credential, iv) - .await - .into_ws_message(), - ClientControlRequest::BandwidthCredentialV2 { enc_credential, iv } => self - .handle_bandwidth_v2(enc_credential, iv) + ClientControlRequest::EcashCredential { enc_credential, iv } => self + .handle_ecash_bandwidth(enc_credential, iv) .await .into_ws_message(), + ClientControlRequest::BandwidthCredential { .. } => { + RequestHandlingError::IllegalRequest { + additional_context: "coconut credential are not longer supported".into(), + } + .into_error_message() + } + ClientControlRequest::BandwidthCredentialV2 { .. } => { + RequestHandlingError::IllegalRequest { + additional_context: "coconut credential are not longer supported".into(), + } + .into_error_message() + } ClientControlRequest::ClaimFreeTestnetBandwidth => self .handle_claim_testnet_bandwidth() .await diff --git a/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs b/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs deleted file mode 100644 index 26a7e76640..0000000000 --- a/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs +++ /dev/null @@ -1,284 +0,0 @@ -// Copyright 2022-2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use super::authenticated::RequestHandlingError; -use log::*; -use nym_credentials_interface::VerificationKey; -use nym_gateway_requests::models::CredentialSpendingRequest; -use nym_validator_client::coconut::all_coconut_api_clients; -use nym_validator_client::nym_api::EpochId; -use nym_validator_client::nyxd::contract_traits::{MultisigQueryClient, NymContractsProvider}; -use nym_validator_client::nyxd::AccountId; -use nym_validator_client::{ - nyxd::{ - contract_traits::{CoconutBandwidthSigningClient, DkgQueryClient, MultisigSigningClient}, - cosmwasm_client::logs::{find_attribute, BANDWIDTH_PROPOSAL_ID}, - Coin, - }, - CoconutApiClient, DirectSigningHttpRpcNyxdClient, -}; -use std::collections::HashMap; -use std::ops::Deref; -use tokio::sync::{RwLock, RwLockReadGuard}; - -pub(crate) struct CoconutVerifier { - address: AccountId, - nyxd_client: RwLock, - - // **CURRENTLY** api client addresses don't change during the epochs - api_clients: RwLock>>, - - // keys never change during epochs - master_keys: RwLock>, - mix_denom_base: String, -} - -impl CoconutVerifier { - pub async fn new( - nyxd_client: DirectSigningHttpRpcNyxdClient, - only_coconut_credentials: bool, - ) -> Result { - let mix_denom_base = nyxd_client.current_chain_details().mix_denom.base.clone(); - let address = nyxd_client.address(); - - let mut master_keys = HashMap::new(); - let mut api_clients = HashMap::new(); - - // don't make it a hard failure in case we're running on mainnet (where DKG hasn't been deployed yet) - if nyxd_client.dkg_contract_address().is_none() { - if !only_coconut_credentials { - warn!( - "the DKG contract address is not available - \ - no coconut credentials will be redeemable \ - (if the DKG ceremony hasn't been run yet this warning is expected)" - ); - } else { - // if we require coconut credentials, we MUST have DKG contract available - return Err(RequestHandlingError::UnavailableDkgContract); - } - - return Ok(CoconutVerifier { - address, - nyxd_client: RwLock::new(nyxd_client), - api_clients: Default::default(), - master_keys: Default::default(), - mix_denom_base, - }); - } - - let Ok(current_epoch) = nyxd_client.get_current_epoch().await else { - // another case of somebody putting a placeholder address that doesn't exist - error!("the specified DKG contract address is invalid - no coconut credentials will be redeemable"); - if only_coconut_credentials { - // if we require coconut credentials, we MUST have DKG contract available - return Err(RequestHandlingError::UnavailableDkgContract); - } - return Ok(CoconutVerifier { - address, - nyxd_client: RwLock::new(nyxd_client), - api_clients: Default::default(), - master_keys: Default::default(), - mix_denom_base, - }); - }; - - // might as well obtain the key for the current epoch, if applicable - if current_epoch.state.is_in_progress() { - // note: even though we're constructing clients here, we will NOT be making any network requests - let epoch_api_clients = - all_coconut_api_clients(&nyxd_client, current_epoch.epoch_id).await?; - let threshold = nyxd_client.get_current_epoch_threshold().await?; - - // SAFETY: - // if epoch state is in the 'in progress' state, it means the threshold value MUST HAVE - // been established. if it wasn't, there's an underlying issue with the DKG contract in which - // case we shouldn't continue anyway because here be dragons - #[allow(clippy::expect_used)] - let threshold = threshold.expect("unavailable threshold value") as usize; - if epoch_api_clients.len() < threshold { - return Err(RequestHandlingError::NotEnoughNymAPIs { - received: epoch_api_clients.len(), - needed: threshold, - }); - } - let aggregated_verification_key = - nym_credentials::obtain_aggregate_verification_key(&epoch_api_clients)?; - - api_clients.insert(current_epoch.epoch_id, epoch_api_clients); - master_keys.insert(current_epoch.epoch_id, aggregated_verification_key); - } - - Ok(CoconutVerifier { - address, - nyxd_client: RwLock::new(nyxd_client), - api_clients: RwLock::new(api_clients), - master_keys: RwLock::new(master_keys), - mix_denom_base, - }) - } - - pub async fn api_clients( - &self, - epoch_id: EpochId, - ) -> Result>, RequestHandlingError> { - let guard = self.api_clients.read().await; - - // the key was already in the map - if let Ok(mapped) = RwLockReadGuard::try_map(guard, |clients| clients.get(&epoch_id)) { - trace!("we already had cached api clients for epoch {epoch_id}"); - return Ok(mapped); - } - - let api_clients = self.query_api_clients(epoch_id).await?; - trace!( - "obtained {} api clients for epoch {epoch_id} from the contract", - api_clients.len() - ); - - // EDGE CASE: - // if this epoch is from the past, we can't query for its threshold - // we can only hope that enough valid keys were submitted - // the best we can do is check if we have at least a api - if api_clients.is_empty() { - return Err(RequestHandlingError::NotEnoughNymAPIs { - received: 0, - needed: 1, - }); - } - - let mut guard = self.api_clients.write().await; - guard.insert(epoch_id, api_clients); - let guard = guard.downgrade(); - trace!("stored api clients for epoch {epoch_id}"); - - // SAFETY: - // we just inserted the entry into the map while NEVER dropping the lock (only downgraded it) - // so it MUST exist and thus the unwrap is fine - #[allow(clippy::unwrap_used)] - Ok(RwLockReadGuard::map(guard, |clients| { - clients.get(&epoch_id).unwrap() - })) - } - - pub async fn verification_key( - &self, - epoch_id: EpochId, - ) -> Result, RequestHandlingError> { - let guard = self.master_keys.read().await; - - // the key was already in the map - if let Ok(mapped) = RwLockReadGuard::try_map(guard, |keys| keys.get(&epoch_id)) { - trace!("we already had cached verification key for epoch {epoch_id}"); - return Ok(mapped); - } - - let api_clients = self.api_clients(epoch_id).await?; - trace!( - "attempting to obtain verification key from {} api clients", - api_clients.len() - ); - - let aggregated_verification_key = - nym_credentials::obtain_aggregate_verification_key(&api_clients)?; - - let mut guard = self.master_keys.write().await; - guard.insert(epoch_id, aggregated_verification_key); - let guard = guard.downgrade(); - trace!("stored aggregated verification key for epoch {epoch_id}"); - - // SAFETY: - // we just inserted the entry into the map while NEVER dropping the lock (only downgraded it) - // so it MUST exist and thus the unwrap is fine - #[allow(clippy::unwrap_used)] - Ok(RwLockReadGuard::map(guard, |keys| { - keys.get(&epoch_id).unwrap() - })) - } - - pub async fn query_api_clients( - &self, - epoch_id: u64, - ) -> Result, RequestHandlingError> { - Ok(all_coconut_api_clients(self.nyxd_client.read().await.deref(), epoch_id).await?) - } - - pub async fn release_bandwidth_voucher_funds( - &self, - api_clients: &[CoconutApiClient], - credential: CredentialSpendingRequest, - ) -> Result<(), RequestHandlingError> { - if !credential.data.typ.is_voucher() { - unimplemented!() - } - - // safety: the voucher funds are released after the credential has already been verified locally - // and the underlying bandwidth value has been extracted, so the below MUST succeed - let voucher_amount = credential.unchecked_voucher_value() as u128; - - let blinded_serial_number = credential - .data - .verify_credential_request - .blinded_serial_number_bs58(); - - let res = self - .nyxd_client - .write() - .await - .spend_credential( - Coin::new(voucher_amount, &self.mix_denom_base), - blinded_serial_number, - self.address.to_string(), - None, - ) - .await?; - let proposal_id = find_attribute(&res.logs, "wasm", BANDWIDTH_PROPOSAL_ID) - .ok_or(RequestHandlingError::ProposalIdError { - reason: String::from("proposal id not found"), - })? - .value - .parse::() - .map_err(|_| RequestHandlingError::ProposalIdError { - reason: String::from("proposal id could not be parsed to u64"), - })?; - - let proposal = self - .nyxd_client - .read() - .await - .query_proposal(proposal_id) - .await?; - if !credential.matches_blinded_serial_number(&proposal.description)? { - return Err(RequestHandlingError::ProposalIdError { - reason: String::from("proposal has different serial number"), - }); - } - - let req = nym_api_requests::coconut::VerifyCredentialBody::new( - credential.data, - proposal_id, - self.address.clone(), - ); - for client in api_clients { - let ret = client.api_client.verify_bandwidth_credential(&req).await; - let client_url = client.api_client.nym_api.current_url(); - match ret { - Ok(res) => { - if !res.verification_result { - warn!("Validator at {client_url} didn't accept the credential. It will probably vote No on the spending proposal"); - } - } - Err(err) => { - warn!("Validator at {client_url} could not be reached. There might be a problem with the coconut endpoint: {err}"); - } - } - } - - self.nyxd_client - .write() - .await - .execute_proposal(proposal_id, None) - .await?; - - Ok(()) - } -} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/ecash/credential_sender.rs b/gateway/src/node/client_handling/websocket/connection_handler/ecash/credential_sender.rs new file mode 100644 index 0000000000..dcc1bb62bf --- /dev/null +++ b/gateway/src/node/client_handling/websocket/connection_handler/ecash/credential_sender.rs @@ -0,0 +1,948 @@ +// Copyright 2022-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::client_handling::bandwidth::Bandwidth; +use crate::node::client_handling::websocket::connection_handler::ecash::error::EcashTicketError; +use crate::node::client_handling::websocket::connection_handler::ecash::helpers::for_each_api_concurrent; +use crate::node::client_handling::websocket::connection_handler::ecash::state::SharedState; +use crate::GatewayError; +use cosmwasm_std::Fraction; +use cw_utils::ThresholdResponse; +use futures::channel::mpsc::UnboundedReceiver; +use futures::{Stream, StreamExt}; +use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY; +use nym_api_requests::ecash::models::{BatchRedeemTicketsBody, VerifyEcashTicketBody}; +use nym_credentials_interface::ClientTicket; +use nym_gateway_storage::Storage; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nyxd::contract_traits::{ + EcashSigningClient, MultisigQueryClient, MultisigSigningClient, PagedMultisigQueryClient, +}; +use nym_validator_client::nyxd::cosmwasm_client::ToSingletonContractData; +use nym_validator_client::nyxd::cw3::Status; +use nym_validator_client::nyxd::AccountId; +use nym_validator_client::EcashApiClient; +use si_scale::helpers::bibytes2; +use std::collections::{HashMap, HashSet}; +use std::ops::Deref; +use std::sync::atomic::{AtomicUsize, Ordering}; +use time::OffsetDateTime; +use tokio::sync::{Mutex, RwLockReadGuard}; +use tokio::time::{interval_at, Duration, Instant}; +use tracing::{debug, error, info, instrument, trace, warn}; + +enum ProposalResult { + Executed, + Rejected, + Pending, +} + +impl ProposalResult { + fn is_pending(&self) -> bool { + matches!(self, ProposalResult::Pending) + } + + fn is_rejected(&self) -> bool { + matches!(self, ProposalResult::Rejected) + } +} + +struct PendingVerification { + ticket: ClientTicket, + + // vec of node ids of apis that haven't sent a valid response + pending: Vec, +} + +impl PendingVerification { + fn new(ticket: ClientTicket, pending: Vec) -> Self { + PendingVerification { ticket, pending } + } + + fn to_request_body(&self, gateway_cosmos_addr: AccountId) -> VerifyEcashTicketBody { + VerifyEcashTicketBody { + // TODO: redundant clone + credential: self.ticket.spending_data.clone(), + gateway_cosmos_addr, + } + } +} + +struct PendingRedemptionVote { + proposal_id: u64, + digest: Vec, + included_serial_numbers: Vec>, + epoch_id: EpochId, + + // vec of node ids of apis that haven't sent a valid response + pending: Vec, +} + +impl PendingRedemptionVote { + fn new( + proposal_id: u64, + digest: Vec, + included_serial_numbers: Vec>, + epoch_id: EpochId, + pending: Vec, + ) -> Self { + PendingRedemptionVote { + proposal_id, + digest, + included_serial_numbers, + epoch_id, + pending, + } + } + + fn to_request_body(&self, gateway_cosmos_addr: AccountId) -> BatchRedeemTicketsBody { + BatchRedeemTicketsBody::new( + self.digest.clone(), + self.proposal_id, + self.included_serial_numbers.clone(), + gateway_cosmos_addr, + ) + } +} + +pub(crate) struct CredentialHandlerConfig { + /// Specifies the multiplier for revoking a malformed/double-spent ticket + /// (if it has to go all the way to the nym-api for verification) + /// e.g. if one ticket grants 100Mb and `revocation_bandwidth_penalty` is set to 1.5, + /// the client will lose 150Mb + pub(crate) revocation_bandwidth_penalty: f32, + + /// Specifies the interval for attempting to resolve any failed, pending operations, + /// such as ticket verification or redemption. + pub(crate) pending_poller: Duration, + + pub(crate) minimum_api_quorum: f32, + + /// Specifies the minimum number of tickets this gateway will attempt to redeem. + pub(crate) minimum_redemption_tickets: usize, + + /// Specifies the maximum time between two subsequent tickets redemptions. + /// That's required as nym-apis will purge all ticket information for tickets older than maximum validity. + pub(crate) maximum_time_between_redemption: Duration, +} + +pub(crate) struct CredentialHandler { + config: CredentialHandlerConfig, + multisig_threshold: f32, + ticket_receiver: UnboundedReceiver, + shared_state: SharedState, + pending_tickets: Vec, + pending_redemptions: Vec, +} + +impl CredentialHandler +where + St: Storage + Clone + 'static, +{ + async fn rebuild_pending_tickets( + shared_state: &SharedState, + ) -> Result, EcashTicketError> { + // 1. get all tickets that were not fully verified + let unverified = shared_state.storage.get_all_unverified_tickets().await?; + let mut pending = Vec::with_capacity(unverified.len()); + + // a lookup of ids for signers for given epoch + let mut apis_lookup = HashMap::new(); + + // 2. for each of them, reconstruct missing votes + for ticket in unverified { + let epoch = ticket.spending_data.epoch_id; + assert!(epoch <= i64::MAX as u64); + let signers = match apis_lookup.get(&epoch) { + Some(signers) => signers, + None => { + // get all signers for given epoch + let signers = shared_state.storage.get_signers(epoch as i64).await?; + apis_lookup.insert(epoch, signers); + + // safety: we just inserted that entry + #[allow(clippy::unwrap_used)] + apis_lookup.get(&epoch).unwrap() + } + }; + // get all votes the ticket received + let votes = shared_state + .storage + .get_votes(ticket.ticket_id) + .await? + .into_iter() + .collect::>(); + let mut missing_votes = Vec::new(); + for signer in signers { + // for each signer, check if they have actually voted; if not, that's the missing guy + if !votes.contains(signer) { + missing_votes.push(*signer as u64) + } + } + pending.push(PendingVerification { + ticket, + pending: missing_votes, + }) + } + Ok(pending) + } + + async fn rebuild_pending_votes( + shared_state: &SharedState, + ) -> Result, EcashTicketError> { + // 1. get all tickets that were not fully verified + let unverified = shared_state.storage.get_all_unresolved_proposals().await?; + let mut pending = Vec::with_capacity(unverified.len()); + + let epoch_id = shared_state.current_epoch_id().await?; + let apis = shared_state + .api_clients(epoch_id) + .await? + .iter() + .map(|s| (s.cosmos_address.to_string(), s.node_id)) + .collect::>(); + + for proposal_id in unverified { + // get all of the votes + let votes = shared_state + .start_query() + .await + .get_all_votes(proposal_id as u64) + .await + .map_err(EcashTicketError::chain_query_failure)? + .into_iter() + .map(|v| v.voter) + .collect::>(); + + let mut missing_votes = Vec::new(); + + // see who hasn't voted + for (api_address, api_id) in &apis { + // for each signer, check if they have actually voted; if not, that's the missing guy + if !votes.contains(api_address) { + missing_votes.push(*api_id) + } + } + + // attempt to rebuild SN and digest from the proposal info + storage data + let proposal_info = shared_state + .start_query() + .await + .query_proposal(proposal_id as u64) + .await + .map_err(EcashTicketError::chain_query_failure)?; + + let tickets = shared_state + .storage + .get_all_proposed_tickets_with_sn(proposal_id as u32) + .await?; + let digest = + BatchRedeemTicketsBody::make_digest(tickets.iter().map(|t| &t.serial_number)); + let encoded_digest = bs58::encode(&digest).into_string(); + if encoded_digest != proposal_info.description { + error!("the lost proposal {proposal_id} does not have a matching digest!"); + continue; + } + + pending.push(PendingRedemptionVote { + proposal_id: proposal_id as u64, + digest, + included_serial_numbers: tickets.into_iter().map(|t| t.serial_number).collect(), + epoch_id, + pending: missing_votes, + }) + } + + Ok(pending) + } + + pub(crate) async fn new( + config: CredentialHandlerConfig, + ticket_receiver: UnboundedReceiver, + shared_state: SharedState, + ) -> Result { + let multisig_threshold = shared_state + .nyxd_client + .read() + .await + .query_threshold() + .await?; + + let ThresholdResponse::AbsolutePercentage { percentage, .. } = multisig_threshold else { + return Err(GatewayError::InvalidMultisigThreshold); + }; + + // that's a nasty conversion, but it works : ) + let multisig_threshold = + percentage.numerator().u128() as f32 / percentage.denominator().u128() as f32; + + // on startup read pending credentials and api responses from the storage + let pending_tickets = Self::rebuild_pending_tickets(&shared_state).await?; + + // on startup read pending proposals from the storage + // then reconstruct the votes by querying the multisig contract for votes on those proposals + // digest from the description and count from the message + let pending_redemptions = Self::rebuild_pending_votes(&shared_state).await?; + + Ok(CredentialHandler { + config, + multisig_threshold, + ticket_receiver, + shared_state, + pending_tickets, + pending_redemptions, + }) + } + + // the argument is temporary as we'll be reading from the storage + async fn create_redemption_proposal( + &self, + commitment: &[u8], + number_of_tickets: u16, + ) -> Result { + let res = self + .shared_state + .start_tx() + .await + .request_ticket_redemption( + bs58::encode(commitment).into_string(), + number_of_tickets, + None, + ) + .await + .map_err(|source| EcashTicketError::RedemptionProposalCreationFailure { source })?; + + // that one is quite tricky because proposal exists on chain, but we didn't get the id... + // but it should be quite impossible to ever reach this unless we make breaking changes + let proposal_id = res + .parse_singleton_u64_contract_data() + .inspect_err(|err| error!("reached seemingly impossible error! could not recover the redemption proposal id: {err}")) + .map_err(|source| EcashTicketError::ProposalIdParsingFailure { source })?; + + info!("created redemption proposal {proposal_id} to redeem {number_of_tickets} tickets"); + + Ok(proposal_id) + } + + /// Attempt to send ticket verification request to the provided ecash verifier. + async fn verify_ticket( + &self, + ticket_id: i64, + request: &VerifyEcashTicketBody, + client: &EcashApiClient, + ) -> Result { + match client.api_client.verify_ecash_ticket(request).await { + Ok(res) => { + let accepted = match res.verified { + Ok(_) => { + trace!("{client} has accepted ticket {ticket_id}"); + true + } + Err(rejection) => { + warn!("{client} has rejected ticket {ticket_id}: {rejection}"); + false + } + }; + self.shared_state + .storage + .insert_ticket_verification( + ticket_id, + client.node_id as i64, + OffsetDateTime::now_utc(), + accepted, + ) + .await?; + Ok(accepted) + } + Err(err) => { + error!("failed to send ticket {ticket_id} for verification to ecash signer '{client}': {err}. if we don't reach quorum, we'll retry later"); + Ok(false) + } + } + } + + #[instrument(skip(self))] + async fn revoke_ticket_bandwidth(&self, ticket_id: i64) -> Result<(), EcashTicketError> { + warn!("revoking bandwidth associated with ticket {ticket_id} since it failed verification"); + + let bytes_to_revoke = Bandwidth::ticket_amount(Default::default()).value() as f32 + * self.config.revocation_bandwidth_penalty; + let to_revoke_bi2 = bibytes2(bytes_to_revoke as f64); + + info!(to_revoke_bi2); + + self.shared_state + .storage + .revoke_ticket_bandwidth(ticket_id, bytes_to_revoke as i64) + .await?; + Ok(()) + } + + /// Attempt to send the pending ticket to all ecash verifiers that haven't yet returned valid response. + async fn send_pending_ticket_for_verification( + &self, + pending: &mut PendingVerification, + api_clients: Option>>, + ) -> Result { + let ticket_id = pending.ticket.ticket_id; + let api_clients = match api_clients { + Some(clients) => clients, + None => { + self.shared_state + .api_clients(pending.ticket.spending_data.epoch_id) + .await? + } + }; + + let verification_request = pending.to_request_body(self.shared_state.address.clone()); + + let total = api_clients.len(); + let api_failures = Mutex::new(Vec::new()); + let rejected = AtomicUsize::new(0); + + // this vector will never contain more than ~30 entries so linear lookup is fine. + // it's probably even faster than hashset due to overhead + futures::stream::iter( + api_clients + .deref() + .iter() + .filter(|client| pending.pending.contains(&client.node_id)), + ) + .for_each_concurrent(32, |ecash_client| async { + // errors are only returned on hard, storage, failures + match self + .verify_ticket( + pending.ticket.ticket_id, + &verification_request, + ecash_client, + ) + .await + { + Err(err) => { + error!("internal failure. could not proceed with ticket verification: {err}"); + api_failures.lock().await.push(ecash_client.node_id); + } + Ok(false) => { + rejected.fetch_add(1, Ordering::SeqCst); + } + _ => {} + } + }) + .await; + + let api_failures = api_failures.into_inner(); + let num_failures = api_failures.len(); + pending.pending = api_failures; + + let rejected = rejected.into_inner(); + let rejected_ratio = rejected as f32 / total as f32; + let rejected_perc = rejected_ratio * 100.; + if rejected_ratio >= (1. - self.config.minimum_api_quorum) { + error!("{rejected_perc:.2}% of signers rejected ticket {ticket_id}. we won't be able to redeem it"); + + self.shared_state + .storage + .update_rejected_ticket(pending.ticket.ticket_id) + .await?; + self.revoke_ticket_bandwidth(pending.ticket.ticket_id) + .await?; + } + + let accepted_ratio = (total - rejected - num_failures) as f32 / total as f32; + let accepted_perc = accepted_ratio * 100.; + match accepted_ratio { + n if n < self.multisig_threshold => error!("less than 2/3 of signers ({accepted_perc:.2}%) accepted ticket {ticket_id}. we won't be able to spend it"), + n if n < self.config.minimum_api_quorum => warn!("less than 80%, but more than 67% of signers ({accepted_perc:.2}%) accepted ticket {ticket_id}. technically we could redeem it, but we'll wait for the bigger quorum"), + _ => { + trace!("{accepted_perc:.2}% of signers accepted ticket {ticket_id}"); + self.shared_state.storage.update_verified_ticket(pending.ticket.ticket_id).await?; + return Ok(true) + } + } + + Ok(false) + } + + async fn send_ticket_for_verification( + &mut self, + ticket: ClientTicket, + ) -> Result<(), EcashTicketError> { + let api_clients = self + .shared_state + .api_clients(ticket.spending_data.epoch_id) + .await?; + + let ids = api_clients.iter().map(|c| c.node_id).collect(); + let mut pending = PendingVerification::new(ticket, ids); + + let got_quorum = self + .send_pending_ticket_for_verification(&mut pending, Some(api_clients)) + .await?; + if !got_quorum { + debug!("failed to reach quorum for ticket {}. apis: {:?} haven't responded. we'll retry later", pending.ticket.ticket_id, pending.pending); + self.pending_tickets.push(pending); + } else { + // since we reached the quorum we no longer need to hold the ticket's binary data + self.shared_state + .storage + .remove_verified_ticket_binary_data(pending.ticket.ticket_id) + .await?; + } + + Ok(()) + } + + async fn handle_client_ticket(&mut self, ticket: ClientTicket) { + // attempt to send for verification + let ticket_id = ticket.ticket_id; + if let Err(err) = self.send_ticket_for_verification(ticket).await { + error!("failed to verify ticket {ticket_id}: {err}") + } + } + + async fn resolve_pending(&mut self) -> Result<(), EcashTicketError> { + let mut still_failing = Vec::new(); + + // 1. attempt to resolve all pending proposals + while let Some(mut pending) = self.pending_redemptions.pop() { + match self.try_resolve_pending_proposal(&mut pending, None).await { + Ok(resolution) => { + if resolution.is_pending() { + warn!("still failed to reach quorum for proposal {}. apis: {:?} haven't responded. we'll retry later", pending.proposal_id, pending.pending); + still_failing.push(pending); + } else { + self.shared_state + .storage + .clear_post_proposal_data( + pending.proposal_id as u32, + OffsetDateTime::now_utc(), + resolution.is_rejected(), + ) + .await?; + } + } + Err(err) => { + error!("experienced internal error when attempting to resolve pending proposal: {err}"); + // make sure to update internal state to not lose any data + self.pending_redemptions.push(pending); + self.pending_redemptions.append(&mut still_failing); + return Err(err); + } + } + } + + let mut still_failing = Vec::new(); + + // 2. attempt to verify the remaining tickets + while let Some(mut pending) = self.pending_tickets.pop() { + // possible optimisation: if there's a lot of pending tickets, pre-emptively grab locks for api_clients + match self + .send_pending_ticket_for_verification(&mut pending, None) + .await + { + Ok(got_quorum) => { + if !got_quorum { + warn!("still failed to reach quorum for ticket {}. apis: {:?} haven't responded. we'll retry later", pending.ticket.ticket_id, pending.pending); + still_failing.push(pending); + } else { + // since we reached the quorum we no longer need to hold the ticket's binary data + self.shared_state + .storage + .remove_verified_ticket_binary_data(pending.ticket.ticket_id) + .await?; + } + } + Err(err) => { + error!("experienced internal error when attempting to resolve pending ticket: {err}"); + // make sure to update internal state to not lose any data + self.pending_tickets.push(pending); + self.pending_tickets.append(&mut still_failing); + return Err(err); + } + } + } + // at this point self.pending_tickets is empty + self.pending_tickets = still_failing; + Ok(()) + } + + /// Attempt to send batch redemption request to the provided ecash verifier. + async fn redeem_tickets( + &self, + proposal_id: u64, + request: &BatchRedeemTicketsBody, + client: &EcashApiClient, + ) -> Result { + match client.api_client.batch_redeem_ecash_tickets(request).await { + Ok(res) => { + let accepted = if res.proposal_accepted { + trace!("{client} has accepted proposal {proposal_id}"); + true + } else { + warn!("{client} has rejected proposal {proposal_id}"); + false + }; + + Ok(accepted) + } + Err(err) => { + error!("failed to send proposal {proposal_id} for redemption vote to ecash signer '{client}': {err}. if we don't reach quorum, we'll retry later"); + Ok(false) + } + } + } + + async fn try_execute_proposal(&self, proposal_id: u64) -> Result<(), EcashTicketError> { + self.shared_state + .start_tx() + .await + .execute_proposal(proposal_id, None) + .await + .map_err( + |source| EcashTicketError::RedemptionProposalExecutionFailure { + proposal_id, + source, + }, + )?; + Ok(()) + } + + async fn get_proposal_status(&self, proposal_id: u64) -> Result { + Ok(self + .shared_state + .start_query() + .await + .query_proposal(proposal_id) + .await + .map_err(EcashTicketError::chain_query_failure)? + .status) + } + + async fn try_finalize_proposal( + &self, + proposal_id: u64, + ) -> Result { + match self.get_proposal_status(proposal_id).await? { + Status::Pending => { + // the voting hasn't even begun! + error!("impossible case! the proposal {proposal_id} is still pending"); + Ok(ProposalResult::Pending) + } + Status::Open => { + debug!("proposal {proposal_id} is still open and needs more votes"); + Ok(ProposalResult::Pending) + } + Status::Rejected => { + warn!("proposal {proposal_id} has been rejected"); + Ok(ProposalResult::Rejected) + } + Status::Passed => { + info!( + "proposal {proposal_id} has already been passed - we just need to execute it" + ); + self.try_execute_proposal(proposal_id).await?; + info!("executed proposal {proposal_id}"); + Ok(ProposalResult::Executed) + } + Status::Executed => { + info!("proposal {proposal_id} has already been executed - nothing to do!"); + Ok(ProposalResult::Executed) + } + } + } + + async fn try_resolve_pending_proposal( + &self, + pending: &mut PendingRedemptionVote, + api_clients: Option>>, + ) -> Result { + let proposal_id = pending.proposal_id; + + info!( + "attempting to resolve pending redemption proposal {proposal_id} to redeem {} tickets", + pending.included_serial_numbers.len() + ); + + // check if the proposal still needs more votes from the apis + let result = self.try_finalize_proposal(proposal_id).await?; + if !result.is_pending() { + return Ok(result); + } + + let api_clients = match api_clients { + Some(clients) => clients, + None => self.shared_state.api_clients(pending.epoch_id).await?, + }; + + let redemption_request = pending.to_request_body(self.shared_state.address.clone()); + + // TODO: optimisation: tell other apis they can purge our tickets even if they haven't voted + + let total = api_clients.len(); + let api_failures = Mutex::new(Vec::new()); + let rejected = AtomicUsize::new(0); + + for_each_api_concurrent(&api_clients, &pending.pending, |ecash_client| async { + // errors are only returned on hard, storage, failures + match self + .redeem_tickets(pending.proposal_id, &redemption_request, ecash_client) + .await + { + Err(err) => { + error!("internal failure. could not proceed with ticket redemption: {err}"); + api_failures.lock().await.push(ecash_client.node_id); + } + Ok(false) => { + rejected.fetch_add(1, Ordering::SeqCst); + } + _ => {} + } + }) + .await; + + let api_failures = api_failures.into_inner(); + let num_failures = api_failures.len(); + pending.pending = api_failures; + + let rejected = rejected.into_inner(); + let rejected_ratio = rejected as f32 / total as f32; + let rejected_perc = rejected_ratio * 100.; + if rejected_ratio >= (1. - self.multisig_threshold) { + error!("{rejected_perc:.2}% of signers rejected proposal {proposal_id}. we won't be able to execute it"); + // no need to query the chain as with so many rejections it's impossible it has passed. + return Ok(ProposalResult::Rejected); + } + + let accepted_ratio = (total - rejected - num_failures) as f32 / total as f32; + let accepted_perc = accepted_ratio * 100.; + match accepted_ratio { + n if n < self.multisig_threshold => { + error!("less than 2/3 of signers ({accepted_perc:.2}%) accepted proposal {proposal_id}. we're not yet be able to execute it to get funds out"); + return Ok(ProposalResult::Pending); + } + n if n < self.config.minimum_api_quorum => { + warn!("the system seems to be a bit unstable: less than 80%, but more than 67% of signers ({accepted_perc:.2}%) accepted proposal {proposal_id}"); + } + _ => { + trace!("{accepted_perc:.2}% of signers accepted proposal {proposal_id}"); + } + } + + // attempt to execute the proposal if it reached the required threshold + self.try_finalize_proposal(proposal_id).await + } + + async fn maybe_redeem_tickets(&mut self) -> Result<(), EcashTicketError> { + if !self.pending_tickets.is_empty() { + return Err(EcashTicketError::PendingTickets); + } + + let latest_stored = self.shared_state.storage.latest_proposal().await?; + + // check if we have already created the proposal but crashed before persisting it in the db + // + // if we have some persisted proposals in storage, try to see if there's anything more recent on chain + // (i.e. the missing proposal) + // if not (i.e. this would have been our first) check the latest page of proposals. + // while this is not ideal, realistically speaking we probably crashed few minutes ago + // and worst case scenario we'll just recreate the proposal instead + // + // LIMITATION: if MULTIPLE proposals got created in between, well. though luck. + let latest_on_chain = if let Some(latest_stored) = &latest_stored { + // those are sorted in ASCENDING way + self.shared_state + .proposals_since(latest_stored.proposal_id as u64) + .await? + .pop() + } else { + // but those are DESCENDING + self.shared_state + .last_proposal_page() + .await? + .first() + .cloned() + }; + + let now = OffsetDateTime::now_utc(); + + let prior_proposal = match (&latest_stored, latest_on_chain) { + (None, None) => { + // we haven't created any proposals before + trace!("this could be our first redemption proposal"); + None + } + (Some(stored), None) => { + if stored.created_at + MIN_BATCH_REDEMPTION_DELAY > now { + trace!("too soon to create new redemption proposal"); + return Ok(()); + } + None + } + (_, Some(on_chain)) => { + warn!("we seem to have crashed after creating proposal, but before persisting it onto disk!"); + + Some(on_chain) + } + }; + + // technically we could have been just caching all of those serial numbers as we verify tickets, + // but given how infrequently we call this, there's no point in wasting this memory + let verified_tickets = self + .shared_state + .storage + .get_all_verified_tickets_with_sn() + .await?; + + // TODO: somehow simplify that nasty nested if + if verified_tickets.len() < self.config.minimum_redemption_tickets { + // bypass the number of tickets check if we're about to lose our rewards due to expiration + if let Some(latest_stored) = latest_stored { + if latest_stored.created_at + self.config.maximum_time_between_redemption < now { + {} + } else { + debug!("we only have {} verified tickets. there's no point in creating a redemption request yet. (we need at least {} (configurable))", verified_tickets.len(), self.config.minimum_redemption_tickets); + return Ok(()); + } + } else { + // first proposal + debug!("we only have {} verified tickets. there's no point in creating a redemption request yet. (we need at least {} (configurable))", verified_tickets.len(), self.config.minimum_redemption_tickets); + return Ok(()); + } + } + + // this should have been ensured when querying + assert!(verified_tickets.len() <= u16::MAX as usize); + + let digest = + BatchRedeemTicketsBody::make_digest(verified_tickets.iter().map(|t| &t.serial_number)); + let encoded_digest = bs58::encode(&digest).into_string(); + + let prior_proposal_id = if let Some(prior_proposal) = prior_proposal { + if prior_proposal.description == encoded_digest { + info!("we have already created proposal for those tickets"); + Some(prior_proposal.id) + } else { + warn!( + "our missed proposal seem to have been for different tickets - abandoning it" + ); + None + } + } else { + None + }; + + // if the proposal has already existed on chain, do use it. otherwise create a new one + let proposal_id = if let Some(prior) = prior_proposal_id { + prior + } else { + self.create_redemption_proposal(&digest, verified_tickets.len() as u16) + .await? + }; + + if proposal_id > u32::MAX as u64 { + // realistically will we ever reach it? no. + panic!( + "we have created more than {} proposals. we can't handle that.", + u32::MAX + ) + } + + self.shared_state + .storage + .insert_redemption_proposal( + &verified_tickets, + proposal_id as u32, + OffsetDateTime::now_utc(), + ) + .await?; + + let current_epoch = self.shared_state.current_epoch_id().await?; + let api_clients = self.shared_state.api_clients(current_epoch).await?; + let ids = api_clients.iter().map(|c| c.node_id).collect(); + let mut pending = PendingRedemptionVote::new( + proposal_id, + digest, + verified_tickets + .into_iter() + .map(|t| t.serial_number) + .collect(), + current_epoch, + ids, + ); + + let resolution = self + .try_resolve_pending_proposal(&mut pending, Some(api_clients)) + .await?; + if resolution.is_pending() { + warn!("failed to reach quorum for proposal {proposal_id}. apis: {:?} haven't responded. we'll retry later", pending.pending); + self.pending_redemptions.push(pending); + } else { + self.shared_state + .storage + .clear_post_proposal_data( + proposal_id as u32, + OffsetDateTime::now_utc(), + resolution.is_rejected(), + ) + .await?; + } + + Ok(()) + } + + async fn periodic_operations(&mut self) -> Result<(), EcashTicketError> { + trace!("attempting to resolve all pending operations -> tickets that are waiting for verification and possibly redemption"); + + // 1. retry all operations that have failed in the past: verification requests and pending redemption + self.resolve_pending().await?; + + // 2. if applicable, attempt to redeem all newly verified tickets + self.maybe_redeem_tickets().await?; + + Ok(()) + } + + async fn run(mut self, mut shutdown: nym_task::TaskClient) { + info!("Starting Ecash CredentialSender"); + + // attempt to clear any pending operations + info!("attempting to resolve any pending operations"); + if let Err(err) = self.periodic_operations().await { + error!("failed to resolve pending operations on startup: {err}") + } + + let start = Instant::now() + self.config.pending_poller; + let mut resolver_interval = interval_at(start, self.config.pending_poller); + + while !shutdown.is_shutdown() { + tokio::select! { + biased; + _ = shutdown.recv() => { + trace!("client_handling::credentialSender : received shutdown"); + }, + Some(ticket) = self.ticket_receiver.next() => { + let (queued_up, _) = self.ticket_receiver.size_hint(); + + // this will help us determine if we need to parallelize it + match queued_up { + n if n < 5 => debug!("there are {n} tickets queued up that need processing"), + n if (5..20).contains(&n) => info!("there are {n} tickets queued up that need processing"), + n if (20..50).contains(&n) => warn!("there are {n} tickets queued up that need processing!"), + n => error!("there are {n} tickets queued up that need processing!"), + } + + self.handle_client_ticket(ticket).await + }, + _ = resolver_interval.tick() => { + if let Err(err) = self.periodic_operations().await { + error!("failed to deal with periodic operations: {err}") + } + } + } + } + } + + pub(crate) fn start(self, shutdown: nym_task::TaskClient) { + tokio::spawn(async move { self.run(shutdown).await }); + } +} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/ecash/double_spending.rs b/gateway/src/node/client_handling/websocket/connection_handler/ecash/double_spending.rs new file mode 100644 index 0000000000..a8cebdaf7b --- /dev/null +++ b/gateway/src/node/client_handling/websocket/connection_handler/ecash/double_spending.rs @@ -0,0 +1,100 @@ +// Copyright 2022-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::client_handling::websocket::connection_handler::ecash::error::EcashTicketError; +use crate::node::client_handling::websocket::connection_handler::ecash::state::SharedState; +use crate::node::Storage; +use nym_ecash_double_spending::DoubleSpendingFilter; +use nym_task::TaskClient; +use nym_validator_client::client::NymApiClientExt; +use nym_validator_client::EcashApiClient; +use rand::prelude::SliceRandom; +use rand::thread_rng; +use std::sync::Arc; +use tokio::sync::{RwLock, RwLockReadGuard}; +use tokio::time::{interval, Duration}; +use tracing::{info, trace, warn}; + +#[derive(Clone)] +pub(crate) struct DoubleSpendingDetector { + spent_serial_numbers: Arc>, + shared_state: SharedState, +} + +impl DoubleSpendingDetector +where + S: Storage + Clone + Send + Sync + 'static, +{ + pub(crate) fn new(shared_state: SharedState) -> Self { + DoubleSpendingDetector { + spent_serial_numbers: Arc::new(RwLock::new(DoubleSpendingFilter::new_empty_ecash())), + shared_state, + } + } + + pub(crate) async fn check(&self, serial_number: &Vec) -> bool { + self.spent_serial_numbers.read().await.check(serial_number) + } + + async fn latest_api_endpoints( + &self, + ) -> Result>, EcashTicketError> { + let epoch_id = self.shared_state.current_epoch_id().await?; + self.shared_state.api_clients(epoch_id).await + } + + async fn refresh_bloomfilter(&self) { + let mut filter_builder = self.spent_serial_numbers.read().await.rebuild(); + + let api_clients = match self.latest_api_endpoints().await { + Ok(clients) => clients, + Err(err) => { + warn!("failed to obtain current api clients: {err}"); + return; + } + }; + + let mut clients = api_clients + .iter() + .map(|c| c.api_client.clone()) + .collect::>(); + clients.shuffle(&mut thread_rng()); + + for client in clients { + match client.nym_api.double_spending_filter_v1().await { + Ok(response) => { + // due to relative big size of the filter, query only one api since all of them should contain + // roughly the same data anyway. + filter_builder.add_bytes(&response.bitmap); + *self.spent_serial_numbers.write().await = filter_builder.build(); + return; + } + Err(err) => { + warn!("Validator @ {} could not be reached. There might be a problem with the ecash endpoint: {err}", client.api_url()); + } + } + } + + warn!("none of the validators could be reached. the bloomfilter will remain unchanged."); + } + + async fn run(&self, mut shutdown: TaskClient) { + info!("Starting Ecash DoubleSpendingDetector"); + let mut interval = interval(Duration::from_secs(600)); + + while !shutdown.is_shutdown() { + tokio::select! { + biased; + _ = shutdown.recv() => { + trace!("ecash_verifier::DoubleSpendingDetector : received shutdown"); + }, + _ = interval.tick() => self.refresh_bloomfilter().await, + + } + } + } + + pub(crate) fn start(self, shutdown: nym_task::TaskClient) { + tokio::spawn(async move { self.run(shutdown).await }); + } +} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/ecash/error.rs b/gateway/src/node/client_handling/websocket/connection_handler/ecash/error.rs new file mode 100644 index 0000000000..c39ad89720 --- /dev/null +++ b/gateway/src/node/client_handling/websocket/connection_handler/ecash/error.rs @@ -0,0 +1,83 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_gateway_storage::error::StorageError; +use nym_validator_client::coconut::EcashApiError; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nyxd::error::NyxdError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum EcashTicketError { + // TODO: this should be more granual + #[error(transparent)] + ApiFailure(#[from] EcashApiError), + + #[error(transparent)] + CredentialError(#[from] nym_credentials::error::Error), + + #[error("the provided ticket failed to get verified")] + MalformedTicket, + + #[error("failed to verify provided ticket due to invalid expiration date signatures")] + MalformedTicketInvalidDateSignatures, + + #[error("provided payinfo's public key does not match provider's")] + InvalidPayInfoPublicKey, + + #[error("provided payinfo's timestamp is invalid")] + InvalidPayInfoTimestamp, + + #[error("received payinfo is a duplicate")] + DuplicatePayInfo, + + #[error("could not handle the ecash ticket due to internal storage failure: {source}")] + InternalStorageFailure { + #[from] + source: StorageError, + }, + + #[error("failed to create ticket redemption proposal: {source}")] + RedemptionProposalCreationFailure { + #[source] + source: NyxdError, + }, + + #[error("failed to execute ticket redemption proposal {proposal_id}: {source}")] + RedemptionProposalExecutionFailure { + proposal_id: u64, + + #[source] + source: NyxdError, + }, + + #[error("failed to parse out the redemption proposal id: {source}")] + ProposalIdParsingFailure { + #[source] + source: NyxdError, + }, + + #[error("failed to query the nyx chain: {source}")] + ChainQueryFailure { + #[source] + source: NyxdError, + }, + + #[error("Not enough nym API endpoints provided. Needed {needed}, received {received}")] + NotEnoughNymAPIs { received: usize, needed: usize }, + + #[error("the DKG contract is unavailable")] + UnavailableDkgContract, + + #[error("the DKG threshold value for epoch {epoch_id} is currently unavailable. we're probably mid-epoch transition")] + DKGThresholdUnavailable { epoch_id: EpochId }, + + #[error("could not create redemption proposal as we have tickets pending full verification")] + PendingTickets, +} + +impl EcashTicketError { + pub fn chain_query_failure(source: NyxdError) -> EcashTicketError { + EcashTicketError::ChainQueryFailure { source } + } +} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/ecash/helpers.rs b/gateway/src/node/client_handling/websocket/connection_handler/ecash/helpers.rs new file mode 100644 index 0000000000..9d767fba6c --- /dev/null +++ b/gateway/src/node/client_handling/websocket/connection_handler/ecash/helpers.rs @@ -0,0 +1,36 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use futures::{Stream, StreamExt}; +use nym_validator_client::EcashApiClient; +use std::future::Future; +use std::ops::Deref; +use tokio::sync::RwLockReadGuard; + +pub(crate) fn apis_stream<'a>( + // if needed we could make this argument more generic to accept either locks or iterators, etc. + all_clients: &'a RwLockReadGuard<'a, Vec>, + filter_by_id: &'a [u64], +) -> impl Stream + 'a { + // this vector will never contain more than ~30 entries so linear lookup is fine. + // it's probably even faster than hashset due to overhead + futures::stream::iter( + all_clients + .deref() + .iter() + .filter(|client| filter_by_id.contains(&client.node_id)), + ) +} + +pub(crate) async fn for_each_api_concurrent<'a, F, Fut>( + all_clients: &'a RwLockReadGuard<'a, Vec>, + filter_by_id: &'a [u64], + f: F, +) where + F: FnMut(&'a EcashApiClient) -> Fut, + Fut: Future, +{ + apis_stream(all_clients, filter_by_id) + .for_each_concurrent(32, f) + .await +} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/ecash/mod.rs b/gateway/src/node/client_handling/websocket/connection_handler/ecash/mod.rs new file mode 100644 index 0000000000..69c3d46800 --- /dev/null +++ b/gateway/src/node/client_handling/websocket/connection_handler/ecash/mod.rs @@ -0,0 +1,174 @@ +// Copyright 2022-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::client_handling::websocket::connection_handler::ecash::state::SharedState; +use crate::GatewayError; +use credential_sender::CredentialHandler; +use double_spending::DoubleSpendingDetector; +use futures::channel::mpsc::{self, UnboundedSender}; +use nym_credentials::CredentialSpendingData; +use nym_credentials_interface::{ClientTicket, CompactEcashError, NymPayInfo, VerificationKeyAuth}; +use nym_gateway_storage::Storage; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use time::OffsetDateTime; +use tokio::sync::{Mutex, RwLockReadGuard}; +use tracing::error; + +use crate::node::client_handling::websocket::connection_handler::ecash::credential_sender::CredentialHandlerConfig; +use crate::node::client_handling::websocket::connection_handler::ecash::error::EcashTicketError; + +pub(crate) mod credential_sender; +pub(crate) mod double_spending; +pub(crate) mod error; +mod helpers; +mod state; + +const TIME_RANGE_SEC: i64 = 30; + +pub struct EcashManager { + shared_state: SharedState, + + pk_bytes: [u8; 32], // bytes representation of a pub key representing the verifier + pay_infos: Mutex>, + cred_sender: UnboundedSender, + double_spend_detector: DoubleSpendingDetector, +} + +impl EcashManager +where + S: Storage + Clone + 'static, +{ + pub async fn new( + credential_handler_cfg: CredentialHandlerConfig, + nyxd_client: DirectSigningHttpRpcNyxdClient, + pk_bytes: [u8; 32], + shutdown: nym_task::TaskClient, + storage: S, + ) -> Result { + let shared_state = SharedState::new(nyxd_client, storage).await?; + + let double_spend_detector = DoubleSpendingDetector::new(shared_state.clone()); + double_spend_detector.clone().start(shutdown.clone()); + + let (cred_sender, cred_receiver) = mpsc::unbounded(); + + let cs = + CredentialHandler::new(credential_handler_cfg, cred_receiver, shared_state.clone()) + .await?; + cs.start(shutdown); + + Ok(EcashManager { + shared_state, + pk_bytes, + pay_infos: Default::default(), + cred_sender, + double_spend_detector, + }) + } + + pub async fn verification_key( + &self, + epoch_id: EpochId, + ) -> Result, EcashTicketError> { + self.shared_state.verification_key(epoch_id).await + } + + //Check for duplicate pay_info, then check the payment, then insert pay_info if everything succeeded + pub async fn check_payment( + &self, + credential: &CredentialSpendingData, + aggregated_verification_key: &VerificationKeyAuth, + ) -> Result<(), EcashTicketError> { + let insert_index = self.verify_pay_info(credential.pay_info.into()).await?; + + credential + .verify(aggregated_verification_key) + .map_err(|err| match err { + CompactEcashError::ExpirationDateSignatureValidity => { + EcashTicketError::MalformedTicketInvalidDateSignatures + } + _ => EcashTicketError::MalformedTicket, + })?; + + self.insert_pay_info(credential.pay_info.into(), insert_index) + .await + } + + pub async fn verify_pay_info(&self, pay_info: NymPayInfo) -> Result { + //Public key check + if pay_info.pk() != self.pk_bytes { + return Err(EcashTicketError::InvalidPayInfoPublicKey); + } + + //Timestamp range check + let timestamp = OffsetDateTime::now_utc().unix_timestamp(); + let tmin = timestamp - TIME_RANGE_SEC; + let tmax = timestamp + TIME_RANGE_SEC; + if pay_info.timestamp() > tmax || pay_info.timestamp() < tmin { + return Err(EcashTicketError::InvalidPayInfoTimestamp); + } + + let mut inner = self.pay_infos.lock().await; + + //Cleanup inner + let low = inner.partition_point(|x| x.timestamp() < tmin); + let high = inner.partition_point(|x| x.timestamp() < tmax); + inner.truncate(high); + drop(inner.drain(..low)); + + //Duplicate check + match inner.binary_search_by(|info| info.timestamp().cmp(&pay_info.timestamp())) { + Result::Err(index) => Ok(index), + Result::Ok(index) => { + if inner[index] == pay_info { + return Err(EcashTicketError::DuplicatePayInfo); + } + //tbh, I don't expect ending up here if all parties are honest + //binary search returns an arbitrary match, so we have to check for potential multiple matches + let mut i = index as i64; + while i >= 0 && inner[i as usize].timestamp() == pay_info.timestamp() { + if inner[i as usize] == pay_info { + return Err(EcashTicketError::DuplicatePayInfo); + } + i -= 1; + } + + let mut i = index + 1; + while i < inner.len() && inner[i].timestamp() == pay_info.timestamp() { + if inner[i] == pay_info { + return Err(EcashTicketError::DuplicatePayInfo); + } + i += 1; + } + Ok(index) + } + } + } + + async fn insert_pay_info( + &self, + pay_info: NymPayInfo, + index: usize, + ) -> Result<(), EcashTicketError> { + let mut inner = self.pay_infos.lock().await; + if index > inner.len() { + inner.push(pay_info); + return Ok(()); + } + inner.insert(index, pay_info); + Ok(()) + } + + pub async fn check_double_spend(&self, serial_number: &Vec) -> bool { + self.double_spend_detector.check(serial_number).await + } + + pub fn async_verify(&self, ticket: ClientTicket) { + // TODO: I guess do something for shutdowns + let _ = self + .cred_sender + .unbounded_send(ticket) + .inspect_err(|_| error!("failed to send the client ticket for verification task")); + } +} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/ecash/state.rs b/gateway/src/node/client_handling/websocket/connection_handler/ecash/state.rs new file mode 100644 index 0000000000..57f39e95fd --- /dev/null +++ b/gateway/src/node/client_handling/websocket/connection_handler/ecash/state.rs @@ -0,0 +1,257 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::node::client_handling::websocket::connection_handler::ecash::error::EcashTicketError; +use crate::node::Storage; +use crate::GatewayError; +use cosmwasm_std::{from_binary, CosmosMsg, WasmMsg}; +use nym_credentials_interface::VerificationKeyAuth; +use nym_ecash_contract_common::msg::ExecuteMsg; +use nym_validator_client::coconut::all_ecash_api_clients; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nyxd::contract_traits::{ + DkgQueryClient, MultisigQueryClient, NymContractsProvider, +}; +use nym_validator_client::nyxd::cw3::ProposalResponse; +use nym_validator_client::nyxd::AccountId; +use nym_validator_client::{DirectSigningHttpRpcNyxdClient, EcashApiClient}; +use std::collections::BTreeMap; +use std::ops::Deref; +use std::sync::Arc; +use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +use tracing::{error, trace, warn}; + +// state shared by different subtasks dealing with credentials +#[derive(Clone)] +pub(crate) struct SharedState { + pub(crate) nyxd_client: Arc>, + pub(crate) address: AccountId, + pub(crate) epoch_data: Arc>>, + pub(crate) storage: S, +} + +impl SharedState +where + S: Storage + Clone, +{ + pub(crate) async fn new( + nyxd_client: DirectSigningHttpRpcNyxdClient, + storage: S, + ) -> Result { + let address = nyxd_client.address(); + + if nyxd_client.dkg_contract_address().is_none() { + error!("the DKG contract address is not available"); + return Err(EcashTicketError::UnavailableDkgContract.into()); + } + + let Ok(current_epoch) = nyxd_client.get_current_epoch().await else { + error!("the specified DKG contract address is invalid - no coconut credentials will be redeemable"); + // if we require coconut credentials, we MUST have DKG contract available + return Err(EcashTicketError::UnavailableDkgContract.into()); + }; + + let this = SharedState { + nyxd_client: Arc::new(RwLock::new(nyxd_client)), + address, + epoch_data: Arc::new(RwLock::new(BTreeMap::new())), + storage, + }; + + // might as well obtain the data for the current epoch, if applicable + if current_epoch.state.is_in_progress() { + if let Err(err) = this.set_epoch_data(current_epoch.epoch_id).await { + warn!("failed to set initial epoch data: {err}") + } + } + + Ok(this) + } + + fn created_redemption_proposal(&self, proposal: &ProposalResponse) -> bool { + let Some(msg) = proposal.msgs.first() else { + return false; + }; + let CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) = msg else { + return false; + }; + let Ok(ExecuteMsg::RedeemTickets { gw, .. }) = from_binary(msg) else { + return false; + }; + + gw == self.address.as_ref() + } + + /// retrieve all redemption proposals made by this gateway since, but excluding, the provided id + pub(crate) async fn proposals_since( + &self, + proposal_id: u64, + ) -> Result, EcashTicketError> { + Ok(self + .start_query() + .await + .list_proposals(Some(proposal_id), None) + .await + .map_err(EcashTicketError::chain_query_failure)? + .proposals + .into_iter() + .filter(|p| self.created_redemption_proposal(p)) + .collect()) + } + + /// retrieve all redemption proposals made by this gateway that are available on the last page of the query + pub(crate) async fn last_proposal_page( + &self, + ) -> Result, EcashTicketError> { + Ok(self + .start_query() + .await + .reverse_proposals(None, None) + .await + .map_err(EcashTicketError::chain_query_failure)? + .proposals + .into_iter() + .filter(|p| self.created_redemption_proposal(p)) + .collect()) + } + + async fn set_epoch_data( + &self, + epoch_id: EpochId, + ) -> Result>, EcashTicketError> { + let Some(threshold) = self.threshold(epoch_id).await? else { + return Err(EcashTicketError::DKGThresholdUnavailable { epoch_id }); + }; + + // TODO: optimise: query nym-apis for aggregate key instead + // (when the below code was originally written, that query didn't exist) + let api_clients = self.query_api_clients(epoch_id).await?; + + if api_clients.len() < threshold as usize { + return Err(EcashTicketError::NotEnoughNymAPIs { + received: api_clients.len(), + needed: threshold as usize, + }); + } + + let aggregated_verification_key = + nym_credentials::aggregate_verification_keys(&api_clients)?; + + let mut guard = self.epoch_data.write().await; + + self.storage + .insert_epoch_signers( + epoch_id as i64, + api_clients.iter().map(|c| c.node_id as i64).collect(), + ) + .await?; + + guard.insert( + epoch_id, + EpochState { + api_clients, + master_key: aggregated_verification_key, + threshold, + }, + ); + Ok(guard) + } + + async fn query_api_clients( + &self, + epoch_id: EpochId, + ) -> Result, EcashTicketError> { + Ok(all_ecash_api_clients(self.nyxd_client.read().await.deref(), epoch_id).await?) + } + + pub(crate) async fn threshold( + &self, + epoch_id: EpochId, + ) -> Result, EcashTicketError> { + self.nyxd_client + .read() + .await + .get_epoch_threshold(epoch_id) + .await + .map_err(EcashTicketError::chain_query_failure) + } + + pub(crate) async fn api_clients( + &self, + epoch_id: EpochId, + ) -> Result>, EcashTicketError> { + let guard = self.epoch_data.read().await; + + // the key was already in the map + if let Ok(mapped) = + RwLockReadGuard::try_map(guard, |data| data.get(&epoch_id).map(|d| &d.api_clients)) + { + trace!("we already had cached api clients for epoch {epoch_id}"); + return Ok(mapped); + } + + let write_guard = self.set_epoch_data(epoch_id).await?; + let guard = write_guard.downgrade(); + + // SAFETY: + // we just inserted the entry into the map while NEVER dropping the lock (only downgraded it) + // so it MUST exist and thus the unwrap is fine + #[allow(clippy::unwrap_used)] + Ok(RwLockReadGuard::map(guard, |data| { + data.get(&epoch_id).map(|d| &d.api_clients).unwrap() + })) + } + + pub(crate) async fn verification_key( + &self, + epoch_id: EpochId, + ) -> Result, EcashTicketError> { + let guard = self.epoch_data.read().await; + + // the key was already in the map + if let Ok(mapped) = + RwLockReadGuard::try_map(guard, |data| data.get(&epoch_id).map(|d| &d.master_key)) + { + trace!("we already had cached api clients for epoch {epoch_id}"); + return Ok(mapped); + } + + let write_guard = self.set_epoch_data(epoch_id).await?; + let guard = write_guard.downgrade(); + + // SAFETY: + // we just inserted the entry into the map while NEVER dropping the lock (only downgraded it) + // so it MUST exist and thus the unwrap is fine + #[allow(clippy::unwrap_used)] + Ok(RwLockReadGuard::map(guard, |data| { + data.get(&epoch_id).map(|d| &d.master_key).unwrap() + })) + } + + pub(crate) async fn start_tx(&self) -> RwLockWriteGuard { + self.nyxd_client.write().await + } + + pub(crate) async fn start_query(&self) -> RwLockReadGuard { + self.nyxd_client.read().await + } + + pub(crate) async fn current_epoch_id(&self) -> Result { + Ok(self + .start_query() + .await + .get_current_epoch() + .await + .map_err(EcashTicketError::chain_query_failure)? + .epoch_id) + } +} + +pub(crate) struct EpochState { + // note: **CURRENTLY** api client addresses don't change during the epochs + pub(crate) api_clients: Vec, + pub(crate) master_key: VerificationKeyAuth, + + #[allow(unused)] + pub(crate) threshold: u64, +} 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 4f00ebcf90..cccaf71dff 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -6,7 +6,7 @@ use futures::{ channel::{mpsc, oneshot}, SinkExt, StreamExt, }; -use log::*; +use nym_credentials_interface::AvailableBandwidth; use nym_crypto::asymmetric::identity; use nym_gateway_requests::authentication::encrypted_address::{ EncryptedAddressBytes, EncryptedAddressConversionError, @@ -20,26 +20,26 @@ use nym_gateway_requests::{ }; use nym_mixnet_client::forwarder::MixForwardingSender; use nym_sphinx::DestinationAddressBytes; +use nym_task::TaskClient; use rand::{CryptoRng, Rng}; +use std::net::SocketAddr; use std::time::Duration; use thiserror::Error; use tokio::io::{AsyncRead, AsyncWrite}; use tokio_tungstenite::tungstenite::{protocol::Message, Error as WsError}; +use tracing::*; use crate::node::client_handling::websocket::common_state::CommonHandlerState; -use crate::node::client_handling::websocket::connection_handler::AvailableBandwidth; -use crate::node::{ - client_handling::{ - active_clients::ActiveClientsStore, - websocket::{ - connection_handler::{ - AuthenticatedHandler, ClientDetails, InitialAuthResult, SocketStream, - }, - message_receiver::{IsActive, IsActiveRequestSender}, +use crate::node::client_handling::{ + active_clients::ActiveClientsStore, + websocket::{ + connection_handler::{ + AuthenticatedHandler, ClientDetails, InitialAuthResult, SocketStream, }, + message_receiver::{IsActive, IsActiveRequestSender}, }, - storage::{error::StorageError, Storage}, }; +use nym_gateway_storage::{error::StorageError, Storage}; #[derive(Debug, Error)] pub(crate) enum InitialAuthenticationError { @@ -112,6 +112,9 @@ pub(crate) enum InitialAuthenticationError { #[error("failed to upgrade the client handler: {source}")] HandlerUpgradeFailure { source: RequestHandlingError }, + + #[error("received shutdown")] + ReceivedShutdown, } impl InitialAuthenticationError { @@ -123,11 +126,12 @@ impl InitialAuthenticationError { pub(crate) struct FreshHandler { rng: R, - pub(crate) shared_state: CommonHandlerState, + pub(crate) shared_state: CommonHandlerState, pub(crate) active_clients_store: ActiveClientsStore, pub(crate) outbound_mix_sender: MixForwardingSender, pub(crate) socket_connection: SocketStream, - pub(crate) storage: St, + pub(crate) peer_address: SocketAddr, + pub(crate) shutdown: TaskClient, // currently unused (but populated) pub(crate) negotiated_protocol: Option, @@ -136,7 +140,7 @@ pub(crate) struct FreshHandler { impl FreshHandler where R: Rng + CryptoRng, - St: Storage, + St: Storage + Clone + 'static, { // for time being we assume handle is always constructed from raw socket. // if we decide we want to change it, that's not too difficult @@ -147,18 +151,20 @@ where rng: R, conn: S, outbound_mix_sender: MixForwardingSender, - storage: St, active_clients_store: ActiveClientsStore, - shared_state: CommonHandlerState, + shared_state: CommonHandlerState, + peer_address: SocketAddr, + shutdown: TaskClient, ) -> Self { FreshHandler { rng, active_clients_store, outbound_mix_sender, socket_connection: SocketStream::RawTcp(conn), - storage, + peer_address, negotiated_protocol: None, shared_state, + shutdown, } } @@ -202,6 +208,7 @@ where ws_stream, self.shared_state.local_identity.as_ref(), init_msg, + self.shutdown.clone(), ) .await } @@ -311,6 +318,7 @@ where loop { // retrieve some messages let (messages, new_start_next_after) = self + .shared_state .storage .retrieve_messages(client_address, start_next_after) .await?; @@ -326,7 +334,7 @@ where return Err(InitialAuthenticationError::ConnectionError(err)); } else { // if it was successful - remove them from the store - self.storage.remove_messages(ids).await?; + self.shared_state.storage.remove_messages(ids).await?; } // no more messages to grab @@ -356,7 +364,11 @@ where encrypted_address: EncryptedAddressBytes, iv: IV, ) -> Result, InitialAuthenticationError> { - let shared_keys = self.storage.get_shared_keys(client_address).await?; + let shared_keys = self + .shared_state + .storage + .get_shared_keys(client_address) + .await?; if let Some(shared_keys) = shared_keys { // this should never fail as we only ever construct persisted shared keys ourselves when inserting @@ -517,6 +529,11 @@ where /// * `client_address`: address of the client wishing to authenticate. /// * `encrypted_address`: ciphertext of the address of the client wishing to authenticate. /// * `iv`: fresh IV received with the request. + #[instrument(skip_all + fields( + address = %address, + ) + )] async fn handle_authenticate( &mut self, client_protocol_version: Option, @@ -527,6 +544,8 @@ where where S: AsyncRead + AsyncWrite + Unpin, { + debug!("handling client registration"); + let negotiated_protocol = self.negotiate_client_protocol(client_protocol_version)?; // populate the negotiated protocol for future uses self.negotiated_protocol = Some(negotiated_protocol); @@ -543,39 +562,43 @@ where .await?; } - let shared_keys = self + let Some(shared_keys) = self .authenticate_client(address, encrypted_address, iv) - .await?; - let status = shared_keys.is_some(); + .await? + else { + // it feels weird to be returning an 'Ok' here, but I didn't want to change the existing behaviour + return Ok(InitialAuthResult::new_failed(Some(negotiated_protocol))); + }; - let available_bandwidth: AvailableBandwidth = - self.storage.get_available_bandwidth(address).await?.into(); + let client_id = self.shared_state.storage.get_client_id(address).await?; - let bandwidth_remaining = if available_bandwidth.freepass_expired() { - self.expire_freepass(address).await?; + let available_bandwidth: AvailableBandwidth = self + .shared_state + .storage + .get_available_bandwidth(client_id) + .await? + .map(From::from) + .unwrap_or_default(); + + let bandwidth_remaining = if available_bandwidth.expired() { + self.expire_bandwidth(client_id).await?; 0 } else { available_bandwidth.bytes }; - let client_details = - shared_keys.map(|shared_keys| ClientDetails::new(address, shared_keys)); - Ok(InitialAuthResult::new( - client_details, + Some(ClientDetails::new(client_id, address, shared_keys)), ServerResponse::Authenticate { protocol_version: Some(negotiated_protocol), - status, + status: true, bandwidth_remaining, }, )) } - pub(crate) async fn expire_freepass( - &self, - client: DestinationAddressBytes, - ) -> Result<(), StorageError> { - self.storage.reset_freepass_bandwidth(client).await + pub(crate) async fn expire_bandwidth(&self, client_id: i64) -> Result<(), StorageError> { + self.shared_state.storage.reset_bandwidth(client_id).await } /// Attempts to finalize registration of the client by storing the derived shared keys in the @@ -588,34 +611,41 @@ where /// * `client`: details (i.e. address and shared keys) of the registered client async fn register_client( &mut self, - client: &ClientDetails, - ) -> Result + client_address: DestinationAddressBytes, + client_shared_keys: &SharedKeys, + ) -> Result where S: AsyncRead + AsyncWrite + Unpin, { debug!( "Processing register client request for: {}", - client.address.as_base58_string() + client_address.as_base58_string() ); - self.storage - .insert_shared_keys(client.address, &client.shared_keys) + let client_id = self + .shared_state + .storage + .insert_shared_keys(client_address, client_shared_keys) .await?; // see if we have bandwidth entry for the client already, if not, create one with zero value if self + .shared_state .storage - .get_available_bandwidth(client.address) + .get_available_bandwidth(client_id) .await? .is_none() { - self.storage.create_bandwidth_entry(client.address).await?; + self.shared_state + .storage + .create_bandwidth_entry(client_id) + .await?; } - self.push_stored_messages_to_client(client.address, &client.shared_keys) + self.push_stored_messages_to_client(client_address, client_shared_keys) .await?; - Ok(true) + Ok(client_id) } /// Tries to handle the received register request by checking attempting to complete registration @@ -639,20 +669,24 @@ where let remote_identity = Self::extract_remote_identity_from_register_init(&init_data)?; let remote_address = remote_identity.derive_destination_address(); + debug!(remote_client = %remote_identity); + if self.active_clients_store.is_active(remote_address) { return Err(InitialAuthenticationError::DuplicateConnection); } let shared_keys = self.perform_registration_handshake(init_data).await?; - let client_details = ClientDetails::new(remote_address, shared_keys); + let client_id = self.register_client(remote_address, &shared_keys).await?; - let status = self.register_client(&client_details).await?; + debug!(client_id = %client_id, "managed to finalize client registration"); + + let client_details = ClientDetails::new(client_id, remote_address, shared_keys); Ok(InitialAuthResult::new( Some(client_details), ServerResponse::Register { protocol_version: Some(negotiated_protocol), - status, + status: true, }, )) } @@ -705,111 +739,125 @@ where { trace!("Started waiting for authenticate/register request..."); - while let Some(msg) = self.read_websocket_message().await { - let msg = match msg { - Ok(msg) => msg, - Err(source) => { - debug!("failed to obtain message from websocket stream! stopping connection handler: {source}"); - return Err(InitialAuthenticationError::FailedToReadMessage { source }); - } - }; + let mut shutdown = self.shutdown.clone(); + loop { + tokio::select! { + biased; + _ = shutdown.recv() => { + trace!("received shutdown signal while performing initial authentication"); + return Err(InitialAuthenticationError::ReceivedShutdown); + }, + msg = self.read_websocket_message() => { + let Some(msg) = msg else { + break; + }; - if msg.is_close() { - return Err(InitialAuthenticationError::CloseMessage); - } + let msg = match msg { + Ok(msg) => msg, + Err(source) => { + debug!("failed to obtain message from websocket stream! stopping connection handler: {source}"); + return Err(InitialAuthenticationError::FailedToReadMessage { source }); + } + }; - // ONLY handle 'Authenticate' or 'Register' requests, ignore everything else - match msg { - // we have explicitly checked for close message - Message::Close(_) => unreachable!(), - Message::Text(text_msg) => { - let (mix_sender, mix_receiver) = mpsc::unbounded(); - return match self.handle_initial_authentication_request(text_msg).await { - Err(err) => { - debug!("authentication failure: {err}"); + if msg.is_close() { + return Err(InitialAuthenticationError::CloseMessage); + } - // try to send error to the client - if let Err(source) = - self.send_websocket_message(err.to_error_message()).await + // ONLY handle 'Authenticate' or 'Register' requests, ignore everything else + match msg { + // we have explicitly checked for close message + Message::Close(_) => unreachable!(), + Message::Text(text_msg) => { + let (mix_sender, mix_receiver) = mpsc::unbounded(); + return match self.handle_initial_authentication_request(text_msg).await { + Err(err) => { + debug!("authentication failure: {err}"); + + // try to send error to the client + if let Err(source) = + self.send_websocket_message(err.to_error_message()).await + { + debug!("Failed to send authentication error response: {source}"); + return Err(InitialAuthenticationError::ErrorResponseSendFailure { + source, + }); + } + // return the underlying error + Err(err) + } + Ok(auth_result) => { + // try to send auth response back to the client + if let Err(source) = self + .send_websocket_message(auth_result.server_response.into()) + .await + { + debug!("Failed to send authentication response: {source}"); + return Err(InitialAuthenticationError::ResponseSendFailure { + source, + }); + } + + if let Some(client_details) = auth_result.client_details { + // Channel for handlers to ask other handlers if they are still active. + let (is_active_request_sender, is_active_request_receiver) = + mpsc::unbounded(); + self.active_clients_store.insert_remote( + client_details.address, + mix_sender, + is_active_request_sender, + ); + AuthenticatedHandler::upgrade( + self, + client_details, + mix_receiver, + is_active_request_receiver, + ) + .await + .map_err(|source| { + InitialAuthenticationError::HandlerUpgradeFailure { source } + }) + } else { + // honestly, it's been so long I don't remember under what conditions its possible (if at all) + // to have empty client details + Err(InitialAuthenticationError::EmptyClientDetails) + } + } + }; + } + Message::Binary(_) => { + // perhaps logging level should be reduced here, let's leave it for now and see what happens + // if client is working correctly, this should have never happened + debug!("possibly received a sphinx packet without prior authentication. Request is going to be ignored"); + if let Err(source) = self + .send_websocket_message( + ServerResponse::new_error( + "binary request without prior authentication", + ) + .into(), + ) + .await { - debug!("Failed to send authentication error response: {source}"); return Err(InitialAuthenticationError::ErrorResponseSendFailure { source, }); } - // return the underlying error - Err(err) + return Err(InitialAuthenticationError::BinaryRequestWithoutAuthentication); } - Ok(auth_result) => { - // try to send auth response back to the client - if let Err(source) = self - .send_websocket_message(auth_result.server_response.into()) - .await - { - debug!("Failed to send authentication response: {source}"); - return Err(InitialAuthenticationError::ResponseSendFailure { - source, - }); - } - if let Some(client_details) = auth_result.client_details { - // Channel for handlers to ask other handlers if they are still active. - let (is_active_request_sender, is_active_request_receiver) = - mpsc::unbounded(); - self.active_clients_store.insert_remote( - client_details.address, - mix_sender, - is_active_request_sender, - ); - AuthenticatedHandler::upgrade( - self, - client_details, - mix_receiver, - is_active_request_receiver, - ) - .await - .map_err(|source| { - InitialAuthenticationError::HandlerUpgradeFailure { source } - }) - } else { - // honestly, it's been so long I don't remember under what conditions its possible (if at all) - // to have empty client details - Err(InitialAuthenticationError::EmptyClientDetails) - } - } + _ => continue, }; } - Message::Binary(_) => { - // perhaps logging level should be reduced here, let's leave it for now and see what happens - // if client is working correctly, this should have never happened - debug!("possibly received a sphinx packet without prior authentication. Request is going to be ignored"); - if let Err(source) = self - .send_websocket_message( - ServerResponse::new_error( - "binary request without prior authentication", - ) - .into(), - ) - .await - { - return Err(InitialAuthenticationError::ErrorResponseSendFailure { - source, - }); - } - return Err(InitialAuthenticationError::BinaryRequestWithoutAuthentication); - } - - _ => continue, - }; + } } Err(InitialAuthenticationError::ClosedConnection) } - pub(crate) async fn start_handling(self, shutdown: nym_task::TaskClient) + pub(crate) async fn start_handling(self) where S: AsyncRead + AsyncWrite + Unpin + Send, { - super::handle_connection(self, shutdown).await + super::handle_connection(self).await } } diff --git a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs index ee880d4832..5eca9ad070 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs @@ -2,26 +2,29 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::config::Config; -use crate::node::storage::Storage; -use log::{trace, warn}; +use fresh::InitialAuthenticationError; +use nym_credentials_interface::AvailableBandwidth; use nym_gateway_requests::registration::handshake::SharedKeys; use nym_gateway_requests::ServerResponse; +use nym_gateway_storage::Storage; use nym_sphinx::DestinationAddressBytes; -use nym_task::TaskClient; use rand::{CryptoRng, Rng}; use std::time::Duration; use time::OffsetDateTime; use tokio::io::{AsyncRead, AsyncWrite}; use tokio_tungstenite::WebSocketStream; +use tracing::{instrument, trace, warn}; use zeroize::{Zeroize, ZeroizeOnDrop}; pub(crate) use self::authenticated::AuthenticatedHandler; pub(crate) use self::fresh::FreshHandler; pub(crate) mod authenticated; -pub(crate) mod coconut; +pub(crate) mod ecash; mod fresh; +const WEBSOCKET_HANDSHAKE_TIMEOUT: Duration = Duration::from_millis(1_500); + // TODO: note for my future self to consider the following idea: // split the socket connection into sink and stream // stream will be for reading explicit requests @@ -44,13 +47,15 @@ impl SocketStream { pub(crate) struct ClientDetails { #[zeroize(skip)] pub(crate) address: DestinationAddressBytes, + pub(crate) id: i64, pub(crate) shared_keys: SharedKeys, } impl ClientDetails { - pub(crate) fn new(address: DestinationAddressBytes, shared_keys: SharedKeys) -> Self { + pub(crate) fn new(id: i64, address: DestinationAddressBytes, shared_keys: SharedKeys) -> Self { ClientDetails { address, + id, shared_keys, } } @@ -68,49 +73,62 @@ impl InitialAuthResult { server_response, } } + + fn new_failed(protocol_version: Option) -> Self { + InitialAuthResult { + client_details: None, + server_response: ServerResponse::Authenticate { + protocol_version, + status: false, + bandwidth_remaining: 0, + }, + } + } } -pub(crate) async fn handle_connection( - mut handle: FreshHandler, - mut shutdown: TaskClient, -) where +// imo there's no point in including the peer address in anything higher than debug +#[instrument(level = "debug", skip_all, fields(peer = %handle.peer_address))] +pub(crate) async fn handle_connection(mut handle: FreshHandler) +where R: Rng + CryptoRng, S: AsyncRead + AsyncWrite + Unpin + Send, - St: Storage, + St: Storage + Clone + 'static, { // If the connection handler abruptly stops, we shouldn't signal global shutdown - shutdown.mark_as_success(); + handle.shutdown.mark_as_success(); - match shutdown - .run_future(handle.perform_websocket_handshake()) - .await + match tokio::time::timeout( + WEBSOCKET_HANDSHAKE_TIMEOUT, + handle.perform_websocket_handshake(), + ) + .await { - None => { - trace!("received shutdown signal while performing websocket handshake"); + Err(timeout_err) => { + warn!("websocket handshake timedout: {timeout_err}"); return; } - Some(Err(err)) => { + Ok(Err(err)) => { warn!("Failed to complete WebSocket handshake: {err}. Stopping the handler"); return; } - _ => (), + _ => {} } trace!("Managed to perform websocket handshake!"); - match shutdown - .run_future(handle.perform_initial_authentication()) - .await - { - None => { - trace!("received shutdown signal while performing initial authentication"); - return; - } - Some(Err(err)) => { + let shutdown = handle.shutdown.clone(); + match handle.perform_initial_authentication().await { + // For storage error, we want to print the extended storage error, but without + // including it in the error that's returned to the clients + Err(InitialAuthenticationError::StorageError(err)) => { warn!("authentication has failed: {err}"); return; } - Some(Ok(auth_handle)) => auth_handle.listen_for_requests(shutdown).await, + Err(err) => { + warn!("authentication has failed: {err}"); + return; + } + Ok(auth_handle) => auth_handle.listen_for_requests(shutdown).await, } trace!("The handler is done!"); @@ -136,27 +154,15 @@ impl<'a> From<&'a Config> for BandwidthFlushingBehaviourConfig { } } -#[derive(Debug, Clone, Copy, Default)] -pub(crate) struct AvailableBandwidth { - pub(crate) bytes: i64, - pub(crate) freepass_expiration: Option, -} - -impl AvailableBandwidth { - pub(crate) fn freepass_expired(&self) -> bool { - if let Some(expiration) = self.freepass_expiration { - if expiration < OffsetDateTime::now_utc() { - return true; - } - } - false - } -} - pub(crate) struct ClientBandwidth { pub(crate) bandwidth: AvailableBandwidth, pub(crate) last_flushed: OffsetDateTime, - pub(crate) bytes_at_last_flush: i64, + + /// the number of bytes the client had during the last sync. + /// it is used to determine whether the current value should be synced with the storage + /// by checking the delta with the known amount + pub(crate) bytes_at_last_sync: i64, + pub(crate) bytes_delta_since_sync: i64, } impl ClientBandwidth { @@ -164,14 +170,13 @@ impl ClientBandwidth { ClientBandwidth { bandwidth, last_flushed: OffsetDateTime::now_utc(), - bytes_at_last_flush: bandwidth.bytes, + bytes_at_last_sync: bandwidth.bytes, + bytes_delta_since_sync: 0, } } - pub(crate) fn should_flush(&self, cfg: BandwidthFlushingBehaviourConfig) -> bool { - if (self.bytes_at_last_flush - self.bandwidth.bytes).abs() - >= cfg.client_bandwidth_max_delta_flushing_amount - { + pub(crate) fn should_sync(&self, cfg: BandwidthFlushingBehaviourConfig) -> bool { + if self.bytes_delta_since_sync.abs() >= cfg.client_bandwidth_max_delta_flushing_amount { return true; } @@ -182,8 +187,9 @@ impl ClientBandwidth { false } - pub(crate) fn update_flush_data(&mut self) { + pub(crate) fn update_sync_data(&mut self) { self.last_flushed = OffsetDateTime::now_utc(); - self.bytes_at_last_flush = self.bandwidth.bytes; + self.bytes_at_last_sync = self.bandwidth.bytes; + self.bytes_delta_since_sync = 0; } } diff --git a/gateway/src/node/client_handling/websocket/listener.rs b/gateway/src/node/client_handling/websocket/listener.rs index a33f6a0fb5..be3c6c51a2 100644 --- a/gateway/src/node/client_handling/websocket/listener.rs +++ b/gateway/src/node/client_handling/websocket/listener.rs @@ -4,21 +4,24 @@ use crate::node::client_handling::active_clients::ActiveClientsStore; use crate::node::client_handling::websocket::common_state::CommonHandlerState; use crate::node::client_handling::websocket::connection_handler::FreshHandler; -use crate::node::storage::Storage; -use log::*; +use nym_gateway_storage::Storage; use nym_mixnet_client::forwarder::MixForwardingSender; use rand::rngs::OsRng; use std::net::SocketAddr; use std::process; use tokio::task::JoinHandle; +use tracing::*; -pub(crate) struct Listener { +pub(crate) struct Listener { address: SocketAddr, - shared_state: CommonHandlerState, + shared_state: CommonHandlerState, } -impl Listener { - pub(crate) fn new(address: SocketAddr, shared_state: CommonHandlerState) -> Self { +impl Listener +where + S: Storage + Send + Sync + Clone + 'static, +{ + pub(crate) fn new(address: SocketAddr, shared_state: CommonHandlerState) -> Self { Listener { address, shared_state, @@ -27,15 +30,12 @@ impl Listener { // TODO: change the signature to pub(crate) async fn run(&self, handler: Handler) - pub(crate) async fn run( + pub(crate) async fn run( &mut self, outbound_mix_sender: MixForwardingSender, - storage: St, active_clients_store: ActiveClientsStore, mut shutdown: nym_task::TaskClient, - ) where - St: Storage + Clone + 'static, - { + ) { info!("Starting websocket listener at {}", self.address); let tcp_listener = match tokio::net::TcpListener::bind(self.address).await { Ok(listener) => listener, @@ -54,6 +54,7 @@ impl Listener { connection = tcp_listener.accept() => { match connection { Ok((socket, remote_addr)) => { + let shutdown = shutdown.clone().named(format!("ClientConnectionHandler_{remote_addr}")); trace!("received a socket connection from {remote_addr}"); // TODO: I think we *REALLY* need a mechanism for having a maximum number of connected // clients or spawned tokio tasks -> perhaps a worker system? @@ -61,12 +62,12 @@ impl Listener { OsRng, socket, outbound_mix_sender.clone(), - storage.clone(), active_clients_store.clone(), self.shared_state.clone(), + remote_addr, + shutdown, ); - let shutdown = shutdown.clone().named(format!("ClientConnectionHandler_{remote_addr}")); - tokio::spawn(async move { handle.start_handling(shutdown).await }); + tokio::spawn(handle.start_handling()); } Err(err) => warn!("failed to get client: {err}"), } @@ -76,18 +77,14 @@ impl Listener { } } - pub(crate) fn start( + pub(crate) fn start( mut self, outbound_mix_sender: MixForwardingSender, - storage: St, active_clients_store: ActiveClientsStore, shutdown: nym_task::TaskClient, - ) -> JoinHandle<()> - where - St: Storage + Clone + 'static, - { + ) -> JoinHandle<()> { tokio::spawn(async move { - self.run(outbound_mix_sender, storage, active_clients_store, shutdown) + self.run(outbound_mix_sender, active_clients_store, shutdown) .await }) } diff --git a/gateway/src/node/helpers.rs b/gateway/src/node/helpers.rs index 0c920181e0..854254e215 100644 --- a/gateway/src/node/helpers.rs +++ b/gateway/src/node/helpers.rs @@ -4,8 +4,8 @@ use crate::config::Config; use crate::error::GatewayError; -use crate::node::storage::PersistentStorage; use nym_crypto::asymmetric::encryption; +use nym_gateway_storage::PersistentStorage; use nym_pemstore::traits::PemStorableKeyPair; use nym_pemstore::KeyPairPath; diff --git a/gateway/src/node/mixnet_handling/receiver/connection_handler.rs b/gateway/src/node/mixnet_handling/receiver/connection_handler.rs index d07dc6489e..7d6649530f 100644 --- a/gateway/src/node/mixnet_handling/receiver/connection_handler.rs +++ b/gateway/src/node/mixnet_handling/receiver/connection_handler.rs @@ -4,11 +4,9 @@ use crate::node::client_handling::active_clients::ActiveClientsStore; use crate::node::client_handling::websocket::message_receiver::MixMessageSender; use crate::node::mixnet_handling::receiver::packet_processing::PacketProcessor; -use crate::node::storage::error::StorageError; -use crate::node::storage::Storage; use futures::channel::mpsc::SendError; use futures::StreamExt; -use log::*; +use nym_gateway_storage::{error::StorageError, Storage}; use nym_mixnet_client::forwarder::MixForwardingSender; use nym_mixnode_common::packet_processor::processor::ProcessedFinalHop; use nym_sphinx::forwarding::packet::MixPacket; @@ -21,6 +19,7 @@ use std::net::SocketAddr; use thiserror::Error; use tokio::net::TcpStream; use tokio_util::codec::Framed; +use tracing::*; // defines errors that warrant a panic if not thrown in the context of a shutdown #[derive(Debug, Error)] diff --git a/gateway/src/node/mixnet_handling/receiver/listener.rs b/gateway/src/node/mixnet_handling/receiver/listener.rs index 9ca22c77af..8f815f6068 100644 --- a/gateway/src/node/mixnet_handling/receiver/listener.rs +++ b/gateway/src/node/mixnet_handling/receiver/listener.rs @@ -2,12 +2,12 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node::mixnet_handling::receiver::connection_handler::ConnectionHandler; -use crate::node::storage::Storage; -use log::*; +use nym_gateway_storage::Storage; use nym_task::TaskClient; use std::net::SocketAddr; use std::process; use tokio::task::JoinHandle; +use tracing::*; pub(crate) struct Listener { address: SocketAddr, diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index c17c9dd4a8..47d3630b30 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -12,11 +12,10 @@ use crate::http::HttpApiBuilder; use crate::node::client_handling::active_clients::ActiveClientsStore; use crate::node::client_handling::embedded_clients::{LocalEmbeddedClientHandle, MessageRouter}; use crate::node::client_handling::websocket; -use crate::node::client_handling::websocket::connection_handler::coconut::CoconutVerifier; +use crate::node::client_handling::websocket::connection_handler::ecash::EcashManager; use crate::node::helpers::{initialise_main_storage, load_network_requester_config}; use crate::node::mixnet_handling::receiver::connection_handler::ConnectionHandler; use futures::channel::{mpsc, oneshot}; -use log::*; use nym_crypto::asymmetric::{encryption, identity}; use nym_mixnet_client::forwarder::{MixForwardingSender, PacketForwarder}; use nym_network_defaults::NymNetworkDetails; @@ -30,13 +29,14 @@ use rand::thread_rng; use std::net::SocketAddr; use std::path::PathBuf; use std::sync::Arc; +use tracing::*; pub(crate) mod client_handling; pub(crate) mod helpers; pub(crate) mod mixnet_handling; -pub(crate) mod storage; -pub use storage::{InMemStorage, PersistentStorage, Storage}; +use crate::node::client_handling::websocket::connection_handler::ecash::credential_sender::CredentialHandlerConfig; +pub use nym_gateway_storage::{PersistentStorage, Storage}; // TODO: should this struct live here? struct StartedNetworkRequester { @@ -50,7 +50,6 @@ struct StartedNetworkRequester { // TODO: should this struct live here? #[allow(unused)] struct StartedAuthenticator { - #[cfg(feature = "wireguard")] wg_api: Arc, /// Handle to interact with the local authenticator @@ -145,7 +144,6 @@ pub struct Gateway { storage: St, - #[cfg(all(feature = "wireguard", target_os = "linux"))] wireguard_data: Option, run_http_server: bool, @@ -168,7 +166,6 @@ impl Gateway { network_requester_opts, ip_packet_router_opts, authenticator_opts: None, - #[cfg(all(feature = "wireguard", target_os = "linux"))] wireguard_data: None, run_http_server: true, task_client: None, @@ -192,7 +189,6 @@ impl Gateway { identity_keypair, sphinx_keypair, storage, - #[cfg(all(feature = "wireguard", target_os = "linux"))] wireguard_data: None, run_http_server: true, task_client: None, @@ -207,7 +203,6 @@ impl Gateway { self.task_client = Some(task_client) } - #[cfg(all(feature = "wireguard", target_os = "linux"))] pub fn set_wireguard_data(&mut self, wireguard_data: nym_wireguard::WireguardData) { self.wireguard_data = Some(wireguard_data) } @@ -245,12 +240,15 @@ impl Gateway { mixnet_handling::Listener::new(listening_address, shutdown).start(connection_handler); } - #[cfg(all(feature = "wireguard", target_os = "linux"))] + #[cfg(target_os = "linux")] async fn start_authenticator( &mut self, forwarding_channel: MixForwardingSender, shutdown: TaskClient, - ) -> Result> { + ) -> Result> + where + St: Storage + Clone + 'static, + { let opts = self .authenticator_opts .as_ref() @@ -301,19 +299,26 @@ impl Gateway { MessageRouter::new(auth_mix_receiver, packet_router) .start_with_shutdown(router_shutdown); - let wg_api = - nym_wireguard::start_wireguard(shutdown, wireguard_data, peer_response_tx).await?; + let wg_api = nym_wireguard::start_wireguard( + self.storage.clone(), + shutdown, + wireguard_data, + peer_response_tx, + ) + .await?; Ok(StartedAuthenticator { wg_api, handle: LocalEmbeddedClientHandle::new(start_data.address, auth_mix_sender), }) } else { - Err(Box::new(GatewayError::WireguardNotSet)) + Err(Box::new(GatewayError::InternalWireguardError( + "wireguard not set".to_string(), + ))) } } - #[cfg(all(feature = "wireguard", not(target_os = "linux")))] + #[cfg(not(target_os = "linux"))] async fn start_authenticator( &self, _forwarding_channel: MixForwardingSender, @@ -327,9 +332,9 @@ impl Gateway { forwarding_channel: MixForwardingSender, active_clients_store: ActiveClientsStore, shutdown: TaskClient, - coconut_verifier: Arc, + ecash_verifier: Arc>, ) where - St: Storage + Clone + 'static, + St: Storage + Send + Sync + Clone + 'static, { info!("Starting client [web]socket listener..."); @@ -339,7 +344,8 @@ impl Gateway { ); let shared_state = websocket::CommonHandlerState { - coconut_verifier, + ecash_verifier, + storage: self.storage.clone(), local_identity: Arc::clone(&self.identity_keypair), only_coconut_credentials: self.config.gateway.only_coconut_credentials, bandwidth_cfg: (&self.config).into(), @@ -347,7 +353,6 @@ impl Gateway { websocket::Listener::new(listening_address, shared_state).start( forwarding_channel, - self.storage.clone(), active_clients_store, shutdown, ); @@ -576,8 +581,32 @@ impl Gateway { } } - let coconut_verifier = - CoconutVerifier::new(nyxd_client, self.config.gateway.only_coconut_credentials).await?; + let handler_config = CredentialHandlerConfig { + revocation_bandwidth_penalty: self + .config + .debug + .zk_nym_tickets + .revocation_bandwidth_penalty, + pending_poller: self.config.debug.zk_nym_tickets.pending_poller, + minimum_api_quorum: self.config.debug.zk_nym_tickets.minimum_api_quorum, + minimum_redemption_tickets: self.config.debug.zk_nym_tickets.minimum_redemption_tickets, + maximum_time_between_redemption: self + .config + .debug + .zk_nym_tickets + .maximum_time_between_redemption, + }; + + let ecash_manager = { + EcashManager::new( + handler_config, + nyxd_client, + self.identity_keypair.public_key().to_bytes(), + shutdown.fork("EcashVerifier"), + self.storage.clone(), + ) + .await + }?; let mix_forwarding_channel = self.start_packet_forwarder(shutdown.fork("PacketForwarder")); @@ -592,7 +621,7 @@ impl Gateway { mix_forwarding_channel.clone(), active_clients_store.clone(), shutdown.fork("websocket::Listener"), - Arc::new(coconut_verifier), + Arc::new(ecash_manager), ); let nr_request_filter = if self.config.network_requester.enabled { @@ -622,14 +651,15 @@ impl Gateway { info!("embedded ip packet router is disabled"); }; - #[cfg(feature = "wireguard")] - let _wg_api = { + let _wg_api = if self.wireguard_data.is_some() { let embedded_auth = self .start_authenticator(mix_forwarding_channel, shutdown.fork("authenticator")) .await .map_err(|source| GatewayError::AuthenticatorStartError { source })?; active_clients_store.insert_embedded(embedded_auth.handle); Some(embedded_auth.wg_api) + } else { + None }; if self.run_http_server { diff --git a/gateway/src/node/storage/bandwidth.rs b/gateway/src/node/storage/bandwidth.rs deleted file mode 100644 index 1ebcccea85..0000000000 --- a/gateway/src/node/storage/bandwidth.rs +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node::storage::models::{PersistedBandwidth, SpentCredential}; -use time::OffsetDateTime; - -#[derive(Clone)] -pub(crate) struct BandwidthManager { - connection_pool: sqlx::SqlitePool, -} - -impl BandwidthManager { - /// Creates new instance of the `BandwidthManager` with the provided sqlite connection pool. - /// - /// # Arguments - /// - /// * `connection_pool`: database connection pool to use. - pub(crate) fn new(connection_pool: sqlx::SqlitePool) -> Self { - BandwidthManager { connection_pool } - } - - /// Creates a new bandwidth entry for the particular client. - /// - /// # Arguments - /// - /// * `client_address_bs58`: base58-encoded address of the client. - pub(crate) async fn insert_new_client( - &self, - client_address_bs58: &str, - ) -> Result<(), sqlx::Error> { - sqlx::query!( - "INSERT INTO available_bandwidth(client_address_bs58, available) VALUES (?, 0)", - client_address_bs58 - ) - .execute(&self.connection_pool) - .await?; - Ok(()) - } - - /// Set the freepass expiration date of the particular client to the provided date. - /// - /// # Arguments - /// - /// * `client_address_bs58`: base58-encoded address of the client. - /// * `freepass_expiration`: the expiration date of the associated free pass. - pub(crate) async fn set_freepass_expiration( - &self, - client_address_bs58: &str, - freepass_expiration: OffsetDateTime, - ) -> Result<(), sqlx::Error> { - sqlx::query!( - r#" - UPDATE available_bandwidth - SET freepass_expiration = ? - WHERE client_address_bs58 = ? - "#, - freepass_expiration, - client_address_bs58 - ) - .execute(&self.connection_pool) - .await?; - Ok(()) - } - - /// Reset all the bandwidth associated with the freepass and reset its expiration date - /// - /// # Arguments - /// - /// * `client_address_bs58`: base58-encoded address of the client. - pub(crate) async fn reset_freepass_bandwidth( - &self, - client_address_bs58: &str, - ) -> Result<(), sqlx::Error> { - sqlx::query!( - r#" - UPDATE available_bandwidth - SET available = 0, freepass_expiration = NULL - WHERE client_address_bs58 = ? - "#, - client_address_bs58 - ) - .execute(&self.connection_pool) - .await?; - Ok(()) - } - - /// Tries to retrieve available bandwidth for the particular client. - /// - /// # Arguments - /// - /// * `client_address_bs58`: base58-encoded address of the client. - pub(crate) async fn get_available_bandwidth( - &self, - client_address_bs58: &str, - ) -> Result, sqlx::Error> { - sqlx::query_as("SELECT * FROM available_bandwidth WHERE client_address_bs58 = ?") - .bind(client_address_bs58) - .fetch_optional(&self.connection_pool) - .await - } - - /// Sets available bandwidth of the particular client to the provided amount; - /// - /// # Arguments - /// - /// * `client_address`: address of the client - /// * `amount`: the updated client bandwidth amount. - pub(crate) async fn set_available_bandwidth( - &self, - client_address_bs58: &str, - amount: i64, - ) -> Result<(), sqlx::Error> { - sqlx::query!( - r#" - UPDATE available_bandwidth - SET available = ? - WHERE client_address_bs58 = ? - "#, - amount, - client_address_bs58 - ) - .execute(&self.connection_pool) - .await?; - Ok(()) - } - - /// Mark received credential as spent and insert it into the storage. - /// - /// # Arguments - /// - /// * `blinded_serial_number_bs58`: the unique blinded serial number embedded in the credential - /// * `was_freepass`: indicates whether the spent credential was a freepass - /// * `client_address_bs58`: address of the client that spent the credential - pub(crate) async fn insert_spent_credential( - &self, - blinded_serial_number_bs58: &str, - was_freepass: bool, - client_address_bs58: &str, - ) -> Result<(), sqlx::Error> { - sqlx::query!( - r#" - INSERT INTO spent_credential - (blinded_serial_number_bs58, was_freepass, client_address_bs58) - VALUES (?, ?, ?) - "#, - blinded_serial_number_bs58, - was_freepass, - client_address_bs58 - ) - .execute(&self.connection_pool) - .await?; - Ok(()) - } - - /// Retrieve the spent credential with the provided blinded serial number from the storage. - /// - /// # Arguments - /// - /// * `blinded_serial_number_bs58`: the unique blinded serial number embedded in the credential - pub(crate) async fn retrieve_spent_credential( - &self, - blinded_serial_number_bs58: &str, - ) -> Result, sqlx::Error> { - sqlx::query_as!( - SpentCredential, - r#" - SELECT * FROM spent_credential - WHERE blinded_serial_number_bs58 = ? - LIMIT 1 - "#, - blinded_serial_number_bs58, - ) - .fetch_optional(&self.connection_pool) - .await - } -} diff --git a/gateway/src/node/storage/mod.rs b/gateway/src/node/storage/mod.rs deleted file mode 100644 index e7123a44a6..0000000000 --- a/gateway/src/node/storage/mod.rs +++ /dev/null @@ -1,489 +0,0 @@ -// Copyright 2020 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node::storage::bandwidth::BandwidthManager; -use crate::node::storage::error::StorageError; -use crate::node::storage::inboxes::InboxManager; -use crate::node::storage::models::{PersistedBandwidth, PersistedSharedKeys, StoredMessage}; -use crate::node::storage::shared_keys::SharedKeysManager; -use async_trait::async_trait; -use log::{debug, error}; -use nym_credentials_interface::{Base58, BlindedSerialNumber}; -use nym_gateway_requests::registration::handshake::SharedKeys; -use nym_sphinx::DestinationAddressBytes; -use sqlx::ConnectOptions; -use std::path::Path; -use time::OffsetDateTime; - -mod bandwidth; -pub(crate) mod error; -mod inboxes; -mod models; -mod shared_keys; - -#[async_trait] -pub trait Storage: Send + Sync { - /// Inserts provided derived shared keys into the database. - /// If keys previously existed for the provided client, they are overwritten with the new data. - /// - /// # Arguments - /// - /// * `client_address`: address of the client - /// * `shared_keys`: shared encryption (AES128CTR) and mac (hmac-blake3) derived shared keys to store. - async fn insert_shared_keys( - &self, - client_address: DestinationAddressBytes, - shared_keys: &SharedKeys, - ) -> Result<(), StorageError>; - - /// Tries to retrieve shared keys stored for the particular client. - /// - /// # Arguments - /// - /// * `client_address`: address of the client - async fn get_shared_keys( - &self, - client_address: DestinationAddressBytes, - ) -> Result, StorageError>; - - /// Removes from the database shared keys derived with the particular client. - /// - /// # Arguments - /// - /// * `client_address`: address of the client - // currently there is no code flow that causes removal (not overwriting) - // of the stored keys. However, retain the function for consistency and completion sake - #[allow(dead_code)] - async fn remove_shared_keys( - &self, - client_address: DestinationAddressBytes, - ) -> Result<(), StorageError>; - - /// Inserts new message to the storage for an offline client for future retrieval. - /// - /// # Arguments - /// - /// * `client_address`: address of the client - /// * `message`: raw message to store. - async fn store_message( - &self, - client_address: DestinationAddressBytes, - message: Vec, - ) -> Result<(), StorageError>; - - /// Retrieves messages stored for the particular client specified by the provided address. - /// - /// # Arguments - /// - /// * `client_address`: address of the client - /// * `start_after`: optional starting id of the messages to grab - /// - /// returns the retrieved messages alongside optional id of the last message retrieved if - /// there are more messages to retrieve. - async fn retrieve_messages( - &self, - client_address: DestinationAddressBytes, - start_after: Option, - ) -> Result<(Vec, Option), StorageError>; - - /// Removes messages with the specified ids - /// - /// # Arguments - /// - /// * `ids`: ids of the messages to remove - async fn remove_messages(&self, ids: Vec) -> Result<(), StorageError>; - - /// Creates a new bandwidth entry for the particular client. - /// - /// # Arguments - /// - /// * `client_address`: address of the client - async fn create_bandwidth_entry( - &self, - client_address: DestinationAddressBytes, - ) -> Result<(), StorageError>; - - /// Set the freepass expiration date of the particular client to the provided date. - /// - /// # Arguments - /// - /// * `client_address`: address of the client - /// * `freepass_expiration`: the expiration date of the associated free pass. - async fn set_freepass_expiration( - &self, - client_address: DestinationAddressBytes, - freepass_expiration: OffsetDateTime, - ) -> Result<(), StorageError>; - - /// Reset all the bandwidth associated with the freepass and reset its expiration date - /// - /// # Arguments - /// - /// * `client_address`: address of the client - async fn reset_freepass_bandwidth( - &self, - client_address: DestinationAddressBytes, - ) -> Result<(), StorageError>; - - /// Tries to retrieve available bandwidth for the particular client. - /// - /// # Arguments - /// - /// * `client_address`: address of the client - async fn get_available_bandwidth( - &self, - client_address: DestinationAddressBytes, - ) -> Result, StorageError>; - - /// Sets available bandwidth of the particular client to the provided amount; - /// - /// # Arguments - /// - /// * `client_address`: address of the client - /// * `amount`: the updated client bandwidth amount. - async fn set_bandwidth( - &self, - client_address: DestinationAddressBytes, - amount: i64, - ) -> Result<(), StorageError>; - - /// Mark received credential as spent and insert it into the storage. - /// - /// # Arguments - /// - /// * `blinded_serial_number`: the unique blinded serial number embedded in the credential - /// * `client_address`: address of the client that spent the credential - async fn insert_spent_credential( - &self, - blinded_serial_number: BlindedSerialNumber, - was_freepass: bool, - client_address: DestinationAddressBytes, - ) -> Result<(), StorageError>; - - /// Check if the credential with the provided blinded serial number if already present in the storage. - /// - /// # Arguments - /// - /// * `blinded_serial_number`: the unique blinded serial number embedded in the credential - async fn contains_credential( - &self, - blinded_serial_number: &BlindedSerialNumber, - ) -> Result; -} - -// note that clone here is fine as upon cloning the same underlying pool will be used -#[derive(Clone)] -pub struct PersistentStorage { - shared_key_manager: SharedKeysManager, - inbox_manager: InboxManager, - bandwidth_manager: BandwidthManager, -} - -impl PersistentStorage { - /// Initialises `PersistentStorage` using the provided path. - /// - /// # Arguments - /// - /// * `database_path`: path to the database. - /// * `message_retrieval_limit`: maximum number of stored client messages that can be retrieved at once. - pub async fn init + Send>( - database_path: P, - message_retrieval_limit: i64, - ) -> Result { - debug!( - "Attempting to connect to database {:?}", - database_path.as_ref().as_os_str() - ); - - // TODO: we can inject here more stuff based on our gateway global config - // struct. Maybe different pool size or timeout intervals? - let mut opts = sqlx::sqlite::SqliteConnectOptions::new() - .filename(database_path) - .create_if_missing(true); - - // TODO: do we want auto_vacuum ? - - opts.disable_statement_logging(); - - let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { - Ok(db) => db, - Err(err) => { - error!("Failed to connect to SQLx database: {err}"); - return Err(err.into()); - } - }; - - if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await { - error!("Failed to perform migration on the SQLx database: {err}"); - return Err(err.into()); - } - - // the cloning here are cheap as connection pool is stored behind an Arc - Ok(PersistentStorage { - shared_key_manager: SharedKeysManager::new(connection_pool.clone()), - inbox_manager: InboxManager::new(connection_pool.clone(), message_retrieval_limit), - bandwidth_manager: BandwidthManager::new(connection_pool), - }) - } -} - -#[async_trait] -impl Storage for PersistentStorage { - async fn insert_shared_keys( - &self, - client_address: DestinationAddressBytes, - shared_keys: &SharedKeys, - ) -> Result<(), StorageError> { - let persisted_shared_keys = PersistedSharedKeys { - client_address_bs58: client_address.as_base58_string(), - derived_aes128_ctr_blake3_hmac_keys_bs58: shared_keys.to_base58_string(), - }; - self.shared_key_manager - .insert_shared_keys(persisted_shared_keys) - .await?; - Ok(()) - } - - async fn get_shared_keys( - &self, - client_address: DestinationAddressBytes, - ) -> Result, StorageError> { - let keys = self - .shared_key_manager - .get_shared_keys(&client_address.as_base58_string()) - .await?; - Ok(keys) - } - - #[allow(dead_code)] - async fn remove_shared_keys( - &self, - client_address: DestinationAddressBytes, - ) -> Result<(), StorageError> { - self.shared_key_manager - .remove_shared_keys(&client_address.as_base58_string()) - .await?; - Ok(()) - } - - async fn store_message( - &self, - client_address: DestinationAddressBytes, - message: Vec, - ) -> Result<(), StorageError> { - self.inbox_manager - .insert_message(&client_address.as_base58_string(), message) - .await?; - Ok(()) - } - - async fn retrieve_messages( - &self, - client_address: DestinationAddressBytes, - start_after: Option, - ) -> Result<(Vec, Option), StorageError> { - let messages = self - .inbox_manager - .get_messages(&client_address.as_base58_string(), start_after) - .await?; - Ok(messages) - } - - async fn remove_messages(&self, ids: Vec) -> Result<(), StorageError> { - for id in ids { - self.inbox_manager.remove_message(id).await?; - } - Ok(()) - } - - async fn create_bandwidth_entry( - &self, - client_address: DestinationAddressBytes, - ) -> Result<(), StorageError> { - self.bandwidth_manager - .insert_new_client(&client_address.as_base58_string()) - .await?; - Ok(()) - } - - async fn set_freepass_expiration( - &self, - client_address: DestinationAddressBytes, - freepass_expiration: OffsetDateTime, - ) -> Result<(), StorageError> { - self.bandwidth_manager - .set_freepass_expiration(&client_address.as_base58_string(), freepass_expiration) - .await?; - Ok(()) - } - - async fn reset_freepass_bandwidth( - &self, - client_address: DestinationAddressBytes, - ) -> Result<(), StorageError> { - self.bandwidth_manager - .reset_freepass_bandwidth(&client_address.as_base58_string()) - .await?; - Ok(()) - } - - async fn get_available_bandwidth( - &self, - client_address: DestinationAddressBytes, - ) -> Result, StorageError> { - Ok(self - .bandwidth_manager - .get_available_bandwidth(&client_address.as_base58_string()) - .await?) - } - - async fn set_bandwidth( - &self, - client_address: DestinationAddressBytes, - amount: i64, - ) -> Result<(), StorageError> { - self.bandwidth_manager - .set_available_bandwidth(&client_address.as_base58_string(), amount) - .await?; - Ok(()) - } - - async fn insert_spent_credential( - &self, - blinded_serial_number: BlindedSerialNumber, - was_freepass: bool, - client_address: DestinationAddressBytes, - ) -> Result<(), StorageError> { - self.bandwidth_manager - .insert_spent_credential( - &blinded_serial_number.to_bs58(), - was_freepass, - &client_address.as_base58_string(), - ) - .await?; - Ok(()) - } - - async fn contains_credential( - &self, - blinded_serial_number: &BlindedSerialNumber, - ) -> Result { - let cred = self - .bandwidth_manager - .retrieve_spent_credential(&blinded_serial_number.to_bs58()) - .await?; - - Ok(cred.is_some()) - } -} - -/// In-memory implementation of `Storage`. The intention is primarily in testing environments. -#[derive(Clone)] -pub struct InMemStorage; - -//#[cfg(test)] -//impl InMemStorage { -// #[allow(unused)] -// async fn init + Send>() -> Result { -// todo!() -// } -//} - -#[cfg(test)] -#[async_trait] -impl Storage for InMemStorage { - async fn insert_shared_keys( - &self, - _client_address: DestinationAddressBytes, - _shared_keys: &SharedKeys, - ) -> Result<(), StorageError> { - todo!() - } - - async fn get_shared_keys( - &self, - _client_address: DestinationAddressBytes, - ) -> Result, StorageError> { - todo!() - } - - async fn remove_shared_keys( - &self, - _client_address: DestinationAddressBytes, - ) -> Result<(), StorageError> { - todo!() - } - - async fn store_message( - &self, - _client_address: DestinationAddressBytes, - _message: Vec, - ) -> Result<(), StorageError> { - todo!() - } - - async fn retrieve_messages( - &self, - _client_address: DestinationAddressBytes, - _start_after: Option, - ) -> Result<(Vec, Option), StorageError> { - todo!() - } - - async fn remove_messages(&self, _ids: Vec) -> Result<(), StorageError> { - todo!() - } - - async fn create_bandwidth_entry( - &self, - _client_address: DestinationAddressBytes, - ) -> Result<(), StorageError> { - todo!() - } - - async fn set_freepass_expiration( - &self, - _client_address: DestinationAddressBytes, - _freepass_expiration: OffsetDateTime, - ) -> Result<(), StorageError> { - todo!() - } - - async fn reset_freepass_bandwidth( - &self, - _client_address: DestinationAddressBytes, - ) -> Result<(), StorageError> { - todo!() - } - - async fn get_available_bandwidth( - &self, - _client_address: DestinationAddressBytes, - ) -> Result, StorageError> { - todo!() - } - - async fn set_bandwidth( - &self, - _client_address: DestinationAddressBytes, - _amount: i64, - ) -> Result<(), StorageError> { - todo!() - } - - async fn insert_spent_credential( - &self, - _blinded_serial_number: BlindedSerialNumber, - _was_freepass: bool, - _client_address: DestinationAddressBytes, - ) -> Result<(), StorageError> { - todo!() - } - - async fn contains_credential( - &self, - _blinded_serial_number: &BlindedSerialNumber, - ) -> Result { - todo!() - } -} diff --git a/gateway/src/node/storage/models.rs b/gateway/src/node/storage/models.rs deleted file mode 100644 index 999c9f827f..0000000000 --- a/gateway/src/node/storage/models.rs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2021-2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node::client_handling::websocket::connection_handler::AvailableBandwidth; -use sqlx::FromRow; -use time::OffsetDateTime; - -pub struct PersistedSharedKeys { - pub(crate) client_address_bs58: String, - pub(crate) derived_aes128_ctr_blake3_hmac_keys_bs58: String, -} - -pub struct StoredMessage { - pub(crate) id: i64, - #[allow(dead_code)] - pub(crate) client_address_bs58: String, - pub(crate) content: Vec, -} - -#[derive(Debug, Clone, FromRow)] -pub struct PersistedBandwidth { - #[allow(dead_code)] - pub(crate) client_address_bs58: String, - pub(crate) available: i64, - pub(crate) freepass_expiration: Option, -} - -impl From for AvailableBandwidth { - fn from(value: PersistedBandwidth) -> Self { - AvailableBandwidth { - bytes: value.available, - freepass_expiration: value.freepass_expiration, - } - } -} - -impl From> for AvailableBandwidth { - fn from(value: Option) -> Self { - match value { - None => AvailableBandwidth::default(), - Some(b) => b.into(), - } - } -} - -#[derive(Debug, Clone, FromRow)] -pub struct SpentCredential { - #[allow(dead_code)] - pub(crate) blinded_serial_number_bs58: String, - #[allow(dead_code)] - pub(crate) was_freepass: bool, - #[allow(dead_code)] - pub(crate) client_address_bs58: String, -} diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 1d11d5994c..800a85bde4 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-api" license = "GPL-3.0" -version = "1.1.42" +version = "1.1.43" authors = [ "Dave Hrycyszyn ", "Jędrzej Stuczyński ", @@ -19,6 +19,8 @@ rust-version = "1.76.0" async-trait = { workspace = true } bs58 = { workspace = true } bip39 = { workspace = true } +bincode.workspace = true +bloomfilter = "1.0.13" cfg-if = { workspace = true } clap = { workspace = true, features = ["cargo", "derive"] } console-subscriber = { workspace = true, optional = true } # validator-api needs to be built with RUSTFLAGS="--cfg tokio_unstable" @@ -52,6 +54,7 @@ sqlx = { workspace = true, features = [ "sqlite", "macros", "migrate", + "time", ] } okapi = { workspace = true, features = ["impl_json_schema"] } @@ -70,12 +73,16 @@ zeroize = { workspace = true } ## internal #ephemera = { path = "../ephemera" } nym-bandwidth-controller = { path = "../common/bandwidth-controller" } -nym-coconut-bandwidth-contract-common = { path = "../common/cosmwasm-smart-contracts/coconut-bandwidth-contract" } +nym-ecash-contract-common = { path = "../common/cosmwasm-smart-contracts/ecash-contract" } +nym-ecash-double-spending = { path = "../common/ecash-double-spending" } +nym-ecash-time = { path = "../common/ecash-time", features = ["expiration"] } nym-coconut-dkg-common = { path = "../common/cosmwasm-smart-contracts/coconut-dkg" } +nym-compact-ecash = { path = "../common/nym_offline_compact_ecash" } +nym-credentials-interface = { path = "../common/credentials-interface" } #nym-ephemera-common = { path = "../common/cosmwasm-smart-contracts/ephemera" } nym-config = { path = "../common/config" } cosmwasm-std = { workspace = true } -nym-credential-storage = { path = "../common/credential-storage" } +nym-credential-storage = { path = "../common/credential-storage", features = ["persistent-storage"] } nym-credentials = { path = "../common/credentials" } nym-crypto = { path = "../common/crypto" } cw2 = { workspace = true } @@ -93,7 +100,7 @@ nym-sphinx = { path = "../common/nymsphinx" } nym-pemstore = { path = "../common/pemstore" } nym-task = { path = "../common/task" } nym-topology = { path = "../common/topology" } -nym-api-requests = { path = "nym-api-requests", features = ["request-parsing"] } +nym-api-requests = { path = "nym-api-requests", features = ["rocket-traits"] } nym-validator-client = { path = "../common/client-libs/validator-client" } nym-bin-common = { path = "../common/bin-common", features = ["output_format"] } nym-node-tester-utils = { path = "../common/node-tester-utils" } diff --git a/nym-api/migrations/20240708120000_ecash_tables.sql b/nym-api/migrations/20240708120000_ecash_tables.sql new file mode 100644 index 0000000000..ca2daf6fd9 --- /dev/null +++ b/nym-api/migrations/20240708120000_ecash_tables.sql @@ -0,0 +1,101 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + + +CREATE TABLE bloomfilter_parameters ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + num_hashes INTEGER NOT NULL, + bitmap_size INTEGER NOT NULL, + + sip0_key0 BLOB NOT NULL, + sip0_key1 BLOB NOT NULL, + + sip1_key0 BLOB NOT NULL, + sip1_key1 BLOB NOT NULL +); + +-- table containing partial bloomfilters produced from tickets spent on particular date +-- the 'current' bloomfilter is always OR(last_30) +CREATE TABLE partial_bloomfilter ( + date DATE NOT NULL UNIQUE PRIMARY KEY, + parameters INTEGER NOT NULL REFERENCES bloomfilter_parameters(id), + bitmap BLOB NOT NULL +); + +CREATE TABLE master_verification_key ( + epoch_id INTEGER PRIMARY KEY NOT NULL, + serialised_key BLOB NOT NULL +); + +CREATE TABLE global_coin_index_signatures ( + -- we can only have a single entry + epoch_id INTEGER PRIMARY KEY NOT NULL, + + -- combined signatures for all indices + serialised_signatures BLOB NOT NULL +); + +CREATE TABLE partial_coin_index_signatures ( + -- we can only have a single entry + epoch_id INTEGER PRIMARY KEY NOT NULL, + + serialised_signatures BLOB NOT NULL +); + +CREATE TABLE global_expiration_date_signatures ( + expiration_date DATE NOT NULL UNIQUE PRIMARY KEY, + + epoch_id INTEGER NOT NULL, + + -- combined signatures for all tuples issued for given day + serialised_signatures BLOB NOT NULL +); + +CREATE TABLE partial_expiration_date_signatures ( + expiration_date DATE NOT NULL UNIQUE PRIMARY KEY, + + epoch_id INTEGER NOT NULL, + + serialised_signatures BLOB NOT NULL +); + + +DROP TABLE issued_credential; + +-- particular **partial** ecash ticketbook issued in this epoch +CREATE TABLE issued_ticketbook +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + epoch_id INTEGER NOT NULL, + deposit_id INTEGER NOT NULL UNIQUE, + partial_credential BLOB NOT NULL, + signature BLOB NOT NULL, + joined_private_commitments BLOB NOT NULL, + expiration_date DATE NOT NULL, + ticketbook_type_repr INTEGER NOT NULL +); + +CREATE TABLE ticket_providers +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + gateway_address TEXT NOT NULL UNIQUE, + last_batch_verification TIMESTAMP WITHOUT TIME ZONE +); + +CREATE TABLE verified_tickets +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + ticket_data BLOB NOT NULL, + serial_number BLOB NOT NULL UNIQUE, + spending_date DATE NOT NULL, + verified_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + gateway_id INTEGER NOT NULL REFERENCES ticket_providers(id) +); + +-- helper index for getting tickets verified by particular gateway +CREATE INDEX verified_tickets_index ON verified_tickets (gateway_id, verified_at desc); + +-- helper index for getting all tickets with particular spending date for rebuilding the bloomfilters +CREATE INDEX verified_tickets_spending_index ON verified_tickets (spending_date); \ No newline at end of file diff --git a/nym-api/nym-api-requests/Cargo.toml b/nym-api/nym-api-requests/Cargo.toml index 7ed5d2ad18..a4730db70e 100644 --- a/nym-api/nym-api-requests/Cargo.toml +++ b/nym-api/nym-api-requests/Cargo.toml @@ -16,14 +16,20 @@ serde = { workspace = true, features = ["derive"] } ts-rs = { workspace = true, optional = true } tendermint = { workspace = true } time = { workspace = true, features = ["serde", "parsing", "formatting"] } +thiserror.workspace = true rocket = { workspace = true, optional = true } +sha2 = "0.10.8" + # for serde on secp256k1 signatures ecdsa = { workspace = true, features = ["serde"] } +nym-serde-helpers = { path = "../../common/serde-helpers", features = ["bs58", "base64"] } nym-credentials-interface = { path = "../../common/credentials-interface" } nym-crypto = { path = "../../common/crypto", features = ["serde", "asymmetric"] } +nym-ecash-time = { path = "../../common/ecash-time" } +nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } nym-node-requests = { path = "../../nym-node/nym-node-requests", default-features = false } @@ -33,5 +39,5 @@ serde_json.workspace = true [features] default = [] -request-parsing = ["rocket"] +rocket-traits = ["rocket"] generate-ts = ["ts-rs", "nym-mixnet-contract-common/generate-ts"] diff --git a/nym-api/nym-api-requests/src/coconut/models.rs b/nym-api/nym-api-requests/src/coconut/models.rs deleted file mode 100644 index 50df750a82..0000000000 --- a/nym-api/nym-api-requests/src/coconut/models.rs +++ /dev/null @@ -1,287 +0,0 @@ -// Copyright 2023-2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::coconut::helpers::issued_credential_plaintext; -use cosmrs::AccountId; -use nym_credentials_interface::{ - hash_to_scalar, Attribute, BlindSignRequest, BlindedSignature, Bytable, CoconutError, - CredentialSpendingData, VerificationKey, -}; -use nym_crypto::asymmetric::identity; -use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; -use tendermint::hash::Hash; - -#[derive(Serialize, Deserialize)] -pub struct VerifyCredentialBody { - /// The cryptographic material required for spending the underlying credential. - pub credential_data: CredentialSpendingData, - - /// Multisig proposal for releasing funds for the provided bandwidth credential - pub proposal_id: u64, - - /// Cosmos address of the spender of the credential - pub gateway_cosmos_addr: AccountId, -} - -impl VerifyCredentialBody { - pub fn new( - credential_data: CredentialSpendingData, - proposal_id: u64, - gateway_cosmos_addr: AccountId, - ) -> VerifyCredentialBody { - VerifyCredentialBody { - credential_data, - proposal_id, - gateway_cosmos_addr, - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct VerifyCredentialResponse { - pub verification_result: bool, -} - -impl VerifyCredentialResponse { - pub fn new(verification_result: bool) -> Self { - VerifyCredentialResponse { - verification_result, - } - } -} - -// All strings are base58 encoded representations of structs -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct BlindSignRequestBody { - pub inner_sign_request: BlindSignRequest, - - /// Hash of the deposit transaction - pub tx_hash: Hash, - - /// Signature on the inner sign request and the tx hash - pub signature: identity::Signature, - - pub public_attributes_plain: Vec, -} - -impl BlindSignRequestBody { - pub fn new( - inner_sign_request: BlindSignRequest, - tx_hash: Hash, - signature: identity::Signature, - public_attributes_plain: Vec, - ) -> BlindSignRequestBody { - BlindSignRequestBody { - inner_sign_request, - tx_hash, - signature, - public_attributes_plain, - } - } - - pub fn attributes(&self) -> u32 { - (self.public_attributes_plain.len() + self.inner_sign_request.num_private_attributes()) - as u32 - } - - pub fn public_attributes_hashed(&self) -> Vec { - self.public_attributes_plain - .iter() - .map(hash_to_scalar) - .collect() - } - - pub fn encode_commitments(&self) -> Vec { - use nym_credentials_interface::Base58; - - self.inner_sign_request - .get_private_attributes_pedersen_commitments() - .iter() - .map(|c| c.to_bs58()) - .collect() - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct FreePassNonceResponse { - pub current_nonce: [u8; 16], -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct BlindedSignatureResponse { - pub blinded_signature: BlindedSignature, -} - -impl BlindedSignatureResponse { - pub fn new(blinded_signature: BlindedSignature) -> BlindedSignatureResponse { - BlindedSignatureResponse { blinded_signature } - } - - pub fn to_base58_string(&self) -> String { - bs58::encode(&self.to_bytes()).into_string() - } - - pub fn from_base58_string>(val: I) -> Result { - let bytes = bs58::decode(val).into_vec()?; - Self::from_bytes(&bytes) - } - - pub fn to_bytes(&self) -> Vec { - self.blinded_signature.to_byte_vec() - } - - pub fn from_bytes(bytes: &[u8]) -> Result { - Ok(BlindedSignatureResponse { - blinded_signature: BlindedSignature::from_bytes(bytes)?, - }) - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct FreePassRequest { - // secp256k1 key associated with the admin account - pub cosmos_pubkey: cosmrs::crypto::PublicKey, - - pub inner_sign_request: BlindSignRequest, - - // we need to include a nonce here to prevent replay attacks - // (and not making the nym-api store the serial numbers of all issued credential) - pub used_nonce: [u8; 16], - - /// Signature on the nonce - /// to prove the possession of the cosmos key/address - pub nonce_signature: cosmrs::crypto::secp256k1::Signature, - - pub public_attributes_plain: Vec, -} - -impl FreePassRequest { - pub fn new( - cosmos_pubkey: cosmrs::crypto::PublicKey, - inner_sign_request: BlindSignRequest, - used_nonce: [u8; 16], - nonce_signature: cosmrs::crypto::secp256k1::Signature, - public_attributes_plain: Vec, - ) -> Self { - FreePassRequest { - cosmos_pubkey, - inner_sign_request, - used_nonce, - nonce_signature, - public_attributes_plain, - } - } - - pub fn tendermint_pubkey(&self) -> tendermint::PublicKey { - self.cosmos_pubkey.into() - } - - pub fn public_attributes_hashed(&self) -> Vec { - self.public_attributes_plain - .iter() - .map(hash_to_scalar) - .collect() - } -} - -#[derive(Serialize, Deserialize)] -pub struct VerificationKeyResponse { - pub key: VerificationKey, -} - -impl VerificationKeyResponse { - pub fn new(key: VerificationKey) -> VerificationKeyResponse { - VerificationKeyResponse { key } - } -} - -#[derive(Serialize, Deserialize)] -pub struct CosmosAddressResponse { - pub addr: AccountId, -} - -impl CosmosAddressResponse { - pub fn new(addr: AccountId) -> CosmosAddressResponse { - CosmosAddressResponse { addr } - } -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct Pagination { - /// last_key is the last value returned in the previous query. - /// it's used to indicate the start of the next (this) page. - /// the value itself is not included in the response. - pub last_key: Option, - - /// limit is the total number of results to be returned in the result page. - /// If left empty it will default to a value to be set by each app. - pub limit: Option, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct CredentialsRequestBody { - /// Explicit ids of the credentials to retrieve. Note: it can't be set alongside pagination. - pub credential_ids: Vec, - - /// Pagination settings for retrieving credentials. Note: it can't be set alongside explicit ids. - pub pagination: Option>, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct EpochCredentialsResponse { - pub epoch_id: u64, - pub first_epoch_credential_id: Option, - pub total_issued: u32, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct IssuedCredentialsResponse { - // note: BTreeMap returns ordered results so it's fine to use it with pagination - pub credentials: BTreeMap, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct IssuedCredentialResponse { - pub credential: Option, -} - -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct IssuedCredentialBody { - pub credential: IssuedCredential, - - pub signature: identity::Signature, -} - -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct IssuedCredential { - pub id: i64, - pub epoch_id: u32, - pub tx_hash: Hash, - - // NOTE: if we find creation of this guy takes too long, - // change `BlindedSignature` to `BlindedSignatureBytes` - // so that nym-api wouldn't need to parse the value out of its storage - pub blinded_partial_credential: BlindedSignature, - pub bs58_encoded_private_attributes_commitments: Vec, - pub public_attributes: Vec, -} - -impl IssuedCredential { - // this method doesn't have to be reversible so just naively concatenate everything - pub fn signable_plaintext(&self) -> Vec { - issued_credential_plaintext( - self.epoch_id, - self.tx_hash, - &self.blinded_partial_credential, - &self.bs58_encoded_private_attributes_commitments, - &self.public_attributes, - ) - } -} diff --git a/nym-api/nym-api-requests/src/constants.rs b/nym-api/nym-api-requests/src/constants.rs new file mode 100644 index 0000000000..f3bee4522c --- /dev/null +++ b/nym-api/nym-api-requests/src/constants.rs @@ -0,0 +1,7 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use time::Duration; + +// we should probably monitor this constant and adjust it when/if required +pub const MIN_BATCH_REDEMPTION_DELAY: Duration = Duration::DAY; diff --git a/nym-api/nym-api-requests/src/coconut/helpers.rs b/nym-api/nym-api-requests/src/ecash/helpers.rs similarity index 50% rename from nym-api/nym-api-requests/src/coconut/helpers.rs rename to nym-api/nym-api-requests/src/ecash/helpers.rs index 73d1a7f42f..936fced5dd 100644 --- a/nym-api/nym-api-requests/src/coconut/helpers.rs +++ b/nym-api/nym-api-requests/src/ecash/helpers.rs @@ -1,33 +1,34 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_credentials_interface::BlindedSignature; -use tendermint::hash::Hash; +use nym_compact_ecash::BlindedSignature; +use nym_credentials_interface::TicketType; +use nym_ecash_time::EcashTime; +use std::iter::once; +use time::Date; // recomputes plaintext on the credential nym-api has used for signing // // note: this method doesn't have to be reversible so just naively concatenate everything pub fn issued_credential_plaintext( epoch_id: u32, - tx_hash: Hash, + deposit_id: u32, blinded_partial_credential: &BlindedSignature, - bs58_encoded_private_attributes_commitments: &[String], - public_attributes: &[String], + encoded_private_attributes_commitments: &[Vec], + expiration_date: Date, + ticketbook_type: TicketType, ) -> Vec { epoch_id .to_be_bytes() .into_iter() - .chain(tx_hash.as_bytes().iter().copied()) + .chain(deposit_id.to_be_bytes()) .chain(blinded_partial_credential.to_bytes()) .chain( - bs58_encoded_private_attributes_commitments + encoded_private_attributes_commitments .iter() - .flat_map(|attr| attr.as_bytes().iter().copied()), - ) - .chain( - public_attributes - .iter() - .flat_map(|attr| attr.as_bytes().iter().copied()), + .flat_map(|attr| attr.iter().copied()), ) + .chain(expiration_date.ecash_unix_timestamp().to_be_bytes()) + .chain(once(ticketbook_type.to_repr() as u8)) .collect() } diff --git a/nym-api/nym-api-requests/src/coconut/mod.rs b/nym-api/nym-api-requests/src/ecash/mod.rs similarity index 59% rename from nym-api/nym-api-requests/src/coconut/mod.rs rename to nym-api/nym-api-requests/src/ecash/mod.rs index 1971f0b8f8..60f3c1c41c 100644 --- a/nym-api/nym-api-requests/src/coconut/mod.rs +++ b/nym-api/nym-api-requests/src/ecash/mod.rs @@ -5,6 +5,7 @@ pub mod helpers; pub mod models; pub use models::{ - BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody, FreePassRequest, - VerificationKeyResponse, VerifyCredentialBody, VerifyCredentialResponse, + BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody, + PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse, + VerificationKeyResponse, VerifyEcashCredentialBody, }; diff --git a/nym-api/nym-api-requests/src/ecash/models.rs b/nym-api/nym-api-requests/src/ecash/models.rs new file mode 100644 index 0000000000..bb3453463c --- /dev/null +++ b/nym-api/nym-api-requests/src/ecash/models.rs @@ -0,0 +1,461 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash::helpers::issued_credential_plaintext; +use crate::helpers::PlaceholderJsonSchemaImpl; +use cosmrs::AccountId; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use nym_compact_ecash::Bytable; +use nym_credentials_interface::TicketType; +use nym_credentials_interface::{ + BlindedSignature, CompactEcashError, CredentialSpendingData, PublicKeyUser, + VerificationKeyAuth, WithdrawalRequest, +}; +use nym_crypto::asymmetric::identity; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use sha2::Digest; +use std::collections::BTreeMap; +use std::ops::Deref; +use thiserror::Error; +use time::Date; + +#[derive(Serialize, Deserialize, Clone, JsonSchema)] +pub struct VerifyEcashTicketBody { + /// The cryptographic material required for spending the underlying credential. + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub credential: CredentialSpendingData, + + /// Cosmos address of the sender of the credential + #[schemars(with = "String")] + pub gateway_cosmos_addr: AccountId, +} + +#[derive(Serialize, Deserialize, Clone, JsonSchema)] +pub struct VerifyEcashCredentialBody { + /// The cryptographic material required for spending the underlying credential. + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub credential: CredentialSpendingData, + + /// Cosmos address of the sender of the credential + #[schemars(with = "String")] + pub gateway_cosmos_addr: AccountId, + + /// Multisig proposal for releasing funds for the provided bandwidth credential + pub proposal_id: u64, +} + +impl VerifyEcashCredentialBody { + pub fn new( + credential: CredentialSpendingData, + gateway_cosmos_addr: AccountId, + proposal_id: u64, + ) -> VerifyEcashCredentialBody { + VerifyEcashCredentialBody { + credential, + gateway_cosmos_addr, + proposal_id, + } + } +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct EcashTicketVerificationResponse { + pub verified: Result<(), EcashTicketVerificationRejection>, +} + +impl EcashTicketVerificationResponse { + pub fn reject(reason: EcashTicketVerificationRejection) -> Self { + EcashTicketVerificationResponse { + verified: Err(reason), + } + } +} + +#[derive(Debug, Error, Serialize, Deserialize, JsonSchema)] +pub enum EcashTicketVerificationRejection { + #[error("invalid ticket spent date. expected either today's ({today}) or yesterday's* ({yesterday}) date but got {received} instead\n*assuming it's before 1AM UTC")] + InvalidSpentDate { + #[schemars(with = "String")] + #[serde(with = "crate::helpers::date_serde")] + today: Date, + #[schemars(with = "String")] + #[serde(with = "crate::helpers::date_serde")] + yesterday: Date, + #[schemars(with = "String")] + #[serde(with = "crate::helpers::date_serde")] + received: Date, + }, + + #[error("this ticket has already been received before")] + ReplayedTicket, + + #[error("this ticket has already been spent before")] + DoubleSpend, + + #[error("failed to verify the provided ticket")] + InvalidTicket, + + #[error( + "the received payment contained more than a single ticket. that's currently not supported" + )] + MultipleTickets, +} + +// All strings are base58 encoded representations of structs +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, JsonSchema)] +pub struct BlindSignRequestBody { + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub inner_sign_request: WithdrawalRequest, + + /// the id of the associated deposit + pub deposit_id: u32, + + /// Signature on the inner sign request and the tx hash + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub signature: identity::Signature, + + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub ecash_pubkey: PublicKeyUser, + + #[schemars(with = "String")] + #[serde(with = "crate::helpers::date_serde")] + pub expiration_date: Date, + + #[schemars(with = "String")] + pub ticketbook_type: TicketType, +} + +impl BlindSignRequestBody { + pub fn new( + inner_sign_request: WithdrawalRequest, + deposit_id: u32, + signature: identity::Signature, + ecash_pubkey: PublicKeyUser, + expiration_date: Date, + ticketbook_type: TicketType, + ) -> BlindSignRequestBody { + BlindSignRequestBody { + inner_sign_request, + deposit_id, + signature, + ecash_pubkey, + expiration_date, + ticketbook_type, + } + } + + pub fn encode_commitments(&self) -> Vec> { + self.inner_sign_request + .get_private_attributes_commitments() + .iter() + .map(|c| c.to_byte_vec()) + .collect() + } +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct BlindedSignatureResponse { + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub blinded_signature: BlindedSignature, +} + +impl BlindedSignatureResponse { + pub fn new(blinded_signature: BlindedSignature) -> BlindedSignatureResponse { + BlindedSignatureResponse { blinded_signature } + } + + pub fn to_base58_string(&self) -> String { + bs58::encode(&self.to_bytes()).into_string() + } + + pub fn from_base58_string>(val: I) -> Result { + let bytes = bs58::decode(val).into_vec()?; + Self::from_bytes(&bytes) + } + + pub fn to_bytes(&self) -> Vec { + self.blinded_signature.to_bytes().to_vec() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + Ok(BlindedSignatureResponse { + blinded_signature: BlindedSignature::from_bytes(bytes)?, + }) + } +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct MasterVerificationKeyResponse { + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub key: VerificationKeyAuth, +} + +impl MasterVerificationKeyResponse { + pub fn new(key: VerificationKeyAuth) -> MasterVerificationKeyResponse { + MasterVerificationKeyResponse { key } + } +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct VerificationKeyResponse { + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub key: VerificationKeyAuth, +} + +impl VerificationKeyResponse { + pub fn new(key: VerificationKeyAuth) -> VerificationKeyResponse { + VerificationKeyResponse { key } + } +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct CosmosAddressResponse { + #[schemars(with = "String")] + pub addr: AccountId, +} + +impl CosmosAddressResponse { + pub fn new(addr: AccountId) -> CosmosAddressResponse { + CosmosAddressResponse { addr } + } +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct PartialExpirationDateSignatureResponse { + pub epoch_id: u64, + + #[schemars(with = "String")] + #[serde(with = "crate::helpers::date_serde")] + pub expiration_date: Date, + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub signatures: Vec, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct PartialCoinIndicesSignatureResponse { + pub epoch_id: u64, + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub signatures: Vec, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct AggregatedExpirationDateSignatureResponse { + pub epoch_id: u64, + + #[schemars(with = "String")] + #[serde(with = "crate::helpers::date_serde")] + pub expiration_date: Date, + + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub signatures: Vec, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct AggregatedCoinIndicesSignatureResponse { + pub epoch_id: u64, + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub signatures: Vec, +} + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +pub struct Pagination { + /// last_key is the last value returned in the previous query. + /// it's used to indicate the start of the next (this) page. + /// the value itself is not included in the response. + pub last_key: Option, + + /// limit is the total number of results to be returned in the result page. + /// If left empty it will default to a value to be set by each app. + pub limit: Option, +} + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct CredentialsRequestBody { + /// Explicit ids of the credentials to retrieve. Note: it can't be set alongside pagination. + pub credential_ids: Vec, + + /// Pagination settings for retrieving credentials. Note: it can't be set alongside explicit ids. + pub pagination: Option>, +} + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema, PartialEq)] +pub struct SerialNumberWrapper( + #[serde(with = "nym_serde_helpers::bs58")] + #[schemars(with = "String")] + Vec, +); + +impl Deref for SerialNumberWrapper { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef<[u8]> for SerialNumberWrapper { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl From> for SerialNumberWrapper { + fn from(value: Vec) -> Self { + SerialNumberWrapper(value) + } +} + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema, PartialEq)] +pub struct BatchRedeemTicketsBody { + #[serde(with = "nym_serde_helpers::bs58")] + #[schemars(with = "String")] + pub digest: Vec, + pub included_serial_numbers: Vec, + pub proposal_id: u64, + #[schemars(with = "String")] + pub gateway_cosmos_addr: AccountId, +} + +impl BatchRedeemTicketsBody { + pub fn make_digest(serial_numbers: I) -> Vec + where + I: Iterator, + T: AsRef<[u8]>, + { + let mut hasher = sha2::Sha256::new(); + for sn in serial_numbers { + hasher.update(sn) + } + hasher.finalize().to_vec() + } + + pub fn new( + digest: Vec, + proposal_id: u64, + serial_numbers: Vec>, + redeemer: AccountId, + ) -> Self { + BatchRedeemTicketsBody { + digest, + included_serial_numbers: serial_numbers.into_iter().map(Into::into).collect(), + proposal_id, + gateway_cosmos_addr: redeemer, + } + } + + pub fn verify_digest(&self) -> bool { + Self::make_digest(self.included_serial_numbers.iter()) == self.digest + } +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct EcashBatchTicketRedemptionResponse { + pub proposal_accepted: bool, +} + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct SpentCredentialsResponse { + #[serde(with = "nym_serde_helpers::base64")] + #[schemars(with = "String")] + pub bitmap: Vec, +} + +impl SpentCredentialsResponse { + pub fn new(bitmap: Vec) -> SpentCredentialsResponse { + SpentCredentialsResponse { bitmap } + } +} + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct EpochCredentialsResponse { + pub epoch_id: u64, + pub first_epoch_credential_id: Option, + pub total_issued: u32, +} + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct IssuedCredentialsResponse { + // note: BTreeMap returns ordered results so it's fine to use it with pagination + pub credentials: BTreeMap, +} + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct IssuedCredentialResponse { + pub credential: Option, +} + +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct IssuedTicketbookBody { + pub credential: IssuedTicketbook, + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub signature: identity::Signature, +} + +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct IssuedTicketbook { + pub id: i64, + pub epoch_id: u32, + pub deposit_id: u32, + + // NOTE: if we find creation of this guy takes too long, + // change `BlindedSignature` to `BlindedSignatureBytes` + // so that nym-api wouldn't need to parse the value out of its storage + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub blinded_partial_credential: BlindedSignature, + pub encoded_private_attributes_commitments: Vec>, + + #[schemars(with = "String")] + #[serde(with = "crate::helpers::date_serde")] + pub expiration_date: Date, + + #[schemars(with = "String")] + pub ticketbook_type: TicketType, +} + +impl IssuedTicketbook { + // this method doesn't have to be reversible so just naively concatenate everything + pub fn signable_plaintext(&self) -> Vec { + issued_credential_plaintext( + self.epoch_id, + self.deposit_id, + &self.blinded_partial_credential, + &self.encoded_private_attributes_commitments, + self.expiration_date, + self.ticketbook_type, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // had some issues with `Date` and serde... + // so might as well leave this unit test in case we do something to the helper + #[test] + fn aggregated_expiration_date_signature_responses_deserialises_correctly() { + let raw = r#"{"epoch_id":0,"expiration_date":"2024-08-03","signatures":[{"signature":{"h":"9379384af5236bffd7d6c533782d74863f892cf6e0fc8b4b64647e8bd5bccfe63df639a4a556d21826ab7ef54e4adafe","s":"83124c907935eba10c791cbc2cedf8714b68c6a266efead7bc4897c3b9652680bd151493689577ac790b9ccef1112f9b"},"expiration_timestamp":1722643200,"spending_timestamp":1720137600},{"signature":{"h":"817b9fa0eef617ce751963d9c853df56fdf38a643b9ef8649b658d7ec3da61d49d9279f22509c66dd07051132a418c62","s":"af9fd22c2fde4cb36a0ee021c5a756f97f871ea404d639af1be845b277bed583f715d228d3556e79d15eba23a5b0d5e0"},"expiration_timestamp":1722643200,"spending_timestamp":1720224000},{"signature":{"h":"89f04f5ae1a1532551c2e17166775eac3c5ebba0198eff2919073efd733d6e17e0a6496e31061b0797f51b69d0fa17fc","s":"b752cdb2f4d8a4254d24d5773a8490de1ad84207cbe693638d1fdcbffd8426be96fae3586f19c570beb005bb6bf27b9b"},"expiration_timestamp":1722643200,"spending_timestamp":1720310400},{"signature":{"h":"b1072da2612a6ebc0c02c7f673898dfea88eeeeb076a9dd2ab981d47a8176b87804ca6090cfb5b0c128f06adc6fdf0de","s":"8746a62f6333c3948f1fbb05543f0095cd44f610926c71f339511bf3bb97d98fd019d44c4a4dfc6710a9d5c718c5bfc2"},"expiration_timestamp":1722643200,"spending_timestamp":1720396800},{"signature":{"h":"8d266e7e56af7364d79c4190b24d5f0a601d7771c359815f642115123f5418d0304a46226d70e873b10a051f15178630","s":"a11d67c3293b224abb1256050031b32236484dc0f0fe66a9363d322e6b8c9b24719f4d4da0584d6106eb0f59fa6bb3c8"},"expiration_timestamp":1722643200,"spending_timestamp":1720483200},{"signature":{"h":"8ee31ef821949be316528b7b75b81baa6d0cf2846b615ff6d34e478f25746a07287ae6944279550879735740803c7922","s":"aa26b0df4445d8a10a226f1c70908451d211f787458c0743ba891c46bbbbde3bc59777f4edb9cabdd5fd181d772cfa43"},"expiration_timestamp":1722643200,"spending_timestamp":1720569600},{"signature":{"h":"b8fbacde3f9d507fb5bf6d5f96a33dd5c9c40cc9a062e92f9cdc6a2bddf8cbd873ffcc5112de816f98fbf47b74330abb","s":"a791aee82bdcf3cc4203ce657fbd2c3e9c2f0decd032c4ab91713d3cfd0ac57ebb9bb20e133d9a8a4ba1b8692a3b2706"},"expiration_timestamp":1722643200,"spending_timestamp":1720656000},{"signature":{"h":"b9ed48b47c1a1e6a1ae847ffaad01e0b7d22b2c9f9075beedf4924958ca044579f1e9ca18e268e2a0ef743c214934692","s":"a3e9191d0c3651c656a376e99f3d8c121a742ba55522765479ce866df8aaecc0281ae08003439debf7977f23e450a45f"},"expiration_timestamp":1722643200,"spending_timestamp":1720742400},{"signature":{"h":"a6f1fbdcc28953f318f067cde6e3d6ad0362fd46a501a74492be0884308cf306a7f30006181710f8883a380a8d4885f7","s":"9260d035ea97fc1b93d2db3802678005c95a4acdb0ce0c947cb0b9866ba4028a3b833d742f02ed0227ed33cee4ed17b4"},"expiration_timestamp":1722643200,"spending_timestamp":1720828800},{"signature":{"h":"95109fd35f7808084f224aa1fde645728c0c163751fe24d97e0002a7c38dc5a3a592dcb770893ddd25fc67f84e29e0a1","s":"b85279c4566aa365d208b57817ce96f1a1245895fbe25363202da89559fc454a2cfb09972fc3d1c468b96e8bc11b424c"},"expiration_timestamp":1722643200,"spending_timestamp":1720915200},{"signature":{"h":"8f4b7f93871d8f995a248d31664b02e1fe7107d67512f248a8f550d477846ca9f9862a5d44f5da458c6b83f4d8e62494","s":"b87e46a5d25de574eb4e16cf24199b2e0a658e0c1ef965b632165ed2d695637df76d373484dc93cec30e2f0a29ac91f8"},"expiration_timestamp":1722643200,"spending_timestamp":1721001600},{"signature":{"h":"b740491d3527f8fa0b8bed76d234d0737c09a049f653bb1e8a552b29f568aabd077604b08e7337d44f551fbda565e52e","s":"a08634a2560b3a79c8b56ff5387ef76c397c277810d78cc6dfac2161673df761d0ba61e6075e8a4781ec1b2ba7715b93"},"expiration_timestamp":1722643200,"spending_timestamp":1721088000},{"signature":{"h":"8d4b92fe48257d2574223fbe12afcdabd00db976e8d20faaddb5fb570373edb2a15af4ad9a7c10ceda3bb1304b0210aa","s":"8f8592c45a1cca753e5d0b04ffc2947fd27c7716f2751e632f2bc1bf0e73b179ce8080325951beb0886c5cf0fca79372"},"expiration_timestamp":1722643200,"spending_timestamp":1721174400},{"signature":{"h":"891d412e631041d07115cacf847575462d2389402a0e243a2f106b5e1c8f1431fa8e1aedc55cf943aef9fa8125d105a1","s":"ad2038ab95b823a8119bcc473be22101c4b58fd44ad375235c3664341fc617fe129702aec8765d8dc32ac845aec58a6a"},"expiration_timestamp":1722643200,"spending_timestamp":1721260800},{"signature":{"h":"84b97558e7889af167ba01ed608418ddabfc7fa71ba8f9cb045027f994fe9b5f9bb0ccc15822d1c73a37a48b041cd759","s":"adc370bc182ff9c7057591a3d4afcb21c714093e480374a07608c9f9d2baeff8c14fc5de3f0f457ea340e445ecc28487"},"expiration_timestamp":1722643200,"spending_timestamp":1721347200},{"signature":{"h":"b29b245acf66082c0f4e52cbf9db00ecb9cee070ddb1f5b56a9e85e2563dae04186ed03b7b590e0a9f4b4304ae1cd370","s":"8196445495ed1ee098bbe6083ece3522d4669e23dc4f3e2226e0e63294f1bc791e66380deedd21e5679e3019e93c7727"},"expiration_timestamp":1722643200,"spending_timestamp":1721433600},{"signature":{"h":"9750b9bdf1e86e8e4e6de44392f2160059da64838357a16e64155a5b7989fe7958ae95501e7c1f4039cf8b372ac3b214","s":"80707aa94af054fedcb4947809dab7a9317e303a12b7fb30858115c95bf8e46fec7ea9b0ce5097b75d8f7905c205f757"},"expiration_timestamp":1722643200,"spending_timestamp":1721520000},{"signature":{"h":"828be64b5cd147b5c43e68a2084cb77a762967d5621769136a0894c3d1ee50890412120d7c765cb3c835435d57ac4438","s":"956ca67f8f8591e72b8feeb53cbe52a0f69202313aa729c09760fb2a19b346fda1b7afd326822889fb228ca77a503072"},"expiration_timestamp":1722643200,"spending_timestamp":1721606400},{"signature":{"h":"83ac2c778f0b847af8a4bcc37bd5c2734544fe4427ee7ec5c4690738005cc805864fd5944266417ba2795d7333bd8c4a","s":"88936b7f61a0febfbedc8c0c40dad8f9c4eacb41d276ba00b7d03602ca69614faacca0d945713a272e9748dd6dd71f81"},"expiration_timestamp":1722643200,"spending_timestamp":1721692800},{"signature":{"h":"911c126da663478023efd1157586681937ac7f6c5a33b34a102b3611a63780b244ecdf05ecad09701e65ad20c5246f6a","s":"a1188f3b6ced776890b038e2d07b6af67f1a2c08f54a32d48dbfce61be3295e8ea910b6197b111a6ff16dd668eeda9f5"},"expiration_timestamp":1722643200,"spending_timestamp":1721779200},{"signature":{"h":"8c03c749b424e8fb3f26d51e45d21a7e9f59e6836f89f4a1f27b952e2bc8c038f3372f593dfb3bd284feb36918c2f194","s":"b20d3fd194f73142f0dfc68981519b76aa013e6743b2bdf63d41f3aa1e754d1f74c46012adf0332723f861e5aaa69dba"},"expiration_timestamp":1722643200,"spending_timestamp":1721865600},{"signature":{"h":"872de60a19f3ece61a70519b1f7a63476d13a20721ff00ae923a984bd710703af55112092e209970f074a8950b9e246e","s":"854906bbce5a0c2e47fa829441a03f5c3ac9357dc6d107547198f17505cd17852806e6d69b78264921a8c13c4a9fe50c"},"expiration_timestamp":1722643200,"spending_timestamp":1721952000},{"signature":{"h":"a2a57fd2b675fccccae34f9ff465f36638ee3809291c9b37b5830015a1a7c4d873528105f030a05efd16c6411672fe88","s":"b42f31e4ed05d3b026a62c5df97e04568c0c92d3fafaf95fa732ce69fbe7ee091ac025d04e3ce526b3474edaaf278398"},"expiration_timestamp":1722643200,"spending_timestamp":1722038400},{"signature":{"h":"b9be127e812602ac7a612897fd95e5869b360046993139590c82c2844654b6a5a72e9d74ed38eea6ee99ba0978382a3b","s":"b027db49bb9b0ec3931fc1cb4a6a739c968c45a59698d199056ebfcbeca7ddb363a123e55d05fbf5635d915be2a16ffb"},"expiration_timestamp":1722643200,"spending_timestamp":1722124800},{"signature":{"h":"8c40b2a5e17e1f9cdbe85eaaa419126374d35a255b8338474d442d0d40a4e10c3f2668f579155dfdad3fd403523683df","s":"a40cec4b00c9a975db6c307f25425f3a4d0e49eca2c05d5122630c54bd5d114f74e4abb78a7261752a9dfd4c020f8b91"},"expiration_timestamp":1722643200,"spending_timestamp":1722211200},{"signature":{"h":"b72821bc38167e7f422dcc669c19e0aeffb8247192c4bd50da1503b6aa28041bb5605598639bdb8b945dbc804fc852aa","s":"9443cad7c9d6194b601daf21cbc5fe3019d3350dd725f6d9e3ae366162f93e3b738779617f77ec146f0a05f9a40b602b"},"expiration_timestamp":1722643200,"spending_timestamp":1722297600},{"signature":{"h":"8fe36994ac7e92b5ad0869f2b1bfa3247aac27599459cce9d94cf84eeead8bba1ca4b294d16db3588a0eb9d9b28d4051","s":"8eae0ef0e966f9f473bd829f18e28cf9730447452040aaef114701f409ff237965f203f7394a7d61ca745a4b2f058d9b"},"expiration_timestamp":1722643200,"spending_timestamp":1722384000},{"signature":{"h":"a9f91a83a1db0c5609fcacd1290f1650e4e5ab98c3dd9f0b8fa1c54664c12ed17478ae6e598f0e900ace30addf1b0559","s":"801658058f8de9ba1c25518905196d7e4e0bc5e26a7a810c9dce3ac141f01c538c62808b4728ff683dee78670b2846f9"},"expiration_timestamp":1722643200,"spending_timestamp":1722470400},{"signature":{"h":"94ecd95c1d5ae03270079476aad6334dafdcf615157a6a34b05bb755a047bc2515e33a7e099f9fddc4eec3def8904cdf","s":"b59ff6ded76926a9ae97447ba00eac53d9aa0e1168218d990c5b97cc128a4f5a0c42f961f84cad8ad344630b2a15f3a9"},"expiration_timestamp":1722643200,"spending_timestamp":1722556800},{"signature":{"h":"ace778e70775d349c1e679e5ccef634a533738e7af8b51daa45ccbd920f976d15c4ba744d6ae8fdf711b84b20703cef6","s":"80581faceda2bc35b81240017e1ce8f121476c5ba4d75acddfc58ba6aaa2f12d626eaaab185548e6dd716b6e582740b8"},"expiration_timestamp":1722643200,"spending_timestamp":1722643200}]}"#; + let _: AggregatedExpirationDateSignatureResponse = serde_json::from_str(raw).unwrap(); + } + + #[test] + fn batch_redemption_body_roundtrip() { + let sn = vec![b"foomp".to_vec(), b"bar".to_vec()]; + let gateway: AccountId = "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf".parse().unwrap(); + let digest = [42u8; 32].to_vec(); + + let req = BatchRedeemTicketsBody::new(digest, 69, sn, gateway); + let bytes = serde_json::to_vec(&req).unwrap(); + + let de: BatchRedeemTicketsBody = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(req, de); + } +} diff --git a/nym-api/nym-api-requests/src/helpers.rs b/nym-api/nym-api-requests/src/helpers.rs new file mode 100644 index 0000000000..dc2a28c1f5 --- /dev/null +++ b/nym-api/nym-api-requests/src/helpers.rs @@ -0,0 +1,141 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use schemars::JsonSchema; +use time::format_description::{modifier, BorrowedFormatItem, Component}; +use time::OffsetDateTime; + +// just to have something, even if not accurate to generate the swagger docs +#[derive(JsonSchema)] +pub struct PlaceholderJsonSchemaImpl {} + +const DATE_FORMAT: &[BorrowedFormatItem<'_>] = &[ + BorrowedFormatItem::Component(Component::Year(modifier::Year::default())), + BorrowedFormatItem::Literal(b"-"), + BorrowedFormatItem::Component(Component::Month(modifier::Month::default())), + BorrowedFormatItem::Literal(b"-"), + BorrowedFormatItem::Component(Component::Day(modifier::Day::default())), +]; + +pub(crate) const fn unix_epoch() -> OffsetDateTime { + OffsetDateTime::UNIX_EPOCH +} + +pub(crate) mod overengineered_offset_date_time_serde { + use crate::helpers::{unix_epoch, DATE_FORMAT}; + use serde::de::Visitor; + use serde::ser::Error; + use serde::{Deserializer, Serialize, Serializer}; + use std::fmt::Formatter; + use time::format_description::well_known::Rfc3339; + use time::format_description::{modifier, BorrowedFormatItem, Component}; + use time::OffsetDateTime; + + struct OffsetDateTimeVisitor; + + // copied from time library because they keep it private -.- + const DEFAULT_OFFSET_DATE_TIME_FORMAT: &[BorrowedFormatItem<'_>] = &[ + BorrowedFormatItem::Compound(DATE_FORMAT), + BorrowedFormatItem::Literal(b" "), + BorrowedFormatItem::Compound(TIME_FORMAT), + BorrowedFormatItem::Literal(b" "), + BorrowedFormatItem::Compound(UTC_OFFSET_FORMAT), + ]; + + const TIME_FORMAT: &[BorrowedFormatItem<'_>] = &[ + BorrowedFormatItem::Component(Component::Hour(modifier::Hour::default())), + BorrowedFormatItem::Literal(b":"), + BorrowedFormatItem::Component(Component::Minute(modifier::Minute::default())), + BorrowedFormatItem::Literal(b":"), + BorrowedFormatItem::Component(Component::Second(modifier::Second::default())), + BorrowedFormatItem::Literal(b"."), + BorrowedFormatItem::Component(Component::Subsecond(modifier::Subsecond::default())), + ]; + + const UTC_OFFSET_FORMAT: &[BorrowedFormatItem<'_>] = &[ + BorrowedFormatItem::Component(Component::OffsetHour({ + let mut m = modifier::OffsetHour::default(); + m.sign_is_mandatory = true; + m + })), + BorrowedFormatItem::Optional(&BorrowedFormatItem::Compound(&[ + BorrowedFormatItem::Literal(b":"), + BorrowedFormatItem::Component(Component::OffsetMinute( + modifier::OffsetMinute::default(), + )), + BorrowedFormatItem::Optional(&BorrowedFormatItem::Compound(&[ + BorrowedFormatItem::Literal(b":"), + BorrowedFormatItem::Component(Component::OffsetSecond( + modifier::OffsetSecond::default(), + )), + ])), + ])), + ]; + + impl<'de> Visitor<'de> for OffsetDateTimeVisitor { + type Value = OffsetDateTime; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("an rfc3339 or human-readable `OffsetDateTime`") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + // first try rfc3339, if that fails use default human-readable impl from time, + // finally fallback to default unix epoch + Ok(OffsetDateTime::parse(v, &Rfc3339).unwrap_or_else(|_| { + OffsetDateTime::parse(v, &DEFAULT_OFFSET_DATE_TIME_FORMAT) + .unwrap_or_else(|_| unix_epoch()) + })) + } + } + + pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(OffsetDateTimeVisitor) + } + + pub(crate) fn serialize(datetime: &OffsetDateTime, serializer: S) -> Result + where + S: Serializer, + { + // serialize it with human-readable format for compatibility with eclipse and nutella clients + // in the future change it back to rfc3339 + datetime + .format(&DEFAULT_OFFSET_DATE_TIME_FORMAT) + .map_err(S::Error::custom)? + .serialize(serializer) + } +} + +pub(crate) mod date_serde { + use crate::helpers::DATE_FORMAT; + use serde::ser::Error; + use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; + use time::Date; + + pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + + Date::parse(&s, DATE_FORMAT).map_err(de::Error::custom) + } + + pub(crate) fn serialize(datetime: &Date, serializer: S) -> Result + where + S: Serializer, + { + // serialize it with human-readable format for compatibility with eclipse and nutella clients + // in the future change it back to rfc3339 + datetime + .format(&DATE_FORMAT) + .map_err(S::Error::custom)? + .serialize(serializer) + } +} diff --git a/nym-api/nym-api-requests/src/lib.rs b/nym-api/nym-api-requests/src/lib.rs index 5074e16146..7ea5db1f6d 100644 --- a/nym-api/nym-api-requests/src/lib.rs +++ b/nym-api/nym-api-requests/src/lib.rs @@ -4,7 +4,9 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -pub mod coconut; +pub mod constants; +pub mod ecash; +mod helpers; pub mod models; pub mod nym_nodes; pub mod pagination; diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index 05cf9f702e..8245d26d70 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -1,6 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::helpers::unix_epoch; use crate::nym_nodes::NodeRole; use crate::pagination::PaginatedResponse; use cosmwasm_std::{Addr, Coin, Decimal}; @@ -16,7 +17,7 @@ use schemars::gen::SchemaGenerator; use schemars::schema::{InstanceType, Schema, SchemaObject}; use schemars::JsonSchema; use serde::{Deserialize, Deserializer, Serialize}; -use std::fmt::{Display, Formatter}; +use std::fmt::{Debug, Display, Formatter}; use std::net::IpAddr; use std::ops::{Deref, DerefMut}; use std::{fmt, time::Duration}; @@ -41,7 +42,7 @@ impl RequestError { impl Display for RequestError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - self.message.fmt(f) + Display::fmt(&self.message, f) } } @@ -417,10 +418,6 @@ impl From for WebSocket } } -const fn unix_epoch() -> OffsetDateTime { - OffsetDateTime::UNIX_EPOCH -} - pub fn de_rfc3339_or_default<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, @@ -428,110 +425,14 @@ where Ok(time::serde::rfc3339::deserialize(deserializer).unwrap_or_else(|_| unix_epoch())) } -pub(crate) mod overengineered_offset_date_time_serde { - use crate::models::unix_epoch; - use serde::de::Visitor; - use serde::ser::Error; - use serde::{Deserializer, Serialize, Serializer}; - use std::fmt::Formatter; - use time::format_description::well_known::Rfc3339; - use time::format_description::{modifier, BorrowedFormatItem, Component}; - use time::OffsetDateTime; - - struct OffsetDateTimeVisitor; - - // copied from time library because they keep it private -.- - const DEFAULT_OFFSET_DATE_TIME_FORMAT: &[BorrowedFormatItem<'_>] = &[ - BorrowedFormatItem::Compound(DATE_FORMAT), - BorrowedFormatItem::Literal(b" "), - BorrowedFormatItem::Compound(TIME_FORMAT), - BorrowedFormatItem::Literal(b" "), - BorrowedFormatItem::Compound(UTC_OFFSET_FORMAT), - ]; - - const DATE_FORMAT: &[BorrowedFormatItem<'_>] = &[ - BorrowedFormatItem::Component(Component::Year(modifier::Year::default())), - BorrowedFormatItem::Literal(b"-"), - BorrowedFormatItem::Component(Component::Month(modifier::Month::default())), - BorrowedFormatItem::Literal(b"-"), - BorrowedFormatItem::Component(Component::Day(modifier::Day::default())), - ]; - - const TIME_FORMAT: &[BorrowedFormatItem<'_>] = &[ - BorrowedFormatItem::Component(Component::Hour(modifier::Hour::default())), - BorrowedFormatItem::Literal(b":"), - BorrowedFormatItem::Component(Component::Minute(modifier::Minute::default())), - BorrowedFormatItem::Literal(b":"), - BorrowedFormatItem::Component(Component::Second(modifier::Second::default())), - BorrowedFormatItem::Literal(b"."), - BorrowedFormatItem::Component(Component::Subsecond(modifier::Subsecond::default())), - ]; - - const UTC_OFFSET_FORMAT: &[BorrowedFormatItem<'_>] = &[ - BorrowedFormatItem::Component(Component::OffsetHour({ - let mut m = modifier::OffsetHour::default(); - m.sign_is_mandatory = true; - m - })), - BorrowedFormatItem::Optional(&BorrowedFormatItem::Compound(&[ - BorrowedFormatItem::Literal(b":"), - BorrowedFormatItem::Component(Component::OffsetMinute( - modifier::OffsetMinute::default(), - )), - BorrowedFormatItem::Optional(&BorrowedFormatItem::Compound(&[ - BorrowedFormatItem::Literal(b":"), - BorrowedFormatItem::Component(Component::OffsetSecond( - modifier::OffsetSecond::default(), - )), - ])), - ])), - ]; - - impl<'de> Visitor<'de> for OffsetDateTimeVisitor { - type Value = OffsetDateTime; - - fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { - formatter.write_str("an rfc3339 or human-readable `OffsetDateTime`") - } - - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - // first try rfc3339, if that fails use default human-readable impl from time, - // finally fallback to default unix epoch - Ok(OffsetDateTime::parse(v, &Rfc3339).unwrap_or_else(|_| { - OffsetDateTime::parse(v, &DEFAULT_OFFSET_DATE_TIME_FORMAT) - .unwrap_or_else(|_| unix_epoch()) - })) - } - } - - pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_str(OffsetDateTimeVisitor) - } - - pub(crate) fn serialize(datetime: &OffsetDateTime, serializer: S) -> Result - where - S: Serializer, - { - // serialize it with human-readable format for compatibility with eclipse and nutella clients - // in the future change it back to rfc3339 - datetime - .format(&DEFAULT_OFFSET_DATE_TIME_FORMAT) - .map_err(S::Error::custom)? - .serialize(serializer) - } -} - // for all intents and purposes it's just OffsetDateTime, but we need JsonSchema... #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] pub struct OffsetDateTimeJsonSchemaWrapper( - #[serde(default = "unix_epoch", with = "overengineered_offset_date_time_serde")] - pub OffsetDateTime, + #[serde( + default = "unix_epoch", + with = "crate::helpers::overengineered_offset_date_time_serde" + )] + pub OffsetDateTime, ); impl Default for OffsetDateTimeJsonSchemaWrapper { @@ -540,6 +441,12 @@ impl Default for OffsetDateTimeJsonSchemaWrapper { } } +impl Display for OffsetDateTimeJsonSchemaWrapper { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(&self.0, f) + } +} + impl From for OffsetDateTime { fn from(value: OffsetDateTimeJsonSchemaWrapper) -> Self { value.0 diff --git a/nym-api/nym-api-requests/src/nym_nodes.rs b/nym-api/nym-api-requests/src/nym_nodes.rs index ea874b90e7..50b527070a 100644 --- a/nym-api/nym-api-requests/src/nym_nodes.rs +++ b/nym-api/nym-api-requests/src/nym_nodes.rs @@ -17,7 +17,7 @@ pub struct CachedNodesResponse { #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] #[serde(rename_all = "kebab-case")] -#[cfg_attr(feature = "request-parsing", derive(rocket::form::FromFormField))] +#[cfg_attr(feature = "rocket-traits", derive(rocket::form::FromFormField))] pub enum NodeRoleQueryParam { ActiveMixnode, diff --git a/nym-api/src/coconut/api_routes/mod.rs b/nym-api/src/coconut/api_routes/mod.rs deleted file mode 100644 index d56202f067..0000000000 --- a/nym-api/src/coconut/api_routes/mod.rs +++ /dev/null @@ -1,393 +0,0 @@ -// Copyright 2023-2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use std::ops::Deref; - -use k256::ecdsa::signature::Verifier; -use rand::rngs::OsRng; -use rand::RngCore; -use rocket::serde::json::Json; -use rocket::State as RocketState; -use time::OffsetDateTime; - -use nym_api_requests::coconut::models::{ - CredentialsRequestBody, EpochCredentialsResponse, FreePassNonceResponse, FreePassRequest, - IssuedCredentialResponse, IssuedCredentialsResponse, -}; -use nym_api_requests::coconut::{ - BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse, -}; -use nym_coconut_bandwidth_contract_common::spend_credential::{ - funds_from_cosmos_msgs, SpendCredentialStatus, -}; -use nym_coconut_dkg_common::types::EpochId; -use nym_credentials::coconut::bandwidth::freepass::MAX_FREE_PASS_VALIDITY; -use nym_credentials::coconut::bandwidth::{ - bandwidth_credential_params, CredentialType, IssuanceBandwidthCredential, -}; -use nym_validator_client::nyxd::Coin; - -use crate::coconut::api_routes::helpers::build_credentials_response; -use crate::coconut::error::{CoconutError, Result}; -use crate::coconut::helpers::{accepted_vote_err, blind_sign}; -use crate::coconut::state::State; -use crate::coconut::storage::CoconutStorageExt; - -mod helpers; - -fn validate_freepass_public_attributes(res: &FreePassRequest) -> Result<()> { - let public_attributes = &res.public_attributes_plain; - - if public_attributes.len() != IssuanceBandwidthCredential::PUBLIC_ATTRIBUTES as usize { - return Err(CoconutError::InvalidFreePassAttributes { - got: public_attributes.len(), - expected: IssuanceBandwidthCredential::PUBLIC_ATTRIBUTES as usize, - }); - } - - // SAFETY: we just ensured correct number of attributes - let expiry_raw = public_attributes.first().unwrap(); - let type_raw = public_attributes.get(1).unwrap(); - - let parsed_type = type_raw.parse::()?; - if parsed_type != CredentialType::FreePass { - return Err(CoconutError::InvalidFreePassTypeAttribute { got: parsed_type }); - } - - let expiry_timestamp: i64 = expiry_raw - .parse() - .map_err(|source| CoconutError::ExpiryDateParsingFailure { source })?; - - let expiry_date = OffsetDateTime::from_unix_timestamp(expiry_timestamp).map_err(|source| { - CoconutError::InvalidExpiryDate { - unix_timestamp: expiry_timestamp, - source, - } - })?; - let now = OffsetDateTime::now_utc(); - - if expiry_date > now + MAX_FREE_PASS_VALIDITY { - return Err(CoconutError::TooLongFreePass { expiry_date }); - } - - if expiry_date < now { - return Err(CoconutError::FreePassExpiryInThePast { expiry_date }); - } - - Ok(()) -} - -#[get("/free-pass-nonce")] -pub async fn get_current_free_pass_nonce( - state: &RocketState, -) -> Result> { - debug!("Received free pass nonce request"); - - let current_nonce = state.freepass_nonce.read().await; - debug!("the current expected nonce is {current_nonce:?}"); - - Ok(Json(FreePassNonceResponse { - current_nonce: *current_nonce, - })) -} - -#[post("/free-pass", data = "")] -pub async fn post_free_pass( - freepass_request_body: Json, - state: &RocketState, -) -> Result> { - debug!("Received free pass sign request"); - trace!("body: {:?}", freepass_request_body); - - validate_freepass_public_attributes(&freepass_request_body)?; - - // check for explicit admin - let explicit_admin = state.get_authorised_freepass_requester().await; - - // otherwise fallback to bandwidth contract admin - let bandwidth_contract_admin = state - .get_bandwidth_contract_admin() - .await - .cloned() - .inspect_err(|_| error!("our bandwidth contract does not have an admin set! We won't be able to migrate the contract! We should redeploy it ASAP")) - .ok() - .flatten(); - - // extract account prefix - let prefix = match (&explicit_admin, &bandwidth_contract_admin) { - (None, None) => { - error!("neither explicit admin nor bandwidth contract admin has been set!"); - return Err(CoconutError::MissingBandwidthContractAddress); - } - (Some(addr), _) => addr.prefix(), - (None, Some(addr)) => addr.prefix(), - }; - - // derive the address out of the provided pubkey - let requester = match freepass_request_body.cosmos_pubkey.account_id(prefix) { - Ok(address) => address, - Err(err) => { - return Err(CoconutError::AdminAccountDerivationFailure { - formatted_source: err.to_string(), - }) - } - }; - debug!("derived the following address out of the provided public key: {requester}. Going to check it against the authorised admin ({explicit_admin:?}) and fallback to bandwidth contract admin: {bandwidth_contract_admin:?}"); - - // check if request matches any address - if Some(&requester) != explicit_admin.as_ref() - && Some(&requester) != bandwidth_contract_admin.as_ref() - { - return Err(CoconutError::UnauthorisedFreePassAccount { - requester, - explicit_admin, - bandwidth_contract_admin, - }); - } - - // get the write lock on the nonce to block other requests (since we don't need concurrency and nym is the only one getting them) - let mut current_nonce = state.freepass_nonce.write().await; - debug!("the current expected nonce is {current_nonce:?}"); - - if *current_nonce != freepass_request_body.used_nonce { - return Err(CoconutError::InvalidNonce { - current: *current_nonce, - received: freepass_request_body.used_nonce, - }); - } - - // check if we have the signing key available - debug!("checking if we actually have coconut keys derived..."); - let maybe_keypair_guard = state.coconut_keypair.get().await; - let Some(keypair_guard) = maybe_keypair_guard.as_ref() else { - return Err(CoconutError::KeyPairNotDerivedYet); - }; - let Some(signing_key) = keypair_guard.as_ref() else { - return Err(CoconutError::KeyPairNotDerivedYet); - }; - - let tm_pubkey = freepass_request_body.tendermint_pubkey(); - - // currently accounts (excluding validators) don't use ed25519 and are secp256k1-based - let Some(secp256k1_pubkey) = tm_pubkey.secp256k1() else { - return Err(CoconutError::UnsupportedNonSecp256k1Key); - }; - - // make sure the signature actually verifies - secp256k1_pubkey - .verify( - &freepass_request_body.used_nonce, - &freepass_request_body.nonce_signature, - ) - .map_err(|_| CoconutError::FreePassSignatureVerificationFailure)?; - - // produce the partial signature - debug!("producing the partial credential"); - let blinded_signature = - blind_sign(freepass_request_body.deref(), signing_key.keys.secret_key())?; - - // update the number of issued free passes - state.storage.increment_issued_freepasses().await?; - - // update the nonce - OsRng.fill_bytes(current_nonce.as_mut_slice()); - - // finally return the credential to the client - Ok(Json(BlindedSignatureResponse { blinded_signature })) -} - -#[post("/blind-sign", data = "")] -// Until we have serialization and deserialization traits we'll be using a crutch -pub async fn post_blind_sign( - blind_sign_request_body: Json, - state: &RocketState, -) -> Result> { - debug!("Received blind sign request"); - trace!("body: {:?}", blind_sign_request_body); - - // early check: does the request have the expected number of public attributes? - debug!("performing basic request validation"); - if blind_sign_request_body.public_attributes_plain.len() - != IssuanceBandwidthCredential::PUBLIC_ATTRIBUTES as usize - { - return Err(CoconutError::InconsistentPublicAttributes); - } - - // check if we already issued a credential for this tx hash - debug!( - "checking if we have already issued credential for this tx_hash (hash: {})", - blind_sign_request_body.tx_hash - ); - if let Some(blinded_signature) = state - .already_issued(blind_sign_request_body.tx_hash) - .await? - { - return Ok(Json(BlindedSignatureResponse { blinded_signature })); - } - - // check if we have the signing key available - debug!("checking if we actually have coconut keys derived..."); - let maybe_keypair_guard = state.coconut_keypair.get().await; - let Some(keypair_guard) = maybe_keypair_guard.as_ref() else { - return Err(CoconutError::KeyPairNotDerivedYet); - }; - let Some(signing_key) = keypair_guard.as_ref() else { - return Err(CoconutError::KeyPairNotDerivedYet); - }; - - // get the transaction details of the claimed deposit - debug!("getting transaction details from the chain"); - let tx = state - .get_transaction(blind_sign_request_body.tx_hash) - .await?; - - // check validity of the request - debug!("fully validating received request"); - state.validate_request(&blind_sign_request_body, tx).await?; - - // produce the partial signature - debug!("producing the partial credential"); - let blinded_signature = blind_sign( - blind_sign_request_body.deref(), - signing_key.keys.secret_key(), - )?; - - // store the information locally - debug!("storing the issued credential in the database"); - state - .store_issued_credential(blind_sign_request_body.into_inner(), &blinded_signature) - .await?; - - // finally return the credential to the client - Ok(Json(BlindedSignatureResponse { blinded_signature })) -} - -#[post("/verify-bandwidth-credential", data = "")] -pub async fn verify_bandwidth_credential( - verify_credential_body: Json, - state: &RocketState, -) -> Result> { - let proposal_id = verify_credential_body.proposal_id; - let credential_data = &verify_credential_body.credential_data; - let epoch_id = credential_data.epoch_id; - let theta = &credential_data.verify_credential_request; - - let voucher_value: u64 = if credential_data.typ.is_voucher() { - credential_data - .get_bandwidth_attribute() - .ok_or(CoconutError::MissingBandwidthValue)? - .parse() - .map_err(|source| CoconutError::VoucherValueParsingFailure { source })? - } else { - return Err(CoconutError::NotABandwidthVoucher { - typ: credential_data.typ, - }); - }; - - // TODO: introduce a check to make sure we haven't already voted for this proposal to prevent DDOS - - let proposal = state.client.get_proposal(proposal_id).await?; - - // Proposal description is the blinded serial number - if !theta.has_blinded_serial_number(&proposal.description)? { - return Err(CoconutError::IncorrectProposal { - reason: String::from("incorrect blinded serial number in description"), - }); - } - let proposed_release_funds = - funds_from_cosmos_msgs(proposal.msgs).ok_or(CoconutError::IncorrectProposal { - reason: String::from("action is not to release funds"), - })?; - // Credential has not been spent before, and is on its way of being spent - let credential_status = state - .client - .get_spent_credential(theta.blinded_serial_number_bs58()) - .await? - .spend_credential - .ok_or(CoconutError::InvalidCredentialStatus { - status: String::from("Inexistent"), - })? - .status(); - if credential_status != SpendCredentialStatus::InProgress { - return Err(CoconutError::InvalidCredentialStatus { - status: format!("{:?}", credential_status), - }); - } - let verification_key = state.verification_key(epoch_id).await?; - let params = bandwidth_credential_params(); - let mut vote_yes = credential_data.verify(params, &verification_key); - - vote_yes &= Coin::from(proposed_release_funds) - == Coin::new(voucher_value as u128, state.mix_denom.clone()); - - // Vote yes or no on the proposal based on the verification result - let ret = state - .client - .vote_proposal(proposal_id, vote_yes, None) - .await; - accepted_vote_err(ret)?; - - Ok(Json(VerifyCredentialResponse::new(vote_yes))) -} - -#[get("/epoch-credentials/")] -pub async fn epoch_credentials( - epoch: EpochId, - state: &RocketState, -) -> Result> { - let issued = state.storage.get_epoch_credentials(epoch).await?; - - let response = if let Some(issued) = issued { - issued.into() - } else { - EpochCredentialsResponse { - epoch_id: epoch, - first_epoch_credential_id: None, - total_issued: 0, - } - }; - - Ok(Json(response)) -} - -#[get("/issued-credential/")] -pub async fn issued_credential( - id: i64, - state: &RocketState, -) -> Result> { - let issued = state.storage.get_issued_credential(id).await?; - - let credential = if let Some(issued) = issued { - Some(issued.try_into()?) - } else { - None - }; - - Ok(Json(IssuedCredentialResponse { credential })) -} - -#[post("/issued-credentials", data = "")] -pub async fn issued_credentials( - params: Json, - state: &RocketState, -) -> Result> { - let params = params.into_inner(); - - if params.pagination.is_some() && !params.credential_ids.is_empty() { - return Err(CoconutError::InvalidQueryArguments); - } - - let credentials = if let Some(pagination) = params.pagination { - state - .storage - .get_issued_credentials_paged(pagination) - .await? - } else { - state - .storage - .get_issued_credentials(params.credential_ids) - .await? - }; - - build_credentials_response(credentials).map(Json) -} diff --git a/nym-api/src/coconut/comm.rs b/nym-api/src/coconut/comm.rs deleted file mode 100644 index 48e7823fce..0000000000 --- a/nym-api/src/coconut/comm.rs +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright 2022 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::coconut::error::Result; -use crate::nyxd; -use crate::support::nyxd::ClientInner; -use nym_coconut::VerificationKey; -use nym_coconut_dkg_common::types::{Epoch, EpochId}; -use nym_credentials::coconut::utils::obtain_aggregate_verification_key; -use nym_validator_client::coconut::all_coconut_api_clients; -use nym_validator_client::nyxd::contract_traits::DkgQueryClient; -use std::cmp::min; -use std::collections::HashMap; -use std::ops::Deref; -use time::OffsetDateTime; -use tokio::sync::RwLock; - -#[async_trait] -pub trait APICommunicationChannel { - async fn current_epoch(&self) -> Result; - async fn aggregated_verification_key(&self, epoch_id: EpochId) -> Result; -} - -struct CachedEpoch { - valid_until: OffsetDateTime, - current_epoch_id: EpochId, -} - -impl Default for CachedEpoch { - fn default() -> Self { - CachedEpoch { - valid_until: OffsetDateTime::UNIX_EPOCH, - current_epoch_id: 0, - } - } -} - -impl CachedEpoch { - fn is_valid(&self) -> bool { - self.valid_until > OffsetDateTime::now_utc() - } - - async fn update(&mut self, epoch: Epoch) -> Result<()> { - let now = OffsetDateTime::now_utc(); - - let validity_duration = if let Some(epoch_finish) = epoch.deadline { - let state_end = - OffsetDateTime::from_unix_timestamp(epoch_finish.seconds() as i64).unwrap(); - let until_epoch_state_end = state_end - now; - // make it valid until the next epoch transition or next 5min, whichever is smaller - min(until_epoch_state_end, 5 * time::Duration::MINUTE) - } else { - 5 * time::Duration::MINUTE - }; - - self.valid_until = now + validity_duration; - self.current_epoch_id = epoch.epoch_id; - - Ok(()) - } -} - -pub(crate) struct QueryCommunicationChannel { - nyxd_client: nyxd::Client, - - epoch_keys: RwLock>, - cached_epoch: RwLock, -} - -impl QueryCommunicationChannel { - pub fn new(nyxd_client: nyxd::Client) -> Self { - QueryCommunicationChannel { - nyxd_client, - epoch_keys: Default::default(), - cached_epoch: Default::default(), - } - } -} - -#[async_trait] -impl APICommunicationChannel for QueryCommunicationChannel { - async fn current_epoch(&self) -> Result { - let guard = self.cached_epoch.read().await; - if guard.is_valid() { - return Ok(guard.current_epoch_id); - } - - // update cache - drop(guard); - let mut guard = self.cached_epoch.write().await; - - let epoch = match self.nyxd_client.read().await.deref() { - ClientInner::Query(client) => client.get_current_epoch().await?, - ClientInner::Signing(client) => client.get_current_epoch().await?, - }; - - guard.update(epoch).await?; - - return Ok(guard.current_epoch_id); - } - - async fn aggregated_verification_key(&self, epoch_id: EpochId) -> Result { - if let Some(vk) = self.epoch_keys.read().await.get(&epoch_id) { - return Ok(vk.clone()); - } - - let mut guard = self.epoch_keys.write().await; - let coconut_api_clients = match self.nyxd_client.read().await.deref() { - ClientInner::Query(client) => all_coconut_api_clients(client, epoch_id).await?, - ClientInner::Signing(client) => all_coconut_api_clients(client, epoch_id).await?, - }; - - let vk = obtain_aggregate_verification_key(&coconut_api_clients)?; - - guard.insert(epoch_id, vk.clone()); - - Ok(vk) - } -} diff --git a/nym-api/src/coconut/deposit.rs b/nym-api/src/coconut/deposit.rs deleted file mode 100644 index 985e9fadaa..0000000000 --- a/nym-api/src/coconut/deposit.rs +++ /dev/null @@ -1,340 +0,0 @@ -// Copyright 2022 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::coconut::error::{CoconutError, Result}; -use nym_api_requests::coconut::BlindSignRequestBody; -use nym_coconut_bandwidth_contract_common::events::{ - COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ENCRYPTION_KEY, DEPOSIT_IDENTITY_KEY, - DEPOSIT_INFO, DEPOSIT_VALUE, -}; -use nym_credentials::coconut::bandwidth::voucher::BandwidthVoucherIssuanceData; -use nym_credentials::coconut::bandwidth::IssuanceBandwidthCredential; -use nym_crypto::asymmetric::identity; -use nym_validator_client::nyxd::helpers::find_tx_attribute; -use nym_validator_client::nyxd::TxResponse; - -pub async fn validate_deposit_tx(request: &BlindSignRequestBody, tx: TxResponse) -> Result<()> { - if request.public_attributes_plain.len() - != IssuanceBandwidthCredential::PUBLIC_ATTRIBUTES as usize - { - return Err(CoconutError::InconsistentPublicAttributes); - } - - // extract actual public attributes + associated x25519 public key - let deposit_value = find_tx_attribute(&tx, COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_VALUE) - .ok_or(CoconutError::DepositValueNotFound)?; - - let deposit_info = find_tx_attribute(&tx, COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_INFO) - .ok_or(CoconutError::DepositInfoNotFound)?; - - let x25519_raw = find_tx_attribute( - &tx, - COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, - DEPOSIT_IDENTITY_KEY, - ) - .ok_or(CoconutError::DepositVerifKeyNotFound)?; - - // we're not using it anymore, but for legacy reasons it must be present - let _ed25519_raw = find_tx_attribute( - &tx, - COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, - DEPOSIT_ENCRYPTION_KEY, - ) - .ok_or(CoconutError::DepositEncrKeyNotFound)?; - - // check public attributes against request data - // (thinking about it attaching that data might be redundant since we have the source of truth on the chain) - // safety: we won't read data out of bounds since we just checked we have BandwidthVoucher::PUBLIC_ATTRIBUTES values in the vec - if deposit_value != request.public_attributes_plain[0] { - return Err(CoconutError::InconsistentDepositValue { - request: request.public_attributes_plain[0].clone(), - on_chain: deposit_value, - }); - } - - if deposit_info != request.public_attributes_plain[1] { - return Err(CoconutError::InconsistentDepositInfo { - request: request.public_attributes_plain[1].clone(), - on_chain: deposit_info, - }); - } - - // verify signature - let x25519 = identity::PublicKey::from_base58_string(x25519_raw)?; - let plaintext = BandwidthVoucherIssuanceData::request_plaintext( - &request.inner_sign_request, - request.tx_hash, - ); - x25519.verify(plaintext, &request.signature)?; - - Ok(()) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::coconut::tests::{tx_entry_fixture, voucher_fixture}; - use cosmwasm_std::coin; - use nym_coconut::BlindSignRequest; - use nym_coconut_bandwidth_contract_common::events::DEPOSITED_FUNDS_EVENT_TYPE; - use nym_credentials::coconut::bandwidth::CredentialType; - use nym_validator_client::nyxd::{Event, EventAttribute}; - - #[tokio::test] - async fn validate_deposit_tx_test() { - let voucher = voucher_fixture(coin(1234, "unym"), None); - let signing_data = voucher.prepare_for_signing(); - let voucher_data = voucher.get_variant_data().voucher_data().unwrap(); - let correct_request = voucher_data.create_blind_sign_request_body(&signing_data); - - let mut tx_entry = tx_entry_fixture(correct_request.tx_hash); - let good_deposit_attribute = EventAttribute { - key: DEPOSIT_VALUE.to_string(), - value: correct_request.public_attributes_plain[0].clone(), - index: false, - }; - let good_info_attribute = EventAttribute { - key: DEPOSIT_INFO.to_string(), - value: correct_request.public_attributes_plain[1].clone(), - index: false, - }; - let good_identity_key_attribute = EventAttribute { - key: DEPOSIT_IDENTITY_KEY.to_string(), - value: "2eSxwquNJb2nZTEW5p4rbqjHfBaz9UaNhjHHiexPN4He".to_string(), - index: false, - }; - let good_encryption_key_attribute = EventAttribute { - key: DEPOSIT_ENCRYPTION_KEY.to_string(), - value: "2eSxwquNJb2nZTEW5p4rbqjHfBaz9UaNhjHHiexPN4He".to_string(), - index: false, - }; - - tx_entry.tx_result.events.push(Event { - kind: format!("wasm-{}", DEPOSITED_FUNDS_EVENT_TYPE), - attributes: vec![], - }); - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - assert_eq!( - err.to_string(), - CoconutError::DepositValueNotFound.to_string(), - ); - - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - EventAttribute { - key: DEPOSIT_VALUE.parse().unwrap(), - value: "10".parse().unwrap(), - index: false, - }, - good_info_attribute.clone(), - good_identity_key_attribute.clone(), - good_encryption_key_attribute.clone(), - ]; - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - assert_eq!( - err.to_string(), - CoconutError::InconsistentDepositValue { - request: "1234".to_string(), - on_chain: "10".to_string() - } - .to_string() - ); - - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - good_deposit_attribute.clone(), - good_identity_key_attribute.clone(), - good_encryption_key_attribute.clone(), - ]; - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - assert_eq!( - err.to_string(), - CoconutError::DepositInfoNotFound.to_string(), - ); - - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - good_deposit_attribute.clone(), - EventAttribute { - key: DEPOSIT_INFO.parse().unwrap(), - value: "bandwidth deposit info".parse().unwrap(), - index: false, - }, - good_identity_key_attribute.clone(), - good_encryption_key_attribute.clone(), - ]; - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - assert_eq!( - err.to_string(), - CoconutError::InconsistentDepositInfo { - on_chain: "bandwidth deposit info".to_string(), - request: CredentialType::Voucher.to_string(), - } - .to_string(), - ); - - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - good_deposit_attribute.clone(), - good_info_attribute.clone(), - good_encryption_key_attribute.clone(), - ]; - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - assert_eq!( - err.to_string(), - CoconutError::DepositVerifKeyNotFound.to_string(), - ); - - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - good_deposit_attribute.clone(), - good_info_attribute.clone(), - EventAttribute { - key: DEPOSIT_IDENTITY_KEY.parse().unwrap(), - value: "verification key".parse().unwrap(), - index: false, - }, - good_encryption_key_attribute.clone(), - ]; - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - - assert!(matches!( - err, - CoconutError::Ed25519ParseError( - nym_crypto::asymmetric::identity::Ed25519RecoveryError::MalformedPublicKeyString { .. } - ) - )); - - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - good_deposit_attribute.clone(), - good_info_attribute.clone(), - EventAttribute { - key: DEPOSIT_IDENTITY_KEY.parse().unwrap(), - value: "2eSxwquNJb2nZTEW5p4rbqjHfBaz9UaNhjHHiexPN4He" - .parse() - .unwrap(), - index: false, - }, - ]; - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - assert_eq!( - err.to_string(), - CoconutError::DepositEncrKeyNotFound.to_string(), - ); - - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - good_deposit_attribute.clone(), - good_info_attribute.clone(), - EventAttribute { - key: DEPOSIT_IDENTITY_KEY.parse().unwrap(), - value: "6EJGMdEq7t8Npz54uPkftGsdmj7DKntLVputAnDfVZB2" - .parse() - .unwrap(), - index: false, - }, - EventAttribute { - key: DEPOSIT_ENCRYPTION_KEY.parse().unwrap(), - value: "encryption key".parse().unwrap(), - index: false, - }, - ]; - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - - assert!(matches!(err, CoconutError::SignatureVerificationError(..))); - - let expected_encryption_key = "HxnTpWTkgigSTAysVKLE8pEiUULHdTT1BxFfzfJvQRi6"; - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - good_deposit_attribute.clone(), - good_info_attribute.clone(), - EventAttribute { - key: DEPOSIT_IDENTITY_KEY.parse().unwrap(), - value: "6EJGMdEq7t8Npz54uPkftGsdmj7DKntLVputAnDfVZB2" - .parse() - .unwrap(), - index: false, - }, - EventAttribute { - key: DEPOSIT_ENCRYPTION_KEY.parse().unwrap(), - value: expected_encryption_key.parse().unwrap(), - index: false, - }, - ]; - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - assert_eq!( - err.to_string(), - CoconutError::SignatureVerificationError( - nym_crypto::asymmetric::identity::SignatureError::default(), - ) - .to_string(), - ); - - // hard-coded values, that generate a correct signature - let blind_sign_req = BlindSignRequest::from_bytes(&[ - 176, 113, 19, 237, 218, 252, 113, 20, 225, 238, 59, 88, 217, 45, 233, 178, 65, 28, 242, - 0, 222, 48, 110, 216, 26, 111, 51, 235, 61, 74, 200, 15, 130, 245, 45, 170, 155, 190, - 156, 77, 180, 142, 29, 63, 15, 224, 150, 31, 139, 24, 65, 175, 143, 153, 11, 203, 33, - 16, 152, 22, 221, 203, 99, 233, 208, 142, 161, 194, 46, 227, 177, 96, 119, 30, 175, 69, - 104, 14, 2, 191, 26, 94, 30, 165, 15, 28, 40, 176, 1, 78, 253, 79, 20, 137, 102, 74, 2, - 0, 0, 0, 0, 0, 0, 0, 131, 133, 112, 115, 53, 98, 58, 166, 240, 70, 185, 210, 203, 12, - 114, 66, 180, 38, 139, 12, 187, 45, 250, 201, 68, 102, 159, 172, 218, 124, 151, 23, - 172, 18, 216, 122, 246, 7, 185, 76, 20, 167, 123, 122, 152, 241, 175, 226, 176, 8, 170, - 70, 140, 252, 36, 130, 67, 204, 111, 116, 107, 92, 200, 77, 252, 31, 138, 18, 10, 215, - 165, 243, 95, 199, 193, 61, 200, 187, 22, 198, 109, 213, 145, 71, 171, 132, 174, 68, - 105, 248, 0, 115, 50, 55, 199, 84, 67, 16, 125, 216, 250, 154, 115, 174, 9, 206, 44, - 88, 63, 163, 124, 10, 239, 64, 158, 191, 27, 169, 177, 194, 223, 142, 202, 206, 189, - 122, 123, 91, 171, 15, 40, 192, 148, 75, 174, 24, 116, 229, 127, 170, 110, 183, 151, 2, - 118, 168, 22, 113, 87, 237, 91, 228, 249, 120, 114, 255, 53, 175, 245, 39, 2, 0, 0, 0, - 0, 0, 0, 0, 225, 45, 230, 25, 62, 202, 96, 166, 171, 241, 206, 137, 254, 51, 154, 255, - 122, 130, 107, 54, 5, 206, 207, 120, 193, 214, 64, 10, 111, 195, 86, 55, 201, 36, 10, - 18, 154, 158, 183, 87, 185, 59, 228, 89, 134, 193, 217, 188, 64, 164, 249, 21, 248, 20, - 207, 58, 31, 10, 19, 176, 246, 150, 45, 48, 2, 0, 0, 0, 0, 0, 0, 0, 173, 60, 65, 209, - 100, 114, 138, 186, 158, 150, 109, 230, 111, 86, 101, 72, 194, 237, 173, 195, 139, 175, - 238, 25, 169, 18, 188, 75, 77, 54, 111, 20, 115, 235, 195, 2, 123, 133, 164, 81, 15, - 45, 11, 84, 139, 38, 8, 224, 197, 181, 95, 147, 49, 77, 193, 207, 52, 141, 195, 195, - 66, 137, 17, 32, - ]) - .unwrap(); - - let correct_request = BlindSignRequestBody::new( - blind_sign_req, - "7C41AF8266D91DE55E1C8F4712E6A952A165ED3D8C27C7B00428CBD0DE00A52B" - .parse() - .unwrap(), - "3vUCc6MCN5AC2LNgDYjRB1QeErZSN1S8f6K14JHjpUcKWXbjGYFExA8DbwQQBki9gyUqrpBF94Drttb4eMcGQXkp".parse().unwrap(), - voucher.get_plain_public_attributes(), - ); - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - good_deposit_attribute.clone(), - good_info_attribute.clone(), - EventAttribute { - key: DEPOSIT_IDENTITY_KEY.parse().unwrap(), - value: "3xoM5GmUSq7YW4YNQrax1fEFLw1GbZozxe6UUoJcrqLG" - .parse() - .unwrap(), - index: false, - }, - EventAttribute { - key: DEPOSIT_ENCRYPTION_KEY.parse().unwrap(), - value: "HxnTpWTkgigSTAysVKLE8pEiUULHdTT1BxFfzfJvQRi6" - .parse() - .unwrap(), - index: false, - }, - ]; - let res = validate_deposit_tx(&correct_request, tx_entry.clone()).await; - assert!(res.is_ok()) - } -} diff --git a/nym-api/src/coconut/helpers.rs b/nym-api/src/coconut/helpers.rs deleted file mode 100644 index 699c9b8823..0000000000 --- a/nym-api/src/coconut/helpers.rs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::coconut::error::CoconutError; -use crate::coconut::state::bandwidth_credential_params; -use nym_api_requests::coconut::models::FreePassRequest; -use nym_api_requests::coconut::BlindSignRequestBody; -use nym_coconut::{Attribute, BlindSignRequest, BlindedSignature, SecretKey}; -use nym_validator_client::nyxd::error::NyxdError::AbciError; - -// If the result is already established, the vote might be redundant and -// thus the transaction might fail -pub(crate) fn accepted_vote_err(ret: Result<(), CoconutError>) -> Result<(), CoconutError> { - if let Err(CoconutError::NyxdError(AbciError { ref log, .. })) = ret { - let accepted_err = - nym_multisig_contract_common::error::ContractError::NotOpen {}.to_string(); - // If redundant voting is not the case, error out on all other error variants - if !log.contains(&accepted_err) { - ret?; - } - } - Ok(()) -} - -pub(crate) trait CredentialRequest { - fn blind_sign_request(&self) -> &BlindSignRequest; - - fn public_attributes(&self) -> Vec; -} - -impl CredentialRequest for BlindSignRequestBody { - fn blind_sign_request(&self) -> &BlindSignRequest { - &self.inner_sign_request - } - - fn public_attributes(&self) -> Vec { - self.public_attributes_hashed() - } -} - -impl CredentialRequest for FreePassRequest { - fn blind_sign_request(&self) -> &BlindSignRequest { - &self.inner_sign_request - } - - fn public_attributes(&self) -> Vec { - self.public_attributes_hashed() - } -} - -pub(crate) fn blind_sign( - request: &C, - signing_key: &SecretKey, -) -> Result { - let public_attributes = request.public_attributes(); - let attributes_ref = public_attributes.iter().collect::>(); - - Ok(nym_coconut::blind_sign( - bandwidth_credential_params(), - signing_key, - request.blind_sign_request(), - &attributes_ref, - )?) -} diff --git a/nym-api/src/coconut/keys/persistence.rs b/nym-api/src/coconut/keys/persistence.rs deleted file mode 100644 index 4a67876036..0000000000 --- a/nym-api/src/coconut/keys/persistence.rs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::coconut::keys::KeyPairWithEpoch; -use crate::coconut::state::bandwidth_credential_params; -use nym_coconut::{CoconutError, KeyPair, SecretKey}; -use nym_coconut_dkg_common::types::EpochId; -use nym_pemstore::traits::PemStorableKey; -use std::mem; - -impl PemStorableKey for KeyPairWithEpoch { - // that's not the best error for this, but it felt like an overkill to define a dedicated struct just for this purpose - type Error = CoconutError; - - fn pem_type() -> &'static str { - "COCONUT KEY WITH EPOCH" - } - - fn to_bytes(&self) -> Vec { - let mut bytes = self.issued_for_epoch.to_be_bytes().to_vec(); - bytes.append(&mut self.keys.secret_key().to_bytes()); - bytes - } - - fn from_bytes(bytes: &[u8]) -> Result { - if bytes.len() <= mem::size_of::() { - return Err(CoconutError::Deserialization( - "insufficient number of bytes to decode secret key with epoch id".into(), - )); - } - let epoch_id = EpochId::from_be_bytes([ - bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], - ]); - - let sk = SecretKey::from_bytes(&bytes[mem::size_of::()..])?; - let vk = sk.verification_key(bandwidth_credential_params()); - - Ok(KeyPairWithEpoch { - keys: KeyPair::from_keys(sk, vk), - issued_for_epoch: epoch_id, - }) - } -} diff --git a/nym-api/src/coconut/mod.rs b/nym-api/src/coconut/mod.rs deleted file mode 100644 index 5047982304..0000000000 --- a/nym-api/src/coconut/mod.rs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use self::comm::APICommunicationChannel; -use crate::coconut::client::Client as LocalClient; -use crate::coconut::state::State; -use crate::support::storage::NymApiStorage; -use keys::KeyPair; -use nym_config::defaults::NYM_API_VERSION; -use nym_crypto::asymmetric::identity; -use nym_validator_client::nym_api::routes::{BANDWIDTH, COCONUT_ROUTES}; -use rocket::fairing::AdHoc; - -pub(crate) mod api_routes; -pub(crate) mod client; -pub(crate) mod comm; -mod deposit; -pub(crate) mod dkg; -pub(crate) mod error; -pub(crate) mod helpers; -pub(crate) mod keys; -pub(crate) mod state; -pub(crate) mod storage; -#[cfg(test)] -pub(crate) mod tests; - -// equivalent of 10nym -pub(crate) const MINIMUM_BALANCE: u128 = 10_000000; - -pub fn stage( - client: C, - mix_denom: String, - identity_keypair: identity::KeyPair, - key_pair: KeyPair, - comm_channel: D, - storage: NymApiStorage, -) -> AdHoc -where - C: LocalClient + Send + Sync + 'static, - D: APICommunicationChannel + Send + Sync + 'static, -{ - let state = State::new( - client, - mix_denom, - identity_keypair, - key_pair, - comm_channel, - storage, - ); - AdHoc::on_ignite("Internal Sign Request Stage", |rocket| async { - rocket.manage(state).mount( - // this format! is so ugly... - format!("/{NYM_API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}"), - routes![ - api_routes::get_current_free_pass_nonce, - api_routes::post_free_pass, - api_routes::post_blind_sign, - api_routes::verify_bandwidth_credential, - api_routes::epoch_credentials, - api_routes::issued_credential, - api_routes::issued_credentials, - ], - ) - }) -} diff --git a/nym-api/src/coconut/state.rs b/nym-api/src/coconut/state.rs deleted file mode 100644 index ac7c3632f8..0000000000 --- a/nym-api/src/coconut/state.rs +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::coconut::client::Client as LocalClient; -use crate::coconut::comm::APICommunicationChannel; -use crate::coconut::deposit::validate_deposit_tx; -use crate::coconut::error::{CoconutError, Result}; -use crate::coconut::keys::KeyPair; -use crate::coconut::storage::CoconutStorageExt; -use crate::support::storage::NymApiStorage; -use nym_api_requests::coconut::helpers::issued_credential_plaintext; -use nym_api_requests::coconut::BlindSignRequestBody; -use nym_coconut::{BlindedSignature, VerificationKey}; -use nym_coconut_dkg_common::types::EpochId; -use nym_crypto::asymmetric::identity; -use nym_validator_client::nyxd::{AccountId, Hash, TxResponse}; -use rand::rngs::OsRng; -use rand::RngCore; -use std::sync::Arc; -use time::{Duration, OffsetDateTime}; -use tokio::sync::{OnceCell, RwLock}; - -pub use nym_credentials::coconut::bandwidth::bandwidth_credential_params; - -pub struct State { - pub(crate) client: Arc, - pub(crate) bandwidth_contract_admin: OnceCell>, - pub(crate) mix_denom: String, - pub(crate) coconut_keypair: KeyPair, - pub(crate) identity_keypair: identity::KeyPair, - pub(crate) comm_channel: Arc, - pub(crate) storage: NymApiStorage, - pub(crate) freepass_nonce: Arc>, - pub(crate) authorised_freepass_requester: Arc>, -} - -const FREEPASS_REQUESTER_TTL: Duration = Duration::hours(1); -const AUTHORISED_FREEPASS_REQUESTER_ENDPOINT: &str = - "https://nymtech.net/.wellknown/authorised-freepass-requester.txt"; - -pub struct AuthorisedFreepassRequester { - address: Option, - refreshed_at: OffsetDateTime, -} - -impl Default for AuthorisedFreepassRequester { - fn default() -> Self { - AuthorisedFreepassRequester { - address: None, - refreshed_at: OffsetDateTime::UNIX_EPOCH, - } - } -} - -impl State { - pub(crate) fn new( - client: C, - mix_denom: String, - identity_keypair: identity::KeyPair, - key_pair: KeyPair, - comm_channel: D, - storage: NymApiStorage, - ) -> Self - where - C: LocalClient + Send + Sync + 'static, - D: APICommunicationChannel + Send + Sync + 'static, - { - let client = Arc::new(client); - let comm_channel = Arc::new(comm_channel); - - let mut nonce = [0u8; 16]; - OsRng.fill_bytes(&mut nonce); - - Self { - client, - bandwidth_contract_admin: OnceCell::new(), - mix_denom, - coconut_keypair: key_pair, - identity_keypair, - comm_channel, - storage, - freepass_nonce: Arc::new(RwLock::new(nonce)), - authorised_freepass_requester: Arc::new(Default::default()), - } - } - - /// Check if this nym-api has already issued a credential for the provided deposit hash. - /// If so, return it. - pub async fn already_issued(&self, tx_hash: Hash) -> Result> { - self.storage - .get_issued_bandwidth_credential_by_hash(&tx_hash.to_string()) - .await? - .map(|cred| cred.try_into()) - .transpose() - } - - pub async fn get_transaction(&self, tx_hash: Hash) -> Result { - self.client.get_tx(tx_hash).await - } - - pub async fn get_bandwidth_contract_admin(&self) -> Result<&Option> { - self.bandwidth_contract_admin - .get_or_try_init(|| async { self.client.bandwidth_contract_admin().await }) - .await - } - - async fn try_get_authorised_freepass_requester(&self) -> Result { - let address = reqwest::Client::builder() - .user_agent(format!( - "nym-api / {} identity: {}", - env!("CARGO_PKG_VERSION"), - self.identity_keypair.public_key().to_base58_string() - )) - .build()? - .get(AUTHORISED_FREEPASS_REQUESTER_ENDPOINT) - .send() - .await? - .text() - .await?; - let trimmed = address.trim(); - - address.parse().map_err( - |_| CoconutError::MalformedAuthorisedFreepassRequesterAddress { - address: trimmed.to_string(), - }, - ) - } - - pub async fn get_authorised_freepass_requester(&self) -> Option { - { - let cached = self.authorised_freepass_requester.read().await; - - // the entry hasn't expired - if cached.refreshed_at + FREEPASS_REQUESTER_TTL >= OffsetDateTime::now_utc() { - if let Some(cached_address) = cached.address.as_ref() { - return Some(cached_address.clone()); - } - } - } - - // refresh cache - let mut cache = self.authorised_freepass_requester.write().await; - - // whatever happens, update refresh time - cache.refreshed_at = OffsetDateTime::now_utc(); - - let refreshed = match self.try_get_authorised_freepass_requester().await { - Ok(upstream) => upstream, - Err(err) => { - warn!("failed to obtain authorised freepass requester address: {err}"); - return None; - } - }; - - cache.address = Some(refreshed.clone()); - Some(refreshed) - } - - pub async fn validate_request( - &self, - request: &BlindSignRequestBody, - tx: TxResponse, - ) -> Result<()> { - validate_deposit_tx(request, tx).await - } - - pub(crate) async fn sign_and_store_credential( - &self, - current_epoch: EpochId, - request_body: BlindSignRequestBody, - blinded_signature: &BlindedSignature, - ) -> Result { - let encoded_commitments = request_body.encode_commitments(); - - let plaintext = issued_credential_plaintext( - current_epoch as u32, - request_body.tx_hash, - blinded_signature, - &encoded_commitments, - &request_body.public_attributes_plain, - ); - - let signature = self.identity_keypair.private_key().sign(plaintext); - - // note: we have a UNIQUE constraint on the tx_hash column of the credential - // and so if the api is processing request for the same hash at the same time, - // only one of them will be successfully inserted to the database - let credential_id = self - .storage - .store_issued_credential( - current_epoch as u32, - request_body.tx_hash, - blinded_signature, - signature, - encoded_commitments, - request_body.public_attributes_plain, - ) - .await?; - - Ok(credential_id) - } - - pub async fn store_issued_credential( - &self, - request_body: BlindSignRequestBody, - blinded_signature: &BlindedSignature, - ) -> Result<()> { - let current_epoch = self.comm_channel.current_epoch().await?; - - // note: we have a UNIQUE constraint on the tx_hash column of the credential - // and so if the api is processing request for the same hash at the same time, - // only one of them will be successfully inserted to the database - let credential_id = self - .sign_and_store_credential(current_epoch, request_body, blinded_signature) - .await?; - self.storage - .update_epoch_credentials_entry(current_epoch, credential_id) - .await?; - debug!("the stored credential has id {credential_id}"); - - Ok(()) - } - - pub async fn verification_key(&self, epoch_id: EpochId) -> Result { - self.comm_channel - .aggregated_verification_key(epoch_id) - .await - } -} diff --git a/nym-api/src/coconut/storage/manager.rs b/nym-api/src/coconut/storage/manager.rs deleted file mode 100644 index c22d295459..0000000000 --- a/nym-api/src/coconut/storage/manager.rs +++ /dev/null @@ -1,363 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::coconut::storage::models::{EpochCredentials, IssuedCredential}; -use crate::support::storage::manager::StorageManager; -use nym_coconut_dkg_common::types::EpochId; - -#[async_trait] -pub trait CoconutStorageManagerExt { - /// Gets the information about all issued partial credentials in this (coconut) epoch. - /// - /// # Arguments - /// - /// * `epoch_id`: Id of the (coconut) epoch in question. - async fn get_epoch_credentials( - &self, - epoch_id: EpochId, - ) -> Result, sqlx::Error>; - - /// Creates new entry for EpochCredentials for this (coconut) epoch. - /// - /// # Arguments - /// - /// * `epoch_id`: Id of the (coconut) epoch in question. - #[allow(dead_code)] - async fn create_epoch_credentials_entry(&self, epoch_id: EpochId) -> Result<(), sqlx::Error>; - - /// Update the EpochCredentials by incrementing the total number of issued credentials, - /// and setting `start_id` if unset (i.e. this is the first credential issued this epoch) - /// - /// # Arguments - /// * `epoch_id`: Id of the (coconut) epoch in question. - /// * `credential_id`: (database) Id of the coconut credential that triggered the update. - async fn update_epoch_credentials_entry( - &self, - epoch_id: EpochId, - credential_id: i64, - ) -> Result<(), sqlx::Error>; - - /// Attempts to retrieve an issued credential from the data store. - /// - /// # Arguments - /// - /// * `credential_id`: (database) id of the issued credential - async fn get_issued_credential( - &self, - credential_id: i64, - ) -> Result, sqlx::Error>; - - /// Attempts to retrieve an issued credential from the data store. - /// - /// # Arguments - /// - /// * `tx_hash`: transaction hash of the deposit used in the issued bandwidth credential - async fn get_issued_bandwidth_credential_by_hash( - &self, - tx_hash: &str, - ) -> Result, sqlx::Error>; - - /// Store the provided issued credential information and return its (database) id. - /// - /// # Arguments - /// - /// * `credential`: partial credential, alongside any data required for verification. - async fn store_issued_credential( - &self, - epoch_id: u32, - tx_hash: String, - bs58_partial_credential: String, - bs58_signature: String, - joined_private_commitments: String, - joined_public_attributes: String, - ) -> Result; - - /// Attempts to retrieve issued credentials from the data store using provided ids. - /// - /// # Arguments - /// - /// * `credential_ids`: (database) ids of the issued credentials - async fn get_issued_credentials( - &self, - credential_ids: Vec, - ) -> Result, sqlx::Error>; - - /// Attempts to retrieve issued credentials from the data store using pagination specification. - /// - /// # Arguments - /// - /// * `start_after`: the value preceding the first retrieved result - /// * `limit`: the maximum number of entries to retrieve - async fn get_issued_credentials_paged( - &self, - start_after: i64, - limit: u32, - ) -> Result, sqlx::Error>; - - async fn increment_issued_freepasses(&self) -> Result<(), sqlx::Error>; -} - -#[async_trait] -impl CoconutStorageManagerExt for StorageManager { - /// Gets the information about all issued partial credentials in this (coconut) epoch. - /// - /// # Arguments - /// - /// * `epoch_id`: Id of the (coconut) epoch in question. - async fn get_epoch_credentials( - &self, - epoch_id: EpochId, - ) -> Result, sqlx::Error> { - // even if we were changing epochs every second, it's rather impossible to overflow here - // within any sane amount of time - assert!(epoch_id <= u32::MAX as u64); - let epoch_id_downcasted = epoch_id as u32; - - sqlx::query_as!( - EpochCredentials, - r#" - SELECT epoch_id as "epoch_id: u32", start_id, total_issued as "total_issued: u32" - FROM epoch_credentials - WHERE epoch_id = ? - "#, - epoch_id_downcasted - ) - .fetch_optional(&self.connection_pool) - .await - } - - /// Creates new entry for EpochCredentials for this (coconut) epoch. - /// - /// # Arguments - /// - /// * `epoch_id`: Id of the (coconut) epoch in question. - async fn create_epoch_credentials_entry(&self, epoch_id: EpochId) -> Result<(), sqlx::Error> { - // even if we were changing epochs every second, it's rather impossible to overflow here - // within any sane amount of time - assert!(epoch_id <= u32::MAX as u64); - let epoch_id_downcasted = epoch_id as u32; - - sqlx::query!( - r#" - INSERT INTO epoch_credentials - (epoch_id, start_id, total_issued) - VALUES (?, ?, ?); - "#, - epoch_id_downcasted, - -1, - 0 - ) - .execute(&self.connection_pool) - .await?; - Ok(()) - } - - // the logic in this function can be summarised with: - // 1. get the current EpochCredentials for this epoch - // 2. if it exists -> increment `total_issued` - // 3. it has invalid (i.e. -1) `start_id` set it to the provided value - // 4. if it didn't exist, create new entry - /// Update the EpochCredentials by incrementing the total number of issued credentials, - /// and setting `start_id` if unset (i.e. this is the first credential issued this epoch) - /// - /// # Arguments - /// * `epoch_id`: Id of the (coconut) epoch in question. - /// * `credential_id`: (database) Id of the coconut credential that triggered the update. - async fn update_epoch_credentials_entry( - &self, - epoch_id: EpochId, - credential_id: i64, - ) -> Result<(), sqlx::Error> { - // even if we were changing epochs every second, it's rather impossible to overflow here - // within any sane amount of time - assert!(epoch_id <= u32::MAX as u64); - let epoch_id_downcasted = epoch_id as u32; - - // make the atomic transaction in case other tasks are attempting to use the pool - let mut tx = self.connection_pool.begin().await?; - - if let Some(existing) = sqlx::query_as!( - EpochCredentials, - r#" - SELECT epoch_id as "epoch_id: u32", start_id, total_issued as "total_issued: u32" - FROM epoch_credentials - WHERE epoch_id = ? - "#, - epoch_id_downcasted - ) - .fetch_optional(&mut tx) - .await? - { - // the entry has existed before -> update it - if existing.total_issued == 0 { - // no credentials has been issued -> we have to set the `start_id` - sqlx::query!( - r#" - UPDATE epoch_credentials - SET total_issued = 1, start_id = ? - WHERE epoch_id = ? - "#, - credential_id, - epoch_id_downcasted - ) - .execute(&mut tx) - .await?; - } else { - // we have issued credentials in this epoch before -> just increment `total_issued` - sqlx::query!( - r#" - UPDATE epoch_credentials - SET total_issued = total_issued + 1 - WHERE epoch_id = ? - "#, - epoch_id_downcasted - ) - .execute(&mut tx) - .await?; - } - } else { - // the entry has never been created -> probably some race condition; create it instead - sqlx::query!( - r#" - INSERT INTO epoch_credentials - (epoch_id, start_id, total_issued) - VALUES (?, ?, ?); - "#, - epoch_id_downcasted, - credential_id, - 1 - ) - .execute(&mut tx) - .await?; - } - - // finally commit the transaction - tx.commit().await - } - - /// Attempts to retrieve an issued credential from the data store. - /// - /// # Arguments - /// - /// * `credential_id`: (database) id of the issued credential - async fn get_issued_credential( - &self, - credential_id: i64, - ) -> Result, sqlx::Error> { - sqlx::query_as!( - IssuedCredential, - r#" - SELECT id, epoch_id as "epoch_id: u32", tx_hash, bs58_partial_credential, bs58_signature,joined_private_commitments, joined_public_attributes - FROM issued_credential - WHERE id = ? - "#, - credential_id - ) - .fetch_optional(&self.connection_pool) - .await - } - - /// Attempts to retrieve an issued credential from the data store. - /// - /// # Arguments - /// - /// * `tx_hash`: transaction hash of the deposit used in the issued bandwidth credential - async fn get_issued_bandwidth_credential_by_hash( - &self, - tx_hash: &str, - ) -> Result, sqlx::Error> { - sqlx::query_as!( - IssuedCredential, - r#" - SELECT id, epoch_id as "epoch_id: u32", tx_hash, bs58_partial_credential, bs58_signature,joined_private_commitments, joined_public_attributes - FROM issued_credential - WHERE tx_hash = ? - "#, - tx_hash - ) - .fetch_optional(&self.connection_pool) - .await - } - - /// Store the provided issued credential information and return its (database) id. - /// - /// # Arguments - /// - /// * `credential`: partial credential, alongside any data required for verification. - async fn store_issued_credential( - &self, - epoch_id: u32, - tx_hash: String, - bs58_partial_credential: String, - bs58_signature: String, - joined_private_commitments: String, - joined_public_attributes: String, - ) -> Result { - let row_id = sqlx::query!( - r#" - INSERT INTO issued_credential - (epoch_id, tx_hash, bs58_partial_credential, bs58_signature, joined_private_commitments, joined_public_attributes) - VALUES - (?, ?, ?, ?, ?, ?) - "#, - epoch_id, tx_hash, bs58_partial_credential, bs58_signature, joined_private_commitments, joined_public_attributes - ).execute(&self.connection_pool).await?.last_insert_rowid(); - - Ok(row_id) - } - - /// Attempts to retrieve issued credentials from the data store using provided ids. - /// - /// # Arguments - /// - /// * `credential_ids`: (database) ids of the issued credentials - async fn get_issued_credentials( - &self, - credential_ids: Vec, - ) -> Result, sqlx::Error> { - // that sucks : ( - // https://stackoverflow.com/a/70032524 - let params = format!("?{}", ", ?".repeat(credential_ids.len() - 1)); - let query_str = format!("SELECT * FROM issued_credential WHERE id IN ( {params} )"); - let mut query = sqlx::query_as(&query_str); - for id in credential_ids { - query = query.bind(id) - } - - query.fetch_all(&self.connection_pool).await - } - - /// Attempts to retrieve issued credentials from the data store using pagination specification. - /// - /// # Arguments - /// - /// * `start_after`: the value preceding the first retrieved result - /// * `limit`: the maximum number of entries to retrieve - async fn get_issued_credentials_paged( - &self, - start_after: i64, - limit: u32, - ) -> Result, sqlx::Error> { - sqlx::query_as!( - IssuedCredential, - r#" - SELECT id, epoch_id as "epoch_id: u32", tx_hash, bs58_partial_credential, bs58_signature,joined_private_commitments, joined_public_attributes - FROM issued_credential - WHERE id > ? - ORDER BY id - LIMIT ? - "#, - start_after, - limit - ) - .fetch_all(&self.connection_pool) - .await - } - - async fn increment_issued_freepasses(&self) -> Result<(), sqlx::Error> { - sqlx::query!("UPDATE issued_freepass SET issued = issued + 1",) - .execute(&self.connection_pool) - .await?; - Ok(()) - } -} diff --git a/nym-api/src/coconut/storage/mod.rs b/nym-api/src/coconut/storage/mod.rs deleted file mode 100644 index 1b29f51b43..0000000000 --- a/nym-api/src/coconut/storage/mod.rs +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::coconut::storage::manager::CoconutStorageManagerExt; -use crate::coconut::storage::models::{join_attributes, EpochCredentials, IssuedCredential}; -use crate::node_status_api::models::NymApiStorageError; -use crate::support::storage::NymApiStorage; -use nym_api_requests::coconut::models::Pagination; -use nym_coconut::{Base58, BlindedSignature}; -use nym_coconut_dkg_common::types::EpochId; -use nym_crypto::asymmetric::identity; -use nym_validator_client::nyxd::Hash; - -pub(crate) mod manager; -pub(crate) mod models; - -const DEFAULT_CREDENTIALS_PAGE_LIMIT: u32 = 100; - -#[async_trait] -pub trait CoconutStorageExt { - async fn get_epoch_credentials( - &self, - epoch_id: EpochId, - ) -> Result, NymApiStorageError>; - - #[allow(dead_code)] - async fn create_epoch_credentials_entry( - &self, - epoch_id: EpochId, - ) -> Result<(), NymApiStorageError>; - - async fn update_epoch_credentials_entry( - &self, - epoch_id: EpochId, - credential_id: i64, - ) -> Result<(), NymApiStorageError>; - - async fn get_issued_credential( - &self, - credential_id: i64, - ) -> Result, NymApiStorageError>; - - async fn get_issued_bandwidth_credential_by_hash( - &self, - tx_hash: &str, - ) -> Result, NymApiStorageError>; - - async fn store_issued_credential( - &self, - epoch_id: u32, - tx_hash: Hash, - partial_credential: &BlindedSignature, - signature: identity::Signature, - private_commitments: Vec, - public_attributes: Vec, - ) -> Result; - - async fn get_issued_credentials( - &self, - credential_ids: Vec, - ) -> Result, NymApiStorageError>; - - async fn get_issued_credentials_paged( - &self, - pagination: Pagination, - ) -> Result, NymApiStorageError>; - - async fn increment_issued_freepasses(&self) -> Result<(), NymApiStorageError>; -} - -#[async_trait] -impl CoconutStorageExt for NymApiStorage { - async fn get_epoch_credentials( - &self, - epoch_id: EpochId, - ) -> Result, NymApiStorageError> { - Ok(self.manager.get_epoch_credentials(epoch_id).await?) - } - - async fn create_epoch_credentials_entry( - &self, - epoch_id: EpochId, - ) -> Result<(), NymApiStorageError> { - Ok(self - .manager - .create_epoch_credentials_entry(epoch_id) - .await?) - } - - async fn update_epoch_credentials_entry( - &self, - epoch_id: EpochId, - credential_id: i64, - ) -> Result<(), NymApiStorageError> { - Ok(self - .manager - .update_epoch_credentials_entry(epoch_id, credential_id) - .await?) - } - - async fn get_issued_credential( - &self, - credential_id: i64, - ) -> Result, NymApiStorageError> { - Ok(self.manager.get_issued_credential(credential_id).await?) - } - - async fn get_issued_bandwidth_credential_by_hash( - &self, - tx_hash: &str, - ) -> Result, NymApiStorageError> { - Ok(self - .manager - .get_issued_bandwidth_credential_by_hash(tx_hash) - .await?) - } - - async fn store_issued_credential( - &self, - epoch_id: u32, - tx_hash: Hash, - partial_credential: &BlindedSignature, - signature: identity::Signature, - private_commitments: Vec, - public_attributes: Vec, - ) -> Result { - Ok(self - .manager - .store_issued_credential( - epoch_id, - tx_hash.to_string(), - partial_credential.to_bs58(), - signature.to_base58_string(), - join_attributes(private_commitments), - join_attributes(public_attributes), - ) - .await?) - } - - async fn get_issued_credentials( - &self, - credential_ids: Vec, - ) -> Result, NymApiStorageError> { - Ok(self.manager.get_issued_credentials(credential_ids).await?) - } - - async fn get_issued_credentials_paged( - &self, - pagination: Pagination, - ) -> Result, NymApiStorageError> { - // rows start at 1 - let start_after = pagination.last_key.unwrap_or(0); - let limit = match pagination.limit { - Some(v) => { - if v == 0 || v > DEFAULT_CREDENTIALS_PAGE_LIMIT { - DEFAULT_CREDENTIALS_PAGE_LIMIT - } else { - v - } - } - None => DEFAULT_CREDENTIALS_PAGE_LIMIT, - }; - - Ok(self - .manager - .get_issued_credentials_paged(start_after, limit) - .await?) - } - - async fn increment_issued_freepasses(&self) -> Result<(), NymApiStorageError> { - Ok(self.manager.increment_issued_freepasses().await?) - } -} diff --git a/nym-api/src/coconut/storage/models.rs b/nym-api/src/coconut/storage/models.rs deleted file mode 100644 index 4d2935c057..0000000000 --- a/nym-api/src/coconut/storage/models.rs +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::coconut::error::CoconutError; -use nym_api_requests::coconut::models::{ - EpochCredentialsResponse, IssuedCredential as ApiIssuedCredential, - IssuedCredentialBody as ApiIssuedCredentialInner, -}; -use nym_api_requests::coconut::BlindedSignatureResponse; -use nym_coconut::{Base58, BlindedSignature}; -use sqlx::FromRow; -use std::fmt::Display; - -pub struct EpochCredentials { - pub epoch_id: u32, - pub start_id: i64, - pub total_issued: u32, -} - -impl From for EpochCredentialsResponse { - fn from(value: EpochCredentials) -> Self { - let first_epoch_credential_id = if value.start_id == -1 { - None - } else { - Some(value.start_id) - }; - - EpochCredentialsResponse { - epoch_id: value.epoch_id as u64, - first_epoch_credential_id, - total_issued: value.total_issued, - } - } -} - -#[derive(FromRow)] -pub struct IssuedCredential { - pub id: i64, - pub epoch_id: u32, - pub tx_hash: String, - - /// base58-encoded issued credential - pub bs58_partial_credential: String, - - /// base58-encoded signature on the issued credential (and the attributes) - pub bs58_signature: String, - - // i.e. "'attr1','attr2',..." - pub joined_private_commitments: String, - - // i.e. "'attr1','attr2',..." - pub joined_public_attributes: String, -} - -impl TryFrom for ApiIssuedCredentialInner { - type Error = CoconutError; - - fn try_from(value: IssuedCredential) -> Result { - Ok(ApiIssuedCredentialInner { - credential: ApiIssuedCredential { - id: value.id, - epoch_id: value.epoch_id, - tx_hash: value - .tx_hash - .parse() - .map_err(|source| CoconutError::TxHashParseError { source })?, - blinded_partial_credential: BlindedSignature::try_from_bs58( - value.bs58_partial_credential, - )?, - bs58_encoded_private_attributes_commitments: split_attributes( - &value.joined_private_commitments, - ), - public_attributes: split_attributes(&value.joined_public_attributes), - }, - signature: value.bs58_signature.parse()?, - }) - } -} - -impl TryFrom for BlindedSignatureResponse { - type Error = CoconutError; - - fn try_from(value: IssuedCredential) -> Result { - Ok(BlindedSignatureResponse { - blinded_signature: BlindedSignature::try_from_bs58(value.bs58_partial_credential)?, - }) - } -} - -impl TryFrom for BlindedSignature { - type Error = CoconutError; - - fn try_from(value: IssuedCredential) -> Result { - Ok(BlindedSignature::try_from_bs58( - value.bs58_partial_credential, - )?) - } -} - -pub fn join_attributes(attrs: I) -> String -where - I: IntoIterator, - M: Display, -{ - // I could have used `attrs.into_iter().join(",")`, - // but my IDE didn't like it (compiler was fine) - itertools::Itertools::join(&mut attrs.into_iter(), ",") -} - -pub fn split_attributes(attrs: &str) -> Vec { - attrs.split(',').map(|s| s.to_string()).collect() -} diff --git a/nym-api/src/ecash/api_routes/aggregation.rs b/nym-api/src/ecash/api_routes/aggregation.rs new file mode 100644 index 0000000000..f552698d86 --- /dev/null +++ b/nym-api/src/ecash/api_routes/aggregation.rs @@ -0,0 +1,81 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::error::{EcashError, Result}; +use crate::ecash::state::EcashState; +use log::trace; +use nym_api_requests::ecash::models::{ + AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, +}; +use nym_api_requests::ecash::VerificationKeyResponse; +use nym_ecash_time::{cred_exp_date, EcashTime}; +use nym_validator_client::nym_api::rfc_3339_date; +use rocket::serde::json::Json; +use rocket::State as RocketState; +use rocket_okapi::openapi; +use time::Date; + +// routes with globally aggregated keys, signatures, etc. + +#[openapi(tag = "Ecash Global Data")] +#[get("/master-verification-key?")] +pub async fn master_verification_key( + epoch_id: Option, + state: &RocketState, +) -> Result> { + trace!("aggregated_verification_key request"); + + // see if we're not in the middle of new dkg + state.ensure_dkg_not_in_progress().await?; + + let key = state.master_verification_key(epoch_id).await?; + + Ok(Json(VerificationKeyResponse::new(key.clone()))) +} + +#[openapi(tag = "Ecash Global Data")] +#[get("/aggregated-expiration-date-signatures?")] +pub async fn expiration_date_signatures( + expiration_date: Option, + state: &RocketState, +) -> Result> { + trace!("aggregated_expiration_date_signatures request"); + + let expiration_date = match expiration_date { + None => cred_exp_date().ecash_date(), + Some(raw) => Date::parse(&raw, &rfc_3339_date()) + .map_err(|_| EcashError::MalformedExpirationDate { raw })?, + }; + + // see if we're not in the middle of new dkg + state.ensure_dkg_not_in_progress().await?; + + let expiration_date_signatures = state + .master_expiration_date_signatures(expiration_date) + .await?; + + Ok(Json(AggregatedExpirationDateSignatureResponse { + epoch_id: expiration_date_signatures.epoch_id, + expiration_date, + signatures: expiration_date_signatures.signatures.clone(), + })) +} + +#[openapi(tag = "Ecash Global Data")] +#[get("/aggregated-coin-indices-signatures?")] +pub async fn coin_indices_signatures( + epoch_id: Option, + state: &RocketState, +) -> Result> { + trace!("aggregated_coin_indices_signatures request"); + + // see if we're not in the middle of new dkg + state.ensure_dkg_not_in_progress().await?; + + let coin_indices_signatures = state.master_coin_index_signatures(epoch_id).await?; + + Ok(Json(AggregatedCoinIndicesSignatureResponse { + epoch_id: coin_indices_signatures.epoch_id, + signatures: coin_indices_signatures.signatures.clone(), + })) +} diff --git a/nym-api/src/coconut/api_routes/helpers.rs b/nym-api/src/ecash/api_routes/helpers.rs similarity index 73% rename from nym-api/src/coconut/api_routes/helpers.rs rename to nym-api/src/ecash/api_routes/helpers.rs index 93446d0d15..521f11067a 100644 --- a/nym-api/src/coconut/api_routes/helpers.rs +++ b/nym-api/src/ecash/api_routes/helpers.rs @@ -1,20 +1,20 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::error::Result; -use crate::coconut::storage::models::IssuedCredential; -use nym_api_requests::coconut::models::IssuedCredentialBody; -use nym_api_requests::coconut::models::IssuedCredentialsResponse; +use crate::ecash::error::Result; +use crate::ecash::storage::models::IssuedTicketbook; +use nym_api_requests::ecash::models::IssuedCredentialsResponse; +use nym_api_requests::ecash::models::IssuedTicketbookBody; use std::collections::BTreeMap; pub(crate) fn build_credentials_response( - raw: Vec, + raw: Vec, ) -> Result { let mut credentials = BTreeMap::new(); for raw_credential in raw { let id = raw_credential.id; - let api_issued = IssuedCredentialBody::try_from(raw_credential)?; + let api_issued = IssuedTicketbookBody::try_from(raw_credential)?; let old = credentials.insert(id, api_issued); if old.is_some() { // why do we panic here rather than return an error? because it's a critical failure because diff --git a/nym-api/src/ecash/api_routes/issued.rs b/nym-api/src/ecash/api_routes/issued.rs new file mode 100644 index 0000000000..5b85e6b0cf --- /dev/null +++ b/nym-api/src/ecash/api_routes/issued.rs @@ -0,0 +1,82 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::api_routes::helpers::build_credentials_response; +use crate::ecash::error::EcashError; +use crate::ecash::state::EcashState; +use crate::ecash::storage::EcashStorageExt; +use nym_api_requests::ecash::models::{ + EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse, +}; +use nym_api_requests::ecash::CredentialsRequestBody; +use nym_coconut_dkg_common::types::EpochId; +use rocket::serde::json::Json; +use rocket::State as RocketState; +use rocket_okapi::openapi; + +#[openapi(tag = "Ecash")] +#[get("/epoch-credentials/")] +pub async fn epoch_credentials( + epoch: EpochId, + state: &RocketState, +) -> crate::ecash::error::Result> { + let issued = state.aux.storage.get_epoch_credentials(epoch).await?; + + let response = if let Some(issued) = issued { + issued.into() + } else { + EpochCredentialsResponse { + epoch_id: epoch, + first_epoch_credential_id: None, + total_issued: 0, + } + }; + + Ok(Json(response)) +} + +#[openapi(tag = "Ecash")] +#[get("/issued-credential/")] +pub async fn issued_credential( + id: i64, + state: &RocketState, +) -> crate::ecash::error::Result> { + let issued = state.aux.storage.get_issued_credential(id).await?; + + let credential = if let Some(issued) = issued { + Some(issued.try_into()?) + } else { + None + }; + + Ok(Json(IssuedCredentialResponse { credential })) +} + +#[openapi(tag = "Ecash")] +#[post("/issued-credentials", data = "")] +pub async fn issued_credentials( + params: Json, + state: &RocketState, +) -> crate::ecash::error::Result> { + let params = params.into_inner(); + + if params.pagination.is_some() && !params.credential_ids.is_empty() { + return Err(EcashError::InvalidQueryArguments); + } + + let credentials = if let Some(pagination) = params.pagination { + state + .aux + .storage + .get_issued_credentials_paged(pagination) + .await? + } else { + state + .aux + .storage + .get_issued_credentials(params.credential_ids) + .await? + }; + + build_credentials_response(credentials).map(Json) +} diff --git a/nym-api/src/ecash/api_routes/mod.rs b/nym-api/src/ecash/api_routes/mod.rs new file mode 100644 index 0000000000..6037a8d18f --- /dev/null +++ b/nym-api/src/ecash/api_routes/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub(crate) mod aggregation; +mod helpers; +pub(crate) mod issued; +pub(crate) mod partial_signing; +pub(crate) mod spending; diff --git a/nym-api/src/ecash/api_routes/partial_signing.rs b/nym-api/src/ecash/api_routes/partial_signing.rs new file mode 100644 index 0000000000..a0958a2a6b --- /dev/null +++ b/nym-api/src/ecash/api_routes/partial_signing.rs @@ -0,0 +1,118 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::error::EcashError; +use crate::ecash::helpers::blind_sign; +use crate::ecash::state::EcashState; +use nym_api_requests::ecash::{ + BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse, + PartialExpirationDateSignatureResponse, +}; +use nym_ecash_time::{cred_exp_date, EcashTime}; +use nym_validator_client::nym_api::rfc_3339_date; +use rocket::serde::json::Json; +use rocket::State as RocketState; +use rocket_okapi::openapi; +use std::ops::Deref; +use time::Date; + +#[openapi(tag = "Ecash")] +#[post("/blind-sign", data = "")] +pub async fn post_blind_sign( + blind_sign_request_body: Json, + state: &RocketState, +) -> crate::ecash::error::Result> { + debug!("Received blind sign request"); + trace!("body: {:?}", blind_sign_request_body); + + // check if we have the signing key available + debug!("checking if we actually have ecash keys derived..."); + let signing_key = state.ecash_signing_key().await?; + + // basic check of expiration date validity + if blind_sign_request_body.expiration_date > cred_exp_date().ecash_date() { + return Err(EcashError::ExpirationDateTooLate); + } + + // see if we're not in the middle of new dkg + state.ensure_dkg_not_in_progress().await?; + + // check if we already issued a credential for this deposit + let deposit_id = blind_sign_request_body.deposit_id; + debug!( + "checking if we have already issued credential for this deposit (deposit_id: {deposit_id})", + ); + if let Some(blinded_signature) = state.already_issued(deposit_id).await? { + return Ok(Json(BlindedSignatureResponse { blinded_signature })); + } + + //check if account was blacklisted + let pub_key_bs58 = blind_sign_request_body.ecash_pubkey.to_base58_string(); + state.aux.ensure_not_blacklisted(&pub_key_bs58).await?; + + // get the deposit details of the claimed id + debug!("getting deposit details from the chain"); + let deposit = state.get_deposit(deposit_id).await?; + + // check validity of the request + debug!("fully validating received request"); + state + .validate_request(&blind_sign_request_body, deposit) + .await?; + + // produce the partial signature + debug!("producing the partial credential"); + let blinded_signature = blind_sign(blind_sign_request_body.deref(), signing_key.deref())?; + + // store the information locally + debug!("storing the issued credential in the database"); + state + .store_issued_credential(blind_sign_request_body.into_inner(), &blinded_signature) + .await?; + + // finally return the credential to the client + Ok(Json(BlindedSignatureResponse { blinded_signature })) +} + +#[openapi(tag = "Ecash")] +#[get("/partial-expiration-date-signatures?")] +pub async fn partial_expiration_date_signatures( + expiration_date: Option, + state: &RocketState, +) -> crate::ecash::error::Result> { + let expiration_date = match expiration_date { + None => cred_exp_date().ecash_date(), + Some(raw) => Date::parse(&raw, &rfc_3339_date()) + .map_err(|_| EcashError::MalformedExpirationDate { raw })?, + }; + + // see if we're not in the middle of new dkg + state.ensure_dkg_not_in_progress().await?; + + let expiration_date_signatures = state + .partial_expiration_date_signatures(expiration_date) + .await?; + + Ok(Json(PartialExpirationDateSignatureResponse { + epoch_id: expiration_date_signatures.epoch_id, + expiration_date, + signatures: expiration_date_signatures.signatures.clone(), + })) +} + +#[openapi(tag = "Ecash")] +#[get("/partial-coin-indices-signatures?")] +pub async fn partial_coin_indices_signatures( + epoch_id: Option, + state: &RocketState, +) -> crate::ecash::error::Result> { + // see if we're not in the middle of new dkg + state.ensure_dkg_not_in_progress().await?; + + let coin_indices_signatures = state.partial_coin_index_signatures(epoch_id).await?; + + Ok(Json(PartialCoinIndicesSignatureResponse { + epoch_id: coin_indices_signatures.epoch_id, + signatures: coin_indices_signatures.signatures.clone(), + })) +} diff --git a/nym-api/src/ecash/api_routes/spending.rs b/nym-api/src/ecash/api_routes/spending.rs new file mode 100644 index 0000000000..b5eedbcdbe --- /dev/null +++ b/nym-api/src/ecash/api_routes/spending.rs @@ -0,0 +1,202 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::error::EcashError; +use crate::ecash::state::EcashState; +use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY; +use nym_api_requests::ecash::models::{ + BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationRejection, + EcashTicketVerificationResponse, SpentCredentialsResponse, VerifyEcashTicketBody, +}; +use nym_compact_ecash::identify::IdentifyResult; +use nym_ecash_time::EcashTime; +use rocket::serde::json::Json; +use rocket::State as RocketState; +use rocket_okapi::openapi; +use std::collections::HashSet; +use std::ops::Deref; +use time::macros::time; +use time::{OffsetDateTime, Time}; + +const ONE_AM: Time = time!(1:00); + +fn reject_ticket( + reason: EcashTicketVerificationRejection, +) -> crate::ecash::error::Result> { + Ok(Json(EcashTicketVerificationResponse::reject(reason))) +} + +// TODO: optimise it; for now it's just dummy split of the original `verify_offline_credential` +// introduce bloomfilter checks without touching storage first, etc. +#[openapi(tag = "Ecash")] +#[post("/verify-ecash-ticket", data = "")] +pub async fn verify_ticket( + // TODO in the future: make it send binary data rather than json + verify_ticket_body: Json, + state: &RocketState, +) -> crate::ecash::error::Result> { + let credential_data = &verify_ticket_body.credential; + let gateway_cosmos_addr = &verify_ticket_body.gateway_cosmos_addr; + + // easy check: is there only a single payment attached? + if credential_data.payment.spend_value != 1 { + return reject_ticket(EcashTicketVerificationRejection::MultipleTickets); + } + + let sn = &credential_data.encoded_serial_number(); + let spend_date = credential_data.spend_date; + let epoch_id = credential_data.epoch_id; + + let now = OffsetDateTime::now_utc(); + let today_ecash = now.ecash_date(); + + #[allow(clippy::unwrap_used)] + let yesterday_ecash = today_ecash.previous_day().unwrap(); + + // only accept yesterday date if we're near the day transition, i.e. before 1AM UTC + if spend_date != today_ecash && now.time() > ONE_AM && spend_date != yesterday_ecash { + return reject_ticket(EcashTicketVerificationRejection::InvalidSpentDate { + today: today_ecash, + yesterday: yesterday_ecash, + received: spend_date, + }); + } + + // check the bloomfilter for obvious double-spending so that we wouldn't need to waste time on crypto verification + // TODO: when blacklisting is implemented, this should get removed + if state.check_bloomfilter(sn).await { + return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); + } + + // actual double spend detection with storage + if let Some(previous_payment) = state.get_ticket_data_by_serial_number(sn).await? { + match nym_compact_ecash::identify::identify( + &credential_data.payment, + &previous_payment.payment, + credential_data.pay_info, + previous_payment.pay_info, + ) { + IdentifyResult::NotADuplicatePayment => {} //SW NOTE This should never happen, quick message? + IdentifyResult::DuplicatePayInfo(_) => { + log::warn!("Identical payInfo"); + return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); + } + IdentifyResult::DoubleSpendingPublicKeys(pub_key) => { + //Actual double spending + log::warn!( + "Double spending attempt for key {}", + pub_key.to_base58_string() + ); + log::error!("UNIMPLEMENTED: blacklisting the double spend key"); + return reject_ticket(EcashTicketVerificationRejection::DoubleSpend); + } + } + } + + let verification_key = state.master_verification_key(Some(epoch_id)).await?; + + // perform actual crypto verification + if credential_data.verify(&verification_key).is_err() { + return reject_ticket(EcashTicketVerificationRejection::InvalidTicket); + } + + // finally get EXCLUSIVE lock on the bloomfilter, check if for the final time and insert the SN + let was_present = state + .update_bloomfilter(sn, spend_date, today_ecash) + .await?; + if was_present { + return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); + } + + //store credential + state + .store_verified_ticket(credential_data, gateway_cosmos_addr) + .await?; + + Ok(Json(EcashTicketVerificationResponse { verified: Ok(()) })) +} + +// // for particular SN returns what gateway has submitted it and whether it has been verified correctly +// pub async fn credential_status() -> ! { +// todo!() +// } + +#[openapi(tag = "Ecash")] +#[post( + "/batch-redeem-ecash-tickets", + data = "" +)] +pub async fn batch_redeem_tickets( + // TODO in the future: make it send binary data rather than json + batch_redeem_credentials_body: Json, + state: &RocketState, +) -> crate::ecash::error::Result> { + // 1. see if that gateway has even submitted any tickets + let Some(provider_info) = state + .get_ticket_provider(batch_redeem_credentials_body.gateway_cosmos_addr.as_ref()) + .await? + else { + return Err(EcashError::NotTicketsProvided); + }; + + // 2. check if the gateway is not trying to spam the redemption requests + // (we have to protect our poor chain) + if let Some(last_redemption) = provider_info.last_batch_verification { + let now = OffsetDateTime::now_utc(); + let next_allowed = last_redemption + MIN_BATCH_REDEMPTION_DELAY; + + if next_allowed > now { + return Err(EcashError::TooFrequentRedemption { + last_redemption, + next_allowed, + }); + } + } + + // 3. verify the request digest + if !batch_redeem_credentials_body.verify_digest() { + return Err(EcashError::MismatchedRequestDigest); + } + + // 4. verify the associated on-chain proposal (whether it's made by correct sender, has valid messages, etc.) + state + .validate_redemption_proposal(&batch_redeem_credentials_body) + .await?; + + let proposal_id = batch_redeem_credentials_body.proposal_id; + let received = batch_redeem_credentials_body + .into_inner() + .included_serial_numbers; + + // 5. check if **every** serial number included in the request has been verified by us + // if we have more than requested, tough luck, they're going to lose them + let verified = state.get_redeemable_tickets(provider_info).await?; + let verified_tickets: HashSet<_> = verified.iter().map(|sn| sn.deref()).collect(); + + for sn in &received { + if !verified_tickets.contains(sn.deref()) { + return Err(EcashError::TicketNotVerified { + serial_number_bs58: bs58::encode(sn).into_string(), + }); + } + } + + // TODO: offload it to separate task with work queue and batching (of tx messages) to vote for multiple proposals in the same tx + state.accept_proposal(proposal_id).await?; + Ok(Json(EcashBatchTicketRedemptionResponse { + proposal_accepted: true, + })) +} + +// explicitly mark it as v1 in the URL because the response type WILL change; +// it will probably be compressed bincode or something +#[openapi(tag = "Ecash")] +#[get("/double-spending-filter-v1")] +pub async fn double_spending_filter_v1( + state: &RocketState, +) -> crate::ecash::error::Result> { + let spent_credentials_export = state.get_bloomfilter_bytes().await; + Ok(Json(SpentCredentialsResponse::new( + spent_credentials_export, + ))) +} diff --git a/nym-api/src/coconut/client.rs b/nym-api/src/ecash/client.rs similarity index 85% rename from nym-api/src/coconut/client.rs rename to nym-api/src/ecash/client.rs index d009989996..e0970f2155 100644 --- a/nym-api/src/coconut/client.rs +++ b/nym-api/src/ecash/client.rs @@ -1,10 +1,9 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::error::Result; +use crate::ecash::error::Result; use cw3::{ProposalResponse, VoteResponse}; use cw4::MemberResponse; -use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; use nym_coconut_dkg_common::dealer::{ DealerDetails, DealerDetailsResponse, RegisteredDealerDetails, }; @@ -19,8 +18,11 @@ use nym_coconut_dkg_common::types::{ use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; use nym_contracts_common::IdentityKey; use nym_dkg::Threshold; +use nym_ecash_contract_common::blacklist::BlacklistedAccountResponse; +use nym_ecash_contract_common::deposit::{DepositId, DepositResponse}; use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult; -use nym_validator_client::nyxd::{AccountId, Fee, Hash, TxResponse}; +use nym_validator_client::nyxd::{AccountId, Fee}; +use nym_validator_client::EcashApiClient; #[async_trait] pub trait Client { @@ -28,9 +30,7 @@ pub trait Client { async fn dkg_contract_address(&self) -> Result; - async fn bandwidth_contract_admin(&self) -> Result>; - - async fn get_tx(&self, tx_hash: Hash) -> Result; + async fn get_deposit(&self, deposit_id: DepositId) -> Result; async fn get_proposal(&self, proposal_id: u64) -> Result; @@ -38,10 +38,10 @@ pub trait Client { async fn get_vote(&self, proposal_id: u64, voter: String) -> Result; - async fn get_spent_credential( + async fn get_blacklisted_account( &self, - blinded_serial_number: String, - ) -> Result; + public_key: String, + ) -> Result; async fn contract_state(&self) -> Result; @@ -50,6 +50,7 @@ pub trait Client { async fn group_member(&self, addr: String) -> Result; async fn get_current_epoch_threshold(&self) -> Result>; + async fn get_epoch_threshold(&self, epoch_id: EpochId) -> Result>; async fn get_self_registered_dealer_details(&self) -> Result; @@ -99,6 +100,8 @@ pub trait Client { async fn get_verification_key_shares(&self, epoch_id: EpochId) -> Result>; + async fn get_registered_ecash_clients(&self, epoch_id: EpochId) -> Result>; + async fn vote_proposal(&self, proposal_id: u64, vote_yes: bool, fee: Option) -> Result<()>; diff --git a/nym-api/src/ecash/comm.rs b/nym-api/src/ecash/comm.rs new file mode 100644 index 0000000000..20941c484c --- /dev/null +++ b/nym-api/src/ecash/comm.rs @@ -0,0 +1,147 @@ +// Copyright 2022-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::client::Client; +use crate::ecash::error::{EcashError, Result}; +use crate::ecash::helpers::CachedImmutableEpochItem; +use crate::{ecash, nyxd}; +use nym_coconut_dkg_common::types::{Epoch, EpochId}; +use nym_dkg::Threshold; +use nym_validator_client::EcashApiClient; +use std::cmp::min; +use time::OffsetDateTime; +use tokio::sync::{RwLock, RwLockWriteGuard}; + +#[async_trait] +pub trait APICommunicationChannel { + async fn current_epoch(&self) -> Result; + + async fn ecash_clients(&self, epoch_id: EpochId) -> Result>; + + async fn ecash_threshold(&self, epoch_id: EpochId) -> Result; + + async fn dkg_in_progress(&self) -> Result; +} + +struct CachedEpoch { + valid_until: OffsetDateTime, + current_epoch: Epoch, +} + +impl Default for CachedEpoch { + fn default() -> Self { + CachedEpoch { + valid_until: OffsetDateTime::UNIX_EPOCH, + current_epoch: Epoch::default(), + } + } +} + +impl CachedEpoch { + fn is_valid(&self) -> bool { + self.valid_until > OffsetDateTime::now_utc() + } + + async fn update(&mut self, epoch: Epoch) -> Result<()> { + let now = OffsetDateTime::now_utc(); + + let validity_duration = if let Some(epoch_finish) = epoch.deadline { + let state_end = + OffsetDateTime::from_unix_timestamp(epoch_finish.seconds() as i64).unwrap(); + let until_epoch_state_end = state_end - now; + // make it valid until the next epoch transition or next 5min, whichever is smaller + min(until_epoch_state_end, 5 * time::Duration::MINUTE) + } else { + 5 * time::Duration::MINUTE + }; + + self.valid_until = now + validity_duration; + self.current_epoch = epoch; + + Ok(()) + } +} + +pub(crate) struct QueryCommunicationChannel { + nyxd_client: nyxd::Client, + + epoch_clients: CachedImmutableEpochItem>, + cached_epoch: RwLock, + threshold_values: CachedImmutableEpochItem, +} + +impl QueryCommunicationChannel { + pub fn new(nyxd_client: nyxd::Client) -> Self { + QueryCommunicationChannel { + nyxd_client, + epoch_clients: Default::default(), + cached_epoch: Default::default(), + threshold_values: Default::default(), + } + } + + async fn update_epoch_cache(&self) -> Result> { + let mut guard = self.cached_epoch.write().await; + + let epoch = ecash::client::Client::get_current_epoch(&self.nyxd_client).await?; + + guard.update(epoch).await?; + Ok(guard) + } +} + +#[async_trait] +impl APICommunicationChannel for QueryCommunicationChannel { + async fn current_epoch(&self) -> Result { + let guard = self.cached_epoch.read().await; + if guard.is_valid() { + return Ok(guard.current_epoch.epoch_id); + } + + // update cache + drop(guard); + let guard = self.update_epoch_cache().await?; + + return Ok(guard.current_epoch.epoch_id); + } + + // TODO: perhaps this should be returning a ReadGuard instead? + async fn ecash_clients(&self, epoch_id: EpochId) -> Result> { + self.epoch_clients + .get_or_init(epoch_id, || async { + self.nyxd_client + .get_registered_ecash_clients(epoch_id) + .await + }) + .await + .map(|guard| guard.clone()) + } + + async fn ecash_threshold(&self, epoch_id: EpochId) -> Result { + self.threshold_values + .get_or_init(epoch_id, || async { + if let Some(threshold) = + ecash::client::Client::get_epoch_threshold(&self.nyxd_client, epoch_id).await? + { + Ok(threshold) + } else { + Err(EcashError::UnavailableThreshold { epoch_id }) + } + }) + .await + .map(|t| *t) + } + + async fn dkg_in_progress(&self) -> Result { + let guard = self.cached_epoch.read().await; + if guard.is_valid() { + return Ok(!guard.current_epoch.state.is_in_progress()); + } + + // update cache + drop(guard); + let guard = self.update_epoch_cache().await?; + + return Ok(!guard.current_epoch.state.is_in_progress()); + } +} diff --git a/nym-api/src/ecash/deposit.rs b/nym-api/src/ecash/deposit.rs new file mode 100644 index 0000000000..9118e9d3af --- /dev/null +++ b/nym-api/src/ecash/deposit.rs @@ -0,0 +1,109 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::error::Result; +use nym_api_requests::ecash::BlindSignRequestBody; +use nym_credentials::IssuanceTicketBook; +use nym_crypto::asymmetric::ed25519; +use nym_ecash_contract_common::deposit::Deposit; + +pub async fn validate_deposit(request: &BlindSignRequestBody, deposit: Deposit) -> Result<()> { + // verify signature with the pubkey used in deposit + let ed25519 = ed25519::PublicKey::from_base58_string(deposit.bs58_encoded_ed25519_pubkey)?; + let plaintext = + IssuanceTicketBook::request_plaintext(&request.inner_sign_request, request.deposit_id); + ed25519.verify(plaintext, &request.signature)?; + + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::ecash::error::EcashError; + use crate::ecash::tests::voucher_fixture; + use nym_compact_ecash::{generate_keypair_user, scheme::withdrawal::WithdrawalRequest}; + use nym_credentials_interface::TicketType; + use rand::rngs::OsRng; + use time::macros::date; + + #[tokio::test] + async fn validate_deposit_test() { + let mut rng = OsRng; + let deposit_id = 42; + let voucher = voucher_fixture(Some(deposit_id)); + let signing_data = voucher.prepare_for_signing(); + let correct_request = voucher.create_blind_sign_request_body(&signing_data); + + let valid_ed25519 = ed25519::KeyPair::new(&mut rng); + let bs58_encoded_ed25519 = valid_ed25519.public_key().to_base58_string(); + + let malformed_deposit = Deposit { + bs58_encoded_ed25519_pubkey: "invalided25519pubkey".to_string(), + }; + + let err = validate_deposit(&correct_request, malformed_deposit) + .await + .unwrap_err(); + + assert!(matches!( + err, + EcashError::Ed25519ParseError( + nym_crypto::asymmetric::identity::Ed25519RecoveryError::MalformedPublicKeyString { .. } + ) + )); + + let wrong_deposit = Deposit { + bs58_encoded_ed25519_pubkey: bs58_encoded_ed25519, + }; + + let err = validate_deposit(&correct_request, wrong_deposit) + .await + .unwrap_err(); + assert!(matches!(err, EcashError::SignatureVerificationError(..))); + + //hard-coded values, that generate a correct signature + let blind_sign_req = WithdrawalRequest::from_bytes(&[ + 48, 168, 84, 109, 206, 194, 237, 227, 205, 67, 60, 127, 85, 54, 246, 120, 88, 129, 121, + 50, 255, 133, 50, 54, 155, 172, 179, 52, 16, 250, 6, 209, 67, 54, 251, 20, 37, 124, + 115, 63, 182, 101, 188, 68, 149, 18, 149, 57, 167, 48, 149, 100, 41, 48, 143, 115, 93, + 90, 244, 164, 161, 224, 65, 160, 63, 141, 65, 86, 128, 136, 128, 194, 40, 106, 158, 40, + 235, 242, 51, 108, 3, 109, 120, 11, 100, 82, 188, 61, 41, 12, 232, 54, 162, 243, 43, + 222, 215, 216, 2, 48, 137, 243, 126, 118, 124, 83, 221, 53, 252, 163, 175, 215, 94, 90, + 249, 172, 3, 222, 13, 45, 166, 245, 126, 173, 199, 89, 206, 11, 22, 204, 47, 26, 40, + 191, 217, 139, 75, 101, 45, 5, 62, 251, 52, 36, 117, 101, 166, 63, 48, 152, 195, 163, + 179, 117, 148, 93, 223, 210, 119, 105, 59, 71, 88, 155, 17, 33, 4, 87, 203, 169, 40, + 93, 203, 153, 213, 105, 107, 181, 214, 2, 25, 19, 187, 217, 243, 246, 185, 152, 81, + 118, 11, 169, 100, 74, 88, 215, 37, 32, 31, 123, 5, 222, 103, 255, 236, 74, 37, 222, + 170, 136, 5, 49, 4, 183, 156, 223, 33, 112, 122, 81, 122, 221, 166, 27, 5, 44, 153, 37, + 229, 107, 32, 95, 45, 147, 187, 40, 141, 22, 9, 222, 232, 125, 34, 52, 152, 157, 14, + 228, 200, 183, 29, 62, 24, 201, 228, 103, 119, 89, 186, 79, 116, 75, 53, 2, 32, 219, + 72, 52, 255, 108, 74, 76, 126, 233, 46, 34, 70, 188, 47, 57, 66, 153, 14, 6, 242, 112, + 129, 108, 166, 188, 226, 183, 51, 45, 195, 190, 58, 32, 170, 54, 18, 64, 215, 82, 118, + 243, 66, 186, 137, 175, 230, 172, 174, 226, 104, 188, 123, 239, 77, 180, 32, 225, 73, + 208, 255, 27, 195, 181, 201, 21, 2, 32, 34, 231, 200, 93, 64, 117, 244, 169, 58, 64, + 39, 5, 228, 205, 119, 135, 221, 130, 241, 205, 184, 182, 34, 248, 85, 26, 241, 233, 52, + 244, 17, 15, 32, 157, 211, 145, 238, 16, 101, 55, 132, 233, 11, 249, 129, 41, 226, 250, + 146, 160, 155, 154, 81, 241, 129, 154, 24, 221, 196, 54, 210, 16, 24, 116, 31, + ]) + .unwrap(); + let expiration_date = date!(2024 - 02 - 19); + let ecash_keypair = generate_keypair_user(); + + let correct_request = BlindSignRequestBody::new( + blind_sign_req, + deposit_id, + "3MpHDLYMCmuMvZ9zkZXPkTK6nKArvQW3dJA1notoPPxnbBW2ommkR2dkpRWoeWSkUjQSLv1nRyiRzMWbobGLw1eh".parse().unwrap(), + ecash_keypair.public_key(), + expiration_date, + TicketType::V1MixnetEntry, + ); + + let good_deposit = Deposit { + bs58_encoded_ed25519_pubkey: "JDTnyotGw3TtbohEamWNjhvGpj3tJz2C4X2Au9PrSTEx".to_string(), + }; + + let res = validate_deposit(&correct_request, good_deposit).await; + assert!(res.is_ok()) + } +} diff --git a/nym-api/src/coconut/dkg/client.rs b/nym-api/src/ecash/dkg/client.rs similarity index 77% rename from nym-api/src/coconut/dkg/client.rs rename to nym-api/src/ecash/dkg/client.rs index f85902ba48..5c8f993690 100644 --- a/nym-api/src/coconut/dkg/client.rs +++ b/nym-api/src/ecash/dkg/client.rs @@ -1,8 +1,8 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::client::Client; -use crate::coconut::error::CoconutError; +use crate::ecash::client::Client; +use crate::ecash::error::EcashError; use cw3::{ProposalResponse, Status, VoteResponse}; use cw4::MemberResponse; use nym_coconut_dkg_common::dealer::{DealerDetails, DealerDetailsResponse}; @@ -16,8 +16,9 @@ use nym_coconut_dkg_common::types::{ use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; use nym_contracts_common::IdentityKey; use nym_dkg::Threshold; -use nym_validator_client::nyxd::cosmwasm_client::logs::{find_attribute, NODE_INDEX}; +use nym_validator_client::nyxd::cosmwasm_client::logs::NODE_INDEX; use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult; +use nym_validator_client::nyxd::helpers::find_attribute_value_in_logs_or_events; use nym_validator_client::nyxd::AccountId; pub(crate) struct DkgClient { @@ -38,19 +39,19 @@ impl DkgClient { self.inner.address().await } - pub(crate) async fn dkg_contract_address(&self) -> Result { + pub(crate) async fn dkg_contract_address(&self) -> Result { self.inner.dkg_contract_address().await } - pub(crate) async fn get_current_epoch(&self) -> Result { + pub(crate) async fn get_current_epoch(&self) -> Result { self.inner.get_current_epoch().await } - pub(crate) async fn get_contract_state(&self) -> Result { + pub(crate) async fn get_contract_state(&self) -> Result { self.inner.contract_state().await } - pub(crate) async fn group_member(&self) -> Result { + pub(crate) async fn group_member(&self) -> Result { self.inner .group_member(self.get_address().await.to_string()) .await @@ -58,13 +59,13 @@ impl DkgClient { pub(crate) async fn get_current_epoch_threshold( &self, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { self.inner.get_current_epoch_threshold().await } pub(crate) async fn get_self_registered_dealer_details( &self, - ) -> Result { + ) -> Result { self.inner.get_self_registered_dealer_details().await } @@ -72,14 +73,14 @@ impl DkgClient { &self, epoch_id: EpochId, dealer: String, - ) -> Result { + ) -> Result { self.inner .get_registered_dealer_details(epoch_id, dealer) .await .map(|d| d.details.is_some()) } - pub(crate) async fn get_current_dealers(&self) -> Result, CoconutError> { + pub(crate) async fn get_current_dealers(&self) -> Result, EcashError> { self.inner.get_current_dealers().await } @@ -87,7 +88,7 @@ impl DkgClient { &self, epoch_id: EpochId, dealer: String, - ) -> Result { + ) -> Result { self.inner .get_dealer_dealings_status(epoch_id, dealer) .await @@ -99,11 +100,11 @@ impl DkgClient { dealer: &str, dealing_index: DealingIndex, chunk_index: ChunkIndex, - ) -> Result { + ) -> Result { self.inner .get_dealing_chunk(epoch_id, dealer, dealing_index, chunk_index) .await? - .ok_or(CoconutError::MissingDealingChunk { + .ok_or(EcashError::MissingDealingChunk { epoch_id, dealer: dealer.to_string(), dealing_index, @@ -115,7 +116,7 @@ impl DkgClient { &self, epoch_id: EpochId, address: S, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { self.inner .get_verification_key_share(epoch_id, address.into()) .await @@ -124,7 +125,7 @@ impl DkgClient { pub(crate) async fn get_verification_own_key_share( &self, epoch_id: EpochId, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { let address = self.inner.address().await; self.get_verification_key_share(epoch_id, address).await } @@ -132,31 +133,28 @@ impl DkgClient { pub(crate) async fn get_verification_key_shares( &self, epoch_id: EpochId, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { self.inner.get_verification_key_shares(epoch_id).await } - pub(crate) async fn get_vote(&self, proposal_id: u64) -> Result { + pub(crate) async fn get_vote(&self, proposal_id: u64) -> Result { let address = self.get_address().await.to_string(); self.inner.get_vote(proposal_id, address).await } - pub(crate) async fn list_proposals(&self) -> Result, CoconutError> { + pub(crate) async fn list_proposals(&self) -> Result, EcashError> { self.inner.list_proposals().await } - pub(crate) async fn get_proposal_status( - &self, - proposal_id: u64, - ) -> Result { + pub(crate) async fn get_proposal_status(&self, proposal_id: u64) -> Result { self.inner.get_proposal(proposal_id).await.map(|p| p.status) } - pub(crate) async fn advance_epoch_state(&self) -> Result<(), CoconutError> { + pub(crate) async fn advance_epoch_state(&self) -> Result<(), EcashError> { self.inner.advance_epoch_state().await } - pub(crate) async fn can_advance_epoch_state(&self) -> Result { + pub(crate) async fn can_advance_epoch_state(&self) -> Result { self.inner.can_advance_epoch_state().await } @@ -166,20 +164,20 @@ impl DkgClient { identity_key: IdentityKey, announce_address: String, resharing: bool, - ) -> Result { + ) -> Result { let res = self .inner .register_dealer(bte_key, identity_key, announce_address, resharing) .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"), - })?; + let node_index = + find_attribute_value_in_logs_or_events(&res.logs, &res.events, "wasm", NODE_INDEX) + .ok_or(EcashError::NodeIndexRecoveryError { + reason: String::from("node index not found"), + })? + .parse::() + .map_err(|_| EcashError::NodeIndexRecoveryError { + reason: String::from("node index could not be parsed"), + })?; Ok(node_index) } @@ -189,7 +187,7 @@ impl DkgClient { dealing_index: DealingIndex, chunks: Vec, resharing: bool, - ) -> Result<(), CoconutError> { + ) -> Result<(), EcashError> { self.inner .submit_dealing_metadata(dealing_index, chunks, resharing) .await?; @@ -199,7 +197,7 @@ impl DkgClient { pub(crate) async fn submit_dealing_chunk( &self, chunk: PartialContractDealing, - ) -> Result<(), CoconutError> { + ) -> Result<(), EcashError> { self.inner.submit_dealing_chunk(chunk).await?; Ok(()) } @@ -208,7 +206,7 @@ impl DkgClient { &self, share: VerificationKeyShare, resharing: bool, - ) -> Result { + ) -> Result { self.inner .submit_verification_key_share(share.clone(), resharing) .await @@ -218,14 +216,14 @@ impl DkgClient { &self, proposal_id: u64, vote_yes: bool, - ) -> Result<(), CoconutError> { + ) -> Result<(), EcashError> { self.inner.vote_proposal(proposal_id, vote_yes, None).await } pub(crate) async fn execute_verification_key_share( &self, proposal_id: u64, - ) -> Result<(), CoconutError> { + ) -> Result<(), EcashError> { self.inner.execute_proposal(proposal_id).await } } diff --git a/nym-api/src/coconut/dkg/controller/error.rs b/nym-api/src/ecash/dkg/controller/error.rs similarity index 79% rename from nym-api/src/coconut/dkg/controller/error.rs rename to nym-api/src/ecash/dkg/controller/error.rs index e634477db6..4311ff7db5 100644 --- a/nym-api/src/coconut/dkg/controller/error.rs +++ b/nym-api/src/ecash/dkg/controller/error.rs @@ -1,12 +1,12 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::dealing::DealingGenerationError; -use crate::coconut::dkg::key_derivation::KeyDerivationError; -use crate::coconut::dkg::key_finalization::KeyFinalizationError; -use crate::coconut::dkg::key_validation::KeyValidationError; -use crate::coconut::dkg::public_key::PublicKeySubmissionError; -use crate::coconut::error::CoconutError; +use crate::ecash::dkg::dealing::DealingGenerationError; +use crate::ecash::dkg::key_derivation::KeyDerivationError; +use crate::ecash::dkg::key_finalization::KeyFinalizationError; +use crate::ecash::dkg::key_validation::KeyValidationError; +use crate::ecash::dkg::public_key::PublicKeySubmissionError; +use crate::ecash::error::EcashError; use std::path::PathBuf; use thiserror::Error; @@ -16,25 +16,25 @@ pub enum DkgError { StatePersistenceFailure { path: PathBuf, #[source] - source: CoconutError, + source: EcashError, }, #[error("failed to query for the current DKG epoch state: {source}")] EpochQueryFailure { #[source] - source: CoconutError, + source: EcashError, }, #[error("failed to query for the epoch state status: {source}")] StateStatusQueryFailure { #[source] - source: CoconutError, + source: EcashError, }, #[error("failed to query the CW4 group contract for the membership status: {source}")] GroupQueryFailure { #[source] - source: CoconutError, + source: EcashError, }, #[error("this API is currently not member of the DKG group and thus can't participate in the process")] @@ -73,6 +73,6 @@ pub enum DkgError { #[error("failed to advance the DKG state: {source}")] StateAdvancementFailure { #[source] - source: CoconutError, + source: EcashError, }, } diff --git a/nym-api/src/coconut/dkg/controller/keys.rs b/nym-api/src/ecash/dkg/controller/keys.rs similarity index 79% rename from nym-api/src/coconut/dkg/controller/keys.rs rename to nym-api/src/ecash/dkg/controller/keys.rs index ce0d16930a..6b806bf792 100644 --- a/nym-api/src/coconut/dkg/controller/keys.rs +++ b/nym-api/src/ecash/dkg/controller/keys.rs @@ -1,8 +1,8 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::client::Client; -use crate::coconut::keys::KeyPairWithEpoch; +use crate::ecash::client::Client; +use crate::ecash::keys::{KeyPairWithEpoch, LegacyCoconutKeyWithEpoch}; use crate::support::{config, nyxd}; use anyhow::{anyhow, bail, Context}; use nym_coconut_dkg_common::types::{EpochId, EpochState}; @@ -35,15 +35,32 @@ pub(crate) fn load_bte_keypair(config: &config::CoconutSigner) -> anyhow::Result .context("bte keypair load failure") } -pub(crate) fn load_coconut_keypair_if_exists( +pub(crate) fn load_ecash_keypair_if_exists( config: &config::CoconutSigner, ) -> anyhow::Result> { if !config.storage_paths.coconut_key_path.exists() { return Ok(None); } - nym_pemstore::load_key(&config.storage_paths.coconut_key_path) - .context("coconut key load failure") - .map(Some) + + // first attempt to load ecash keys directly, + // if that fails fallback to coconut keys and perform migration + if let Ok(ecash_key) = + nym_pemstore::load_key::(&config.storage_paths.coconut_key_path) + { + return Ok(Some(ecash_key)); + } + + if let Ok(legacy_coconut_key) = nym_pemstore::load_key::( + &config.storage_paths.coconut_key_path, + ) { + let migrated_key: KeyPairWithEpoch = legacy_coconut_key.into(); + nym_pemstore::store_key(&migrated_key, &config.storage_paths.coconut_key_path) + .context("migrated key storage failure")?; + + return Ok(Some(migrated_key)); + } + + bail!("ecash key load failure") } // the keys can be considered valid if they were generated for the current dkg epoch diff --git a/nym-api/src/coconut/dkg/controller/mod.rs b/nym-api/src/ecash/dkg/controller/mod.rs similarity index 97% rename from nym-api/src/coconut/dkg/controller/mod.rs rename to nym-api/src/ecash/dkg/controller/mod.rs index db9ce0a82c..6dd454f10d 100644 --- a/nym-api/src/coconut/dkg/controller/mod.rs +++ b/nym-api/src/ecash/dkg/controller/mod.rs @@ -1,10 +1,10 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::client::DkgClient; -use crate::coconut::dkg::controller::error::DkgError; -use crate::coconut::dkg::state::{PersistentState, State}; -use crate::coconut::keys::KeyPair as CoconutKeyPair; +use crate::ecash::dkg::client::DkgClient; +use crate::ecash::dkg::controller::error::DkgError; +use crate::ecash::dkg::state::{PersistentState, State}; +use crate::ecash::keys::KeyPair as CoconutKeyPair; use crate::nyxd; use crate::support::config; use anyhow::{bail, Result}; @@ -340,7 +340,7 @@ impl DkgController { dkg_client, coconut_key_path: Default::default(), state, - rng: crate::coconut::tests::fixtures::test_rng([1u8; 32]), + rng: crate::ecash::tests::fixtures::test_rng([1u8; 32]), polling_rate: Default::default(), } } diff --git a/nym-api/src/coconut/dkg/dealing.rs b/nym-api/src/ecash/dkg/dealing.rs similarity index 97% rename from nym-api/src/coconut/dkg/dealing.rs rename to nym-api/src/ecash/dkg/dealing.rs index 21b5803854..bb8c17d92b 100644 --- a/nym-api/src/coconut/dkg/dealing.rs +++ b/nym-api/src/ecash/dkg/dealing.rs @@ -1,11 +1,11 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg; -use crate::coconut::dkg::controller::keys::archive_coconut_keypair; -use crate::coconut::dkg::controller::DkgController; -use crate::coconut::error::CoconutError; -use crate::coconut::keys::KeyPairWithEpoch; +use crate::ecash::dkg; +use crate::ecash::dkg::controller::keys::archive_coconut_keypair; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::error::EcashError; +use crate::ecash::keys::KeyPairWithEpoch; use log::debug; use nym_coconut_dkg_common::dealing::{chunk_dealing, DealingChunkInfo, MAX_DEALING_CHUNK_SIZE}; use nym_coconut_dkg_common::types::{DealingIndex, EpochId}; @@ -39,7 +39,7 @@ impl Debug for DealingGeneration { #[derive(Debug, Error)] pub enum DealingGenerationError { #[error(transparent)] - CoconutError(#[from] CoconutError), + CoconutError(#[from] EcashError), #[error("can't complete dealing exchange without registering public keys")] IncompletePublicKeyRegistration, @@ -70,7 +70,7 @@ impl DkgController { spec: DealingGeneration, ) -> Result, DealingGenerationError> { let threshold = self.dkg_client.get_current_epoch_threshold().await?.ok_or( - CoconutError::UnrecoverableState { + EcashError::UnrecoverableState { reason: String::from("Threshold should have been set"), }, )?; @@ -79,7 +79,7 @@ impl DkgController { .state .registration_state(epoch_id)? .assigned_index - .ok_or(CoconutError::UnrecoverableState { + .ok_or(EcashError::UnrecoverableState { reason: String::from("Node index should have been set"), })?; @@ -469,14 +469,12 @@ impl DkgController { #[cfg(test)] pub(crate) mod tests { use super::*; - use crate::coconut::dkg::state::registration::KeyRejectionReason; - use crate::coconut::keys::KeyPair; - use crate::coconut::tests::fixtures::{ - dealers_fixtures, test_rng, TestingDkgControllerBuilder, - }; - use crate::coconut::tests::helpers::unchecked_decode_bte_key; - use nym_coconut::{ttp_keygen, Parameters}; + use crate::ecash::dkg::state::registration::KeyRejectionReason; + use crate::ecash::keys::KeyPair; + use crate::ecash::tests::fixtures::{dealers_fixtures, test_rng, TestingDkgControllerBuilder}; + use crate::ecash::tests::helpers::unchecked_decode_bte_key; use nym_coconut_dkg_common::types::DealerRegistrationDetails; + use nym_compact_ecash::ttp_keygen; use nym_dkg::bte::PublicKeyWithProof; #[tokio::test] @@ -619,7 +617,7 @@ pub(crate) mod tests { let epoch = 1; - let mut keys = ttp_keygen(&Parameters::new(4).unwrap(), 3, 4).unwrap(); + let mut keys = ttp_keygen(3, 4).unwrap(); let coconut_keypair = KeyPair::new(); coconut_keypair .set(KeyPairWithEpoch::new(keys.pop().unwrap(), epoch)) @@ -676,7 +674,7 @@ pub(crate) mod tests { let epoch = 1; - let mut keys = ttp_keygen(&Parameters::new(4).unwrap(), 3, 4).unwrap(); + let mut keys = ttp_keygen(3, 4).unwrap(); let coconut_keypair = KeyPair::new(); coconut_keypair .set(KeyPairWithEpoch::new(keys.pop().unwrap(), epoch - 1)) diff --git a/nym-api/src/coconut/dkg/helpers.rs b/nym-api/src/ecash/dkg/helpers.rs similarity index 94% rename from nym-api/src/coconut/dkg/helpers.rs rename to nym-api/src/ecash/dkg/helpers.rs index e36c1c04fc..d57e7962b0 100644 --- a/nym-api/src/coconut/dkg/helpers.rs +++ b/nym-api/src/ecash/dkg/helpers.rs @@ -1,8 +1,8 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::controller::DkgController; -use crate::coconut::error::CoconutError; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::error::EcashError; use cw3::{ProposalResponse, Status}; use nym_coconut_dkg_common::verification_key::owner_from_cosmos_msgs; use nym_validator_client::nyxd::AccountId; @@ -29,7 +29,7 @@ impl DkgController { pub(crate) async fn get_validation_proposals( &self, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { let dkg_contract = self.dkg_client.dkg_contract_address().await?; // FUTURE OPTIMIZATION: don't query for ALL proposals. say if we're in epoch 50, diff --git a/nym-api/src/coconut/dkg/key_derivation.rs b/nym-api/src/ecash/dkg/key_derivation.rs similarity index 94% rename from nym-api/src/coconut/dkg/key_derivation.rs rename to nym-api/src/ecash/dkg/key_derivation.rs index eb6c5b7214..23291806b8 100644 --- a/nym-api/src/coconut/dkg/key_derivation.rs +++ b/nym-api/src/ecash/dkg/key_derivation.rs @@ -1,25 +1,26 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg; -use crate::coconut::dkg::controller::keys::persist_coconut_keypair; -use crate::coconut::dkg::controller::DkgController; -use crate::coconut::dkg::state::key_derivation::{DealerRejectionReason, DerivationFailure}; -use crate::coconut::error::CoconutError; -use crate::coconut::keys::KeyPairWithEpoch; -use crate::coconut::state::bandwidth_credential_params; +use crate::ecash::dkg; +use crate::ecash::dkg::controller::keys::persist_coconut_keypair; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::dkg::state::key_derivation::{DealerRejectionReason, DerivationFailure}; +use crate::ecash::error::EcashError; +use crate::ecash::keys::KeyPairWithEpoch; use cosmwasm_std::Addr; use log::debug; -use nym_coconut::KeyPair as CoconutKeyPair; -use nym_coconut::{check_vk_pairing, Base58, SecretKey, VerificationKey}; use nym_coconut_dkg_common::event_attributes::DKG_PROPOSAL_ID; use nym_coconut_dkg_common::types::{DealingIndex, EpochId, NodeIndex}; +use nym_compact_ecash::scheme::keygen::SecretKeyAuth; +use nym_compact_ecash::utils::check_vk_pairing; +use nym_compact_ecash::{ecash_group_parameters, Base58, KeyPairAuth, VerificationKeyAuth}; use nym_dkg::{ bte::{self, decrypt_share}, combine_shares, try_recover_verification_keys, Dealing, }; -use nym_validator_client::nyxd::cosmwasm_client::logs::{find_attribute, Log}; -use nym_validator_client::nyxd::Hash; +use nym_validator_client::nyxd::cosmwasm_client::logs::Log; +use nym_validator_client::nyxd::helpers::find_attribute_value_in_logs_or_events; +use nym_validator_client::nyxd::{Event, Hash}; use rand::{CryptoRng, RngCore}; use std::collections::{BTreeMap, HashMap}; use std::ops::Deref; @@ -28,7 +29,7 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum KeyDerivationError { #[error(transparent)] - CoconutError(#[from] CoconutError), + CoconutError(#[from] EcashError), #[error("can't complete key derivation without dealing exchange")] IncompleteDealingExchange, @@ -75,7 +76,7 @@ impl DkgController { dealer: &Addr, epoch_receivers: &BTreeMap, raw_dealings: HashMap>, - prior_public_key: Option, + prior_public_key: Option, ) -> Result, DealerRejectionReason>, KeyDerivationError> { let threshold = self.state.threshold(epoch_id)?; @@ -155,7 +156,7 @@ impl DkgController { &self, epoch_id: EpochId, dealer: &Addr, - ) -> Result, KeyDerivationError> { + ) -> Result, KeyDerivationError> { let Some(previous_epoch) = epoch_id.checked_sub(1) else { return Err(KeyDerivationError::ZerothEpochResharing); }; @@ -176,7 +177,7 @@ impl DkgController { // since this share appears as 'verified' on the chain, it means the consensus of dealers confirmed its validity // and thus they must have been able to parse it, so the unwrap/expect here is fine Ok(Some( - VerificationKey::try_from_bs58(&share.share) + VerificationKeyAuth::try_from_bs58(&share.share) .expect("failed to deserialize VERIFIED key"), )) } @@ -424,8 +425,8 @@ impl DkgController { // SAFETY: // we know we had a non-empty map of dealings and thus, at the very least, we must have derived a single secret // (i.e. the x-element) - let sk = SecretKey::create_from_raw(derived_x.unwrap(), derived_secrets); - let derived_vk = sk.verification_key(bandwidth_credential_params()); + let sk = SecretKeyAuth::create_from_raw(derived_x.unwrap(), derived_secrets); + let derived_vk = sk.verification_key(); // make the key we derived out of the decrypted shares matches the partial key // (cryptographically there shouldn't be any reason for the mismatch, @@ -436,42 +437,42 @@ impl DkgController { .derived_partials_for(receiver_index) .ok_or(KeyDerivationError::NoSelfPartialKey { receiver_index })?; - if !check_vk_pairing(bandwidth_credential_params(), &derived_partial, &derived_vk) { + if !check_vk_pairing(ecash_group_parameters(), &derived_partial, &derived_vk) { // can't do anything, we got all dealings, we derived all keys, but somehow they don't match error!("our derived key does not match the expected partials!"); return Ok(Err(DerivationFailure::MismatchedPartialKey)); } Ok(Ok(KeyPairWithEpoch::new( - CoconutKeyPair::from_keys(sk, derived_vk), + KeyPairAuth::from_keys(sk, derived_vk), epoch_id, ))) } async fn submit_partial_verification_key( &self, - key: &VerificationKey, + key: &VerificationKeyAuth, resharing: bool, ) -> Result { - fn extract_proposal_id_from_logs( + fn extract_proposal_id( logs: &[Log], + events: &[Event], tx_hash: Hash, ) -> Result { let event_type = "wasm"; let attribute_key = DKG_PROPOSAL_ID; - let proposal_attribute = find_attribute(logs, event_type, attribute_key).ok_or( - KeyDerivationError::MissingProposalIdAttribute { - tx_hash, - event_type: event_type.to_string(), - attribute_key: attribute_key.to_string(), - }, - )?; + let proposal_value = + find_attribute_value_in_logs_or_events(logs, events, event_type, attribute_key) + .ok_or(KeyDerivationError::MissingProposalIdAttribute { + tx_hash, + event_type: event_type.to_string(), + attribute_key: attribute_key.to_string(), + })?; - proposal_attribute - .value + proposal_value .parse() .map_err(|_| KeyDerivationError::UnparsableProposalId { - raw: proposal_attribute.value.clone(), + raw: proposal_value.clone(), }) } @@ -481,7 +482,7 @@ impl DkgController { .submit_verification_key_share(key.to_bs58(), resharing) .await?; let hash = res.transaction_hash; - let proposal_id = extract_proposal_id_from_logs(&res.logs, hash)?; + let proposal_id = extract_proposal_id(&res.logs, &res.events, hash)?; debug!("Submitted own verification key share, proposal id {proposal_id} is attached to it. tx hash: {hash}"); Ok(proposal_id) @@ -558,7 +559,7 @@ impl DkgController { debug!("we have already generated keys for this epoch but failed to send them to the contract"); let proposal_id = self - .submit_partial_verification_key(keys.keys.verification_key(), resharing) + .submit_partial_verification_key(&keys.keys.verification_key(), resharing) .await?; Ok(Some(proposal_id)) } else { @@ -657,7 +658,7 @@ impl DkgController { } let proposal_id = self - .submit_partial_verification_key(coconut_keypair.keys.verification_key(), resharing) + .submit_partial_verification_key(&coconut_keypair.keys.verification_key(), resharing) .await?; self.state.set_coconut_keypair(coconut_keypair).await; @@ -669,8 +670,8 @@ impl DkgController { // I've (@JS) only updated old, existing, tests. nothing more #[cfg(test)] pub(crate) mod tests { - use crate::coconut::dkg::state::key_derivation::DealerRejectionReason; - use crate::coconut::tests::helpers::{ + use crate::ecash::dkg::state::key_derivation::DealerRejectionReason; + use crate::ecash::tests::helpers::{ exchange_dealings, initialise_controllers, initialise_dkg, submit_public_keys, }; diff --git a/nym-api/src/coconut/dkg/key_finalization.rs b/nym-api/src/ecash/dkg/key_finalization.rs similarity index 96% rename from nym-api/src/coconut/dkg/key_finalization.rs rename to nym-api/src/ecash/dkg/key_finalization.rs index 628f04daa3..9ba5ff958a 100644 --- a/nym-api/src/coconut/dkg/key_finalization.rs +++ b/nym-api/src/ecash/dkg/key_finalization.rs @@ -1,8 +1,8 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::controller::DkgController; -use crate::coconut::error::CoconutError; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::error::EcashError; use cw3::Status; use nym_coconut_dkg_common::types::EpochId; use rand::{CryptoRng, RngCore}; @@ -11,7 +11,7 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum KeyFinalizationError { #[error(transparent)] - CoconutError(#[from] CoconutError), + CoconutError(#[from] EcashError), #[error("our proposal for key verification is still open (or is pending) (proposal id: {proposal_id}) ")] UnresolvedProposal { proposal_id: u64 }, @@ -90,7 +90,7 @@ impl DkgController { #[cfg(test)] mod tests { use super::*; - use crate::coconut::tests::helpers::{ + use crate::ecash::tests::helpers::{ derive_keypairs, exchange_dealings, initialise_controllers, initialise_dkg, submit_public_keys, validate_keys, }; diff --git a/nym-api/src/coconut/dkg/key_validation.rs b/nym-api/src/ecash/dkg/key_validation.rs similarity index 96% rename from nym-api/src/coconut/dkg/key_validation.rs rename to nym-api/src/ecash/dkg/key_validation.rs index ada4f72898..b99bacd962 100644 --- a/nym-api/src/coconut/dkg/key_validation.rs +++ b/nym-api/src/ecash/dkg/key_validation.rs @@ -1,14 +1,15 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::controller::DkgController; -use crate::coconut::error::CoconutError; -use crate::coconut::state::bandwidth_credential_params; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::error::EcashError; use cosmwasm_std::Addr; use cw3::Vote; -use nym_coconut::{check_vk_pairing, Base58, VerificationKey}; use nym_coconut_dkg_common::types::EpochId; use nym_coconut_dkg_common::verification_key::ContractVKShare; +use nym_compact_ecash::{ + ecash_group_parameters, utils::check_vk_pairing, Base58, VerificationKeyAuth, +}; use rand::{CryptoRng, RngCore}; use std::collections::HashMap; use thiserror::Error; @@ -24,7 +25,7 @@ fn vote_matches(voted_yes: bool, chain_vote: Vote) -> bool { #[derive(Debug, Error)] pub enum KeyValidationError { #[error(transparent)] - CoconutError(#[from] CoconutError), + CoconutError(#[from] EcashError), #[error("can't complete key validation without key derivation")] IncompleteKeyDerivation, @@ -45,7 +46,7 @@ pub enum ShareRejectionReason { epoch_id: EpochId, owner: Addr, #[source] - source: nym_coconut::CoconutError, + source: nym_compact_ecash::error::CompactEcashError, }, #[error("did not derive partial keys for {owner} at index {receiver_index} for epoch {epoch_id} during the dealings exchange")] @@ -95,7 +96,7 @@ impl DkgController { }; // attempt to recover the underlying key from its bs58 representation - let recovered_key = match VerificationKey::try_from_bs58(share.share) { + let recovered_key = match VerificationKeyAuth::try_from_bs58(share.share) { Ok(key) => key, Err(source) => { return reject(ShareRejectionReason::MalformedKeyEncoding { @@ -119,7 +120,7 @@ impl DkgController { }); }; - if !check_vk_pairing(bandwidth_credential_params(), &self_derived, &recovered_key) { + if !check_vk_pairing(ecash_group_parameters(), &self_derived, &recovered_key) { return reject(ShareRejectionReason::InconsistentKeys { epoch_id, owner, @@ -258,7 +259,7 @@ impl DkgController { // I've (@JS) only updated old, existing, tests. nothing more #[cfg(test)] mod tests { - use crate::coconut::tests::helpers::{ + use crate::ecash::tests::helpers::{ derive_keypairs, exchange_dealings, initialise_controllers, initialise_dkg, submit_public_keys, }; diff --git a/nym-api/src/coconut/dkg/mod.rs b/nym-api/src/ecash/dkg/mod.rs similarity index 97% rename from nym-api/src/coconut/dkg/mod.rs rename to nym-api/src/ecash/dkg/mod.rs index 46083a2d1a..f7272cd8d2 100644 --- a/nym-api/src/coconut/dkg/mod.rs +++ b/nym-api/src/ecash/dkg/mod.rs @@ -20,11 +20,11 @@ pub(crate) mod state; #[cfg(test)] mod tests { - use crate::coconut::tests::helpers::{ + use crate::ecash::tests::helpers::{ derive_keypairs, exchange_dealings, finalize, init_chain, initialise_controller, initialise_dkg, submit_public_keys, validate_keys, }; - use nym_coconut::aggregate_verification_keys; + use nym_compact_ecash::aggregate_verification_keys; #[tokio::test] #[ignore] // expensive test diff --git a/nym-api/src/coconut/dkg/public_key.rs b/nym-api/src/ecash/dkg/public_key.rs similarity index 96% rename from nym-api/src/coconut/dkg/public_key.rs rename to nym-api/src/ecash/dkg/public_key.rs index b2b7689923..b7f7d3e6ac 100644 --- a/nym-api/src/coconut/dkg/public_key.rs +++ b/nym-api/src/ecash/dkg/public_key.rs @@ -1,8 +1,8 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::controller::DkgController; -use crate::coconut::error::CoconutError; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::error::EcashError; use log::debug; use nym_coconut_dkg_common::types::EpochId; use rand::{CryptoRng, RngCore}; @@ -11,7 +11,7 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum PublicKeySubmissionError { #[error(transparent)] - CoconutError(#[from] CoconutError), + CoconutError(#[from] EcashError), } impl DkgController { @@ -83,7 +83,7 @@ impl DkgController { // I've (@JS) only updated old, existing, tests. nothing more #[cfg(test)] pub(crate) mod tests { - use crate::coconut::tests::fixtures; + use crate::ecash::tests::fixtures; #[tokio::test] async fn submit_public_key() -> anyhow::Result<()> { diff --git a/nym-api/src/coconut/dkg/state/dealing_exchange.rs b/nym-api/src/ecash/dkg/state/dealing_exchange.rs similarity index 94% rename from nym-api/src/coconut/dkg/state/dealing_exchange.rs rename to nym-api/src/ecash/dkg/state/dealing_exchange.rs index 02a0ff1dd2..d75b7aa808 100644 --- a/nym-api/src/coconut/dkg/state/dealing_exchange.rs +++ b/nym-api/src/ecash/dkg/state/dealing_exchange.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use super::serde_helpers::generated_dealings; -use crate::coconut::dkg::state::DkgParticipant; +use crate::ecash::dkg::state::DkgParticipant; use nym_coconut_dkg_common::types::DealingIndex; use nym_dkg::{Dealing, NodeIndex}; use serde::{Deserialize, Serialize}; diff --git a/nym-api/src/coconut/dkg/state/in_progress.rs b/nym-api/src/ecash/dkg/state/in_progress.rs similarity index 100% rename from nym-api/src/coconut/dkg/state/in_progress.rs rename to nym-api/src/ecash/dkg/state/in_progress.rs diff --git a/nym-api/src/coconut/dkg/state/key_derivation.rs b/nym-api/src/ecash/dkg/state/key_derivation.rs similarity index 100% rename from nym-api/src/coconut/dkg/state/key_derivation.rs rename to nym-api/src/ecash/dkg/state/key_derivation.rs diff --git a/nym-api/src/coconut/dkg/state/key_finalization.rs b/nym-api/src/ecash/dkg/state/key_finalization.rs similarity index 100% rename from nym-api/src/coconut/dkg/state/key_finalization.rs rename to nym-api/src/ecash/dkg/state/key_finalization.rs diff --git a/nym-api/src/coconut/dkg/state/key_validation.rs b/nym-api/src/ecash/dkg/state/key_validation.rs similarity index 100% rename from nym-api/src/coconut/dkg/state/key_validation.rs rename to nym-api/src/ecash/dkg/state/key_validation.rs diff --git a/nym-api/src/coconut/dkg/state/mod.rs b/nym-api/src/ecash/dkg/state/mod.rs similarity index 76% rename from nym-api/src/coconut/dkg/state/mod.rs rename to nym-api/src/ecash/dkg/state/mod.rs index 5ef0fae275..eb1cca637e 100644 --- a/nym-api/src/coconut/dkg/state/mod.rs +++ b/nym-api/src/ecash/dkg/state/mod.rs @@ -1,16 +1,14 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::state::dealing_exchange::DealingExchangeState; -use crate::coconut::dkg::state::in_progress::InProgressState; -use crate::coconut::dkg::state::key_derivation::KeyDerivationState; -use crate::coconut::dkg::state::key_finalization::FinalizationState; -use crate::coconut::dkg::state::key_validation::ValidationState; -use crate::coconut::dkg::state::registration::{ - DkgParticipant, ParticipantState, RegistrationState, -}; -use crate::coconut::error::CoconutError; -use crate::coconut::keys::{KeyPair as CoconutKeyPair, KeyPairWithEpoch}; +use crate::ecash::dkg::state::dealing_exchange::DealingExchangeState; +use crate::ecash::dkg::state::in_progress::InProgressState; +use crate::ecash::dkg::state::key_derivation::KeyDerivationState; +use crate::ecash::dkg::state::key_finalization::FinalizationState; +use crate::ecash::dkg::state::key_validation::ValidationState; +use crate::ecash::dkg::state::registration::{DkgParticipant, ParticipantState, RegistrationState}; +use crate::ecash::error::EcashError; +use crate::ecash::keys::{KeyPair as CoconutKeyPair, KeyPairWithEpoch}; use cosmwasm_std::Addr; use log::debug; use nym_coconut_dkg_common::dealer::DealerDetails; @@ -60,13 +58,13 @@ impl From<&State> for PersistentState { } impl PersistentState { - pub fn save_to_file>(&self, path: P) -> Result<(), CoconutError> { + pub fn save_to_file>(&self, path: P) -> Result<(), EcashError> { debug!("persisting the dkg state"); std::fs::write(path, serde_json::to_string(self)?)?; Ok(()) } - pub fn load_from_file>(path: P) -> Result { + pub fn load_from_file>(path: P) -> Result { Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?) } } @@ -136,7 +134,7 @@ impl State { } } - pub fn persist(&self) -> Result<(), CoconutError> { + pub fn persist(&self) -> Result<(), EcashError> { PersistentState::from(self).save_to_file(self.persistent_state_path()) } @@ -158,7 +156,7 @@ impl State { pub fn valid_epoch_receivers( &self, epoch_id: EpochId, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { Ok(self .dealing_exchange_state(epoch_id)? .dealers @@ -177,7 +175,7 @@ impl State { pub fn valid_epoch_receivers_keys( &self, epoch_id: EpochId, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { Ok(self .dealing_exchange_state(epoch_id)? .dealers @@ -186,157 +184,151 @@ impl State { .collect()) } - pub fn dkg_state(&self, epoch_id: EpochId) -> Result<&DkgState, CoconutError> { + pub fn dkg_state(&self, epoch_id: EpochId) -> Result<&DkgState, EcashError> { self.dkg_instances .get(&epoch_id) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } - pub fn dkg_state_mut(&mut self, epoch_id: EpochId) -> Result<&mut DkgState, CoconutError> { + pub fn dkg_state_mut(&mut self, epoch_id: EpochId) -> Result<&mut DkgState, EcashError> { self.dkg_instances .get_mut(&epoch_id) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } - pub fn registration_state( - &self, - epoch_id: EpochId, - ) -> Result<&RegistrationState, CoconutError> { + pub fn registration_state(&self, epoch_id: EpochId) -> Result<&RegistrationState, EcashError> { self.dkg_instances .get(&epoch_id) .map(|state| &state.registration) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn registration_state_mut( &mut self, epoch_id: EpochId, - ) -> Result<&mut RegistrationState, CoconutError> { + ) -> Result<&mut RegistrationState, EcashError> { self.dkg_instances .get_mut(&epoch_id) .map(|state| &mut state.registration) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } - pub fn in_progress_state(&self, epoch_id: EpochId) -> Result<&InProgressState, CoconutError> { + pub fn in_progress_state(&self, epoch_id: EpochId) -> Result<&InProgressState, EcashError> { self.dkg_instances .get(&epoch_id) .map(|state| &state.in_progress) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn in_progress_state_mut( &mut self, epoch_id: EpochId, - ) -> Result<&mut InProgressState, CoconutError> { + ) -> Result<&mut InProgressState, EcashError> { self.dkg_instances .get_mut(&epoch_id) .map(|state| &mut state.in_progress) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn dealing_exchange_state( &self, epoch_id: EpochId, - ) -> Result<&DealingExchangeState, CoconutError> { + ) -> Result<&DealingExchangeState, EcashError> { self.dkg_instances .get(&epoch_id) .map(|state| &state.dealing_exchange) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn dealing_exchange_state_mut( &mut self, epoch_id: EpochId, - ) -> Result<&mut DealingExchangeState, CoconutError> { + ) -> Result<&mut DealingExchangeState, EcashError> { self.dkg_instances .get_mut(&epoch_id) .map(|state| &mut state.dealing_exchange) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn key_derivation_state( &self, epoch_id: EpochId, - ) -> Result<&KeyDerivationState, CoconutError> { + ) -> Result<&KeyDerivationState, EcashError> { self.dkg_instances .get(&epoch_id) .map(|state| &state.key_generation) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn key_derivation_state_mut( &mut self, epoch_id: EpochId, - ) -> Result<&mut KeyDerivationState, CoconutError> { + ) -> Result<&mut KeyDerivationState, EcashError> { self.dkg_instances .get_mut(&epoch_id) .map(|state| &mut state.key_generation) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } - pub fn key_validation_state( - &self, - epoch_id: EpochId, - ) -> Result<&ValidationState, CoconutError> { + pub fn key_validation_state(&self, epoch_id: EpochId) -> Result<&ValidationState, EcashError> { self.dkg_instances .get(&epoch_id) .map(|state| &state.key_validation) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn key_validation_state_mut( &mut self, epoch_id: EpochId, - ) -> Result<&mut ValidationState, CoconutError> { + ) -> Result<&mut ValidationState, EcashError> { self.dkg_instances .get_mut(&epoch_id) .map(|state| &mut state.key_validation) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn key_finalization_state( &self, epoch_id: EpochId, - ) -> Result<&FinalizationState, CoconutError> { + ) -> Result<&FinalizationState, EcashError> { self.dkg_instances .get(&epoch_id) .map(|state| &state.key_finalization) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn key_finalization_state_mut( &mut self, epoch_id: EpochId, - ) -> Result<&mut FinalizationState, CoconutError> { + ) -> Result<&mut FinalizationState, EcashError> { self.dkg_instances .get_mut(&epoch_id) .map(|state| &mut state.key_finalization) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } - pub fn threshold(&self, epoch_id: EpochId) -> Result { + pub fn threshold(&self, epoch_id: EpochId) -> Result { self.key_derivation_state(epoch_id)? .expected_threshold - .ok_or(CoconutError::UnavailableThreshold { epoch_id }) + .ok_or(EcashError::UnavailableThreshold { epoch_id }) } - pub fn assigned_index(&self, epoch_id: EpochId) -> Result { + pub fn assigned_index(&self, epoch_id: EpochId) -> Result { self.registration_state(epoch_id)? .assigned_index - .ok_or(CoconutError::UnavailableAssignedIndex { epoch_id }) + .ok_or(EcashError::UnavailableAssignedIndex { epoch_id }) } - pub fn receiver_index(&self, epoch_id: EpochId) -> Result { + pub fn receiver_index(&self, epoch_id: EpochId) -> Result { self.dealing_exchange_state(epoch_id)? .receiver_index - .ok_or(CoconutError::UnavailableReceiverIndex { epoch_id }) + .ok_or(EcashError::UnavailableReceiverIndex { epoch_id }) } - pub fn proposal_id(&self, epoch_id: EpochId) -> Result { + pub fn proposal_id(&self, epoch_id: EpochId) -> Result { self.key_derivation_state(epoch_id)? .proposal_id - .ok_or(CoconutError::UnavailableProposalId { epoch_id }) + .ok_or(EcashError::UnavailableProposalId { epoch_id }) } pub fn persistent_state_path(&self) -> &Path { diff --git a/nym-api/src/coconut/dkg/state/registration.rs b/nym-api/src/ecash/dkg/state/registration.rs similarity index 98% rename from nym-api/src/coconut/dkg/state/registration.rs rename to nym-api/src/ecash/dkg/state/registration.rs index 8391cebeed..c5a7271aab 100644 --- a/nym-api/src/coconut/dkg/state/registration.rs +++ b/nym-api/src/ecash/dkg/state/registration.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::state::serde_helpers::bte_pk_serde; +use crate::ecash::dkg::state::serde_helpers::bte_pk_serde; use cosmwasm_std::Addr; use nym_coconut_dkg_common::dealer::DealerDetails; use nym_coconut_dkg_common::types::EncodedBTEPublicKeyWithProof; diff --git a/nym-api/src/coconut/dkg/state/serde_helpers.rs b/nym-api/src/ecash/dkg/state/serde_helpers.rs similarity index 100% rename from nym-api/src/coconut/dkg/state/serde_helpers.rs rename to nym-api/src/ecash/dkg/state/serde_helpers.rs diff --git a/nym-api/src/coconut/error.rs b/nym-api/src/ecash/error.rs similarity index 50% rename from nym-api/src/coconut/error.rs rename to nym-api/src/ecash/error.rs index 3fa75c443f..94201847dc 100644 --- a/nym-api/src/coconut/error.rs +++ b/nym-api/src/ecash/error.rs @@ -3,28 +3,35 @@ use crate::node_status_api::models::NymApiStorageError; use nym_coconut_dkg_common::types::{ChunkIndex, DealingIndex, EpochId}; -use nym_credentials::coconut::bandwidth::{CredentialType, UnknownCredentialType}; +use nym_credentials_interface::UnknownTicketType; use nym_crypto::asymmetric::{ encryption::KeyRecoveryError, identity::{Ed25519RecoveryError, SignatureError}, }; use nym_dkg::error::DkgError; -use nym_validator_client::coconut::CoconutApiError; -use nym_validator_client::nyxd::error::{NyxdError, TendermintError}; +use nym_dkg::Threshold; +use nym_ecash_contract_common::deposit::DepositId; +use nym_ecash_contract_common::redeem_credential::BATCH_REDEMPTION_PROPOSAL_TITLE; +use nym_validator_client::coconut::EcashApiError; +use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::AccountId; +use okapi::openapi3::Responses; use rocket::http::{ContentType, Status}; use rocket::response::Responder; use rocket::{response, Request, Response}; +use rocket_okapi::gen::OpenApiGenerator; +use rocket_okapi::response::OpenApiResponderInner; +use rocket_okapi::util::ensure_status_code_exists; use std::io::Cursor; use std::num::ParseIntError; use thiserror::Error; use time::error::ComponentRange; use time::OffsetDateTime; -pub type Result = std::result::Result; +pub type Result = std::result::Result; #[derive(Debug, Error)] -pub enum CoconutError { +pub enum EcashError { #[error(transparent)] IOError(#[from] std::io::Error), @@ -37,49 +44,24 @@ pub enum CoconutError { #[error("failed to derive the admin account from the provided public key: {formatted_source}")] AdminAccountDerivationFailure { formatted_source: String }, - #[error("failed to query for the authorised freepass requester address: {source}")] - FreepassAuthorisedFreepassRequesterQueryFailure { - #[from] - source: reqwest::Error, - }, - - #[error("the provided authorised freepass requester address ({address}) is not a valid cosmos address")] - MalformedAuthorisedFreepassRequesterAddress { address: String }, - - #[error("the requester of the free pass ({requester}) is not authorised. the only allowed account is {explicit_admin:?} or {bandwidth_contract_admin:?}.")] - UnauthorisedFreePassAccount { - requester: AccountId, - explicit_admin: Option, - bandwidth_contract_admin: Option, - }, - - #[error("failed to verify signature on the provided free pass request")] - FreePassSignatureVerificationFailure, - - #[error("the provided signing nonce is invalid. the current value is: {current:?}. got {received:?} instead")] - InvalidNonce { - current: [u8; 16], - received: [u8; 16], - }, - #[error("only secp256k1 keys are supported for free pass issuance")] UnsupportedNonSecp256k1Key, - #[error("received credential request for an unknown type: {0}")] - UnknownCredentialType(#[from] UnknownCredentialType), - - #[error("the provided free pass request had an unexpected number of public attributes. got {got} but expected {expected}")] - InvalidFreePassAttributes { got: usize, expected: usize }, - - #[error("the provided free pass request had an invalid type attribute (got: '{got}')")] - InvalidFreePassTypeAttribute { got: CredentialType }, - #[error("failed to parse the free pass expiry date: {source}")] ExpiryDateParsingFailure { #[source] source: ParseIntError, }, + #[error("the provided expiration date is too late")] + ExpirationDateTooLate, + + #[error("the provided expiration date is too early")] + ExpirationDateTooEarly, + + #[error("the provided expiration date is malformed")] + MalformedExpirationDate { raw: String }, + #[error("failed to parse expiry timestamp into proper datetime: {source}")] InvalidExpiryDate { unix_timestamp: i64, @@ -87,22 +69,9 @@ pub enum CoconutError { source: ComponentRange, }, - #[error( - "the provided free pass request has too long expiry (expiry is set to on {expiry_date})" - )] - TooLongFreePass { expiry_date: OffsetDateTime }, - - #[error("the provided free pass expiry is set in the past!")] - FreePassExpiryInThePast { expiry_date: OffsetDateTime }, - #[error("the received bandwidth voucher did not contain deposit value")] MissingBandwidthValue, - #[error( - "the received bandwidth credential is not a bandwidth voucher. the encoded type is: {typ}" - )] - NotABandwidthVoucher { typ: CredentialType }, - #[error("failed to parse the bandwidth voucher value: {source}")] VoucherValueParsingFailure { #[source] @@ -110,7 +79,7 @@ pub enum CoconutError { }, #[error("coconut api query failure: {0}")] - CoconutApiError(#[from] CoconutApiError), + CoconutApiError(#[from] EcashApiError), #[error(transparent)] SerdeJsonError(#[from] serde_json::Error), @@ -121,12 +90,6 @@ pub enum CoconutError { #[error("could not parse X25519 data: {0}")] X25519ParseError(#[from] KeyRecoveryError), - #[error("could not parse tx hash in request body: {source}")] - TxHashParseError { - #[source] - source: TendermintError, - }, - #[error("could not get transaction details for '{tx_hash}': {source}")] TxRetrievalFailure { tx_hash: String, @@ -140,39 +103,21 @@ pub enum CoconutError { #[error("validator client error: {0}")] ValidatorClientError(#[from] nym_validator_client::ValidatorClientError), - #[error("coconut internal error: {0}")] - CoconutInternalError(#[from] nym_coconut::CoconutError), + #[error("Compact ecash internal error - {0}")] + CompactEcashInternalError(#[from] nym_compact_ecash::error::CompactEcashError), + + #[error("Account linked to this public key has been blacklisted")] + BlacklistedAccount, #[error("could not find a deposit event in the transaction provided")] DepositEventNotFound, - #[error("could not find the deposit value in the event")] - DepositValueNotFound, - #[error("could not find the deposit info in the event")] DepositInfoNotFound, - #[error("could not find the verification key in the event")] - DepositVerifKeyNotFound, - - #[error("could not find the encryption key in the event")] - DepositEncrKeyNotFound, - #[error("signature didn't verify correctly")] SignatureVerificationError(#[from] SignatureError), - #[error("inconsistent public attributes")] - InconsistentPublicAttributes, - - #[error("the provided deposit value is inconsistent. got '{request}' while the value on chain is '{on_chain}'")] - InconsistentDepositValue { request: String, on_chain: String }, - - #[error("the provided deposit info is inconsistent. got '{request}' while the value on chain is '{on_chain}'")] - InconsistentDepositInfo { request: String, on_chain: String }, - - #[error("public attributes in request differ from the ones in deposit: Expected {0}, got {1}")] - DifferentPublicAttributes(String, String), - #[error("storage error: {0}")] StorageError(#[from] NymApiStorageError), @@ -182,9 +127,6 @@ pub enum CoconutError { #[error("incorrect credential proposal description: {reason}")] IncorrectProposal { reason: String }, - #[error("invalid status of credential: {status}")] - InvalidCredentialStatus { status: String }, - #[error("DKG error: {0}")] DkgError(#[from] DkgError), @@ -210,6 +152,9 @@ pub enum CoconutError { #[error("the internal dkg state for epoch {epoch_id} is missing - we might have joined mid exchange")] MissingDkgState { epoch_id: EpochId }, + #[error("a new iteration of DKG is currently in progress. all ticket issuance is halted until that's completed")] + DkgInProgress, + #[error( "the node index value for epoch {epoch_id} is not available - are you sure we are a dealer?" )] @@ -231,9 +176,50 @@ pub enum CoconutError { dealing_index: DealingIndex, chunk_index: ChunkIndex, }, + + #[error("could not find ecash deposit associated with id {deposit_id}")] + NonExistentDeposit { deposit_id: DepositId }, + + #[error("the provided request digest does not match the hash of attached serial numbers")] + MismatchedRequestDigest, + + #[error("the on chain proposal digest does not match the attached request digest")] + MismatchedOnChainDigest, + + #[error("one of the attached tickets {serial_number_bs58} has not been verified before")] + TicketNotVerified { serial_number_bs58: String }, + + #[error("the provided ticket(s) redemption proposal is invalid: {source}")] + RedemptionProposalFailure { + #[from] + source: RedemptionError, + }, + + #[error("this gateway hasn't submitted any tickets for verification")] + NotTicketsProvided, + + #[error("this gateway is attempting to redeem its tickets too often. last redemption happened on {last_redemption}. the earliest next permitted redemption will be on {next_allowed}")] + TooFrequentRedemption { + last_redemption: OffsetDateTime, + next_allowed: OffsetDateTime, + }, + + #[error( + "could not sign the data for epoch {requested}. our current key is for epoch {available}" + )] + InvalidSigningKeyEpoch { + requested: EpochId, + available: EpochId, + }, + + #[error("could not obtain enough shares for aggregation. got {shares} shares whilst the threshold is {threshold}")] + InsufficientNumberOfShares { threshold: Threshold, shares: usize }, + + #[error(transparent)] + UnknownTicketBookType(#[from] UnknownTicketType), } -impl<'r, 'o: 'r> Responder<'r, 'o> for CoconutError { +impl<'r, 'o: 'r> Responder<'r, 'o> for EcashError { fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> { let err_msg = self.to_string(); Response::build() @@ -243,3 +229,74 @@ impl<'r, 'o: 'r> Responder<'r, 'o> for CoconutError { .ok() } } + +impl OpenApiResponderInner for EcashError { + fn responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result { + let mut responses = Responses::default(); + ensure_status_code_exists(&mut responses, 400); + Ok(responses) + } +} + +#[derive(Debug, Error)] +pub enum RedemptionError { + #[error("failed to retrieve proposal {proposal_id} from the chain")] + ProposalRetrievalFailure { proposal_id: u64 }, + + #[error( + "the proposal {proposal_id} has invalid title. got {received} but expected {}", + BATCH_REDEMPTION_PROPOSAL_TITLE + )] + InvalidProposalTitle { proposal_id: u64, received: String }, + + #[error("the proposal {proposal_id} has invalid description. got {received} but expected {expected}")] + InvalidProposalDescription { + proposal_id: u64, + received: String, + expected: String, + }, + + #[error("the proposal {proposal_id} is still pending")] + StillPending { proposal_id: u64 }, + + #[error("the proposal {proposal_id} has already been executed")] + AlreadyExecuted { proposal_id: u64 }, + + #[error("the proposal {proposal_id} has already been rejected")] + AlreadyRejected { proposal_id: u64 }, + + #[error("the proposal {proposal_id} has already been passed")] + AlreadyPassed { proposal_id: u64 }, + + #[error("the proposal {proposal_id} was proposed by an unexpected address {received}. expected the ecash contract at {expected}")] + InvalidProposer { + proposal_id: u64, + received: String, + expected: AccountId, + }, + + #[error( + "the proposal {proposal_id} did not contain exactly a single contract execution message" + )] + TooManyMessages { proposal_id: u64 }, + + #[error("the proposal {proposal_id} did not contain the correct redemption execution message")] + InvalidMessage { proposal_id: u64 }, + + #[error("the proposal {proposal_id} has not been made against the expected e-cash contract")] + InvalidContract { proposal_id: u64 }, + + #[error("the proposal {proposal_id} proposes redemption of tickets for gateway {proposed}, but the request has been sent by {received}")] + InvalidRedemptionTarget { + proposal_id: u64, + proposed: String, + received: String, + }, + + #[error("the proposal {proposal_id} proposes redemption of {proposed} tickets, but the request has been sent for {received} instead")] + InvalidRedemptionTicketCount { + proposal_id: u64, + proposed: u16, + received: u16, + }, +} diff --git a/nym-api/src/ecash/helpers.rs b/nym-api/src/ecash/helpers.rs new file mode 100644 index 0000000000..677b9c355b --- /dev/null +++ b/nym-api/src/ecash/helpers.rs @@ -0,0 +1,140 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::error::EcashError; +use nym_api_requests::ecash::BlindSignRequestBody; +use nym_coconut_dkg_common::types::EpochId; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use nym_compact_ecash::scheme::keygen::SecretKeyAuth; +use nym_compact_ecash::{BlindedSignature, EncodedDate, EncodedTicketType}; +use nym_compact_ecash::{PublicKeyUser, WithdrawalRequest}; +use nym_ecash_time::EcashTime; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::future::Future; +use std::hash::Hash; +use std::ops::Deref; +use tokio::sync::{RwLock, RwLockReadGuard}; + +#[derive(Serialize, Deserialize)] +pub(crate) struct IssuedExpirationDateSignatures { + pub(crate) epoch_id: EpochId, + pub(crate) signatures: Vec, +} + +#[derive(Serialize, Deserialize)] +pub(crate) struct IssuedCoinIndicesSignatures { + pub(crate) epoch_id: EpochId, + pub(crate) signatures: Vec, +} + +pub(crate) trait CredentialRequest { + fn withdrawal_request(&self) -> &WithdrawalRequest; + fn expiration_date_timestamp(&self) -> EncodedDate; + fn ticketbook_type(&self) -> EncodedTicketType; + fn ecash_pubkey(&self) -> PublicKeyUser; +} + +impl CredentialRequest for BlindSignRequestBody { + fn withdrawal_request(&self) -> &WithdrawalRequest { + &self.inner_sign_request + } + + fn expiration_date_timestamp(&self) -> EncodedDate { + self.expiration_date.ecash_unix_timestamp() + } + + fn ticketbook_type(&self) -> EncodedTicketType { + self.ticketbook_type.encode() + } + + fn ecash_pubkey(&self) -> PublicKeyUser { + self.ecash_pubkey.clone() + } +} + +pub(crate) fn blind_sign( + request: &C, + signing_key: &SecretKeyAuth, +) -> Result { + Ok(nym_compact_ecash::scheme::withdrawal::issue( + signing_key, + request.ecash_pubkey().clone(), + request.withdrawal_request(), + request.expiration_date_timestamp(), + request.ticketbook_type(), + )?) +} + +// a map of items that never change for given key +pub(crate) struct CachedImmutableItems { + // I wonder if there's a more efficient structure with OnceLock or OnceCell or something + inner: RwLock>, +} + +// an item that stays constant throughout given epoch +pub(crate) type CachedImmutableEpochItem = CachedImmutableItems; + +impl Default for CachedImmutableItems { + fn default() -> Self { + CachedImmutableItems { + inner: RwLock::new(HashMap::new()), + } + } +} + +impl Deref for CachedImmutableItems { + type Target = RwLock>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl CachedImmutableItems +where + K: Eq + Hash, +{ + pub(crate) async fn get_or_init( + &self, + key: K, + f: F, + ) -> Result, EcashError> + where + F: FnOnce() -> U, + U: Future>, + K: Clone, + { + // 1. see if we already have the item cached + let guard = self.inner.read().await; + if let Ok(item) = RwLockReadGuard::try_map(guard, |map| map.get(&key)) { + return Ok(item); + } + + // 2. attempt to retrieve (and cache) it + let mut write_guard = self.inner.write().await; + + // see if another task has already set the item whilst we were waiting for the lock + if write_guard.get(&key).is_some() { + let read_guard = write_guard.downgrade(); + + // SAFETY: we just checked the entry exists and we never dropped the guard + #[allow(clippy::unwrap_used)] + return Ok(RwLockReadGuard::map(read_guard, |map| { + map.get(&key).unwrap() + })); + } + + let init = f().await?; + write_guard.insert(key.clone(), init); + + let guard = write_guard.downgrade(); + + // SAFETY: + // we just inserted the entry into the map while NEVER dropping the lock (only downgraded it) + // so it MUST exist and thus the unwrap is fine + #[allow(clippy::unwrap_used)] + Ok(RwLockReadGuard::map(guard, |map| map.get(&key).unwrap())) + } +} diff --git a/nym-api/src/coconut/keys/mod.rs b/nym-api/src/ecash/keys/mod.rs similarity index 60% rename from nym-api/src/coconut/keys/mod.rs rename to nym-api/src/ecash/keys/mod.rs index 1902d9e180..87d01865e2 100644 --- a/nym-api/src/coconut/keys/mod.rs +++ b/nym-api/src/ecash/keys/mod.rs @@ -1,8 +1,9 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_coconut::VerificationKey; +use crate::ecash::error::EcashError; use nym_coconut_dkg_common::types::EpochId; +use nym_compact_ecash::{SecretKeyAuth, VerificationKeyAuth}; use nym_dkg::Scalar; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -19,12 +20,29 @@ pub struct KeyPair { #[derive(Debug)] pub struct KeyPairWithEpoch { - pub(crate) keys: nym_coconut::KeyPair, + pub(crate) keys: nym_compact_ecash::KeyPairAuth, + pub(crate) issued_for_epoch: EpochId, +} + +impl From for KeyPairWithEpoch { + fn from(value: LegacyCoconutKeyWithEpoch) -> Self { + let (x, ys) = value.secret_key.hazmat_to_raw(); + let sk = nym_compact_ecash::SecretKeyAuth::create_from_raw(x, ys); + + KeyPairWithEpoch { + keys: sk.into(), + issued_for_epoch: value.issued_for_epoch, + } + } +} + +pub struct LegacyCoconutKeyWithEpoch { + pub(crate) secret_key: nym_coconut::SecretKey, pub(crate) issued_for_epoch: EpochId, } impl KeyPairWithEpoch { - pub(crate) fn new(keys: nym_coconut::KeyPair, issued_for_epoch: EpochId) -> Self { + pub(crate) fn new(keys: nym_compact_ecash::KeyPairAuth, issued_for_epoch: EpochId) -> Self { KeyPairWithEpoch { keys, issued_for_epoch, @@ -64,9 +82,24 @@ impl KeyPair { } } - pub async fn verification_key(&self) -> Option> { - RwLockReadGuard::try_map(self.get().await?, |maybe_keypair| { - maybe_keypair.as_ref().map(|k| k.keys.verification_key()) + pub async fn keys(&self) -> Result, EcashError> { + let keypair_guard = self.get().await.ok_or(EcashError::KeyPairNotDerivedYet)?; + RwLockReadGuard::try_map(keypair_guard, |keypair| keypair.as_ref()) + .map_err(|_| EcashError::KeyPairNotDerivedYet) + } + + pub async fn signing_key(&self) -> Result, EcashError> { + let keypair_guard = self.get().await.ok_or(EcashError::KeyPairNotDerivedYet)?; + + RwLockReadGuard::try_map(keypair_guard, |keypair| { + keypair.as_ref().map(|k| k.keys.secret_key()) + }) + .map_err(|_| EcashError::KeyPairNotDerivedYet) + } + + pub async fn verification_key(&self) -> Option> { + RwLockReadGuard::try_map(self.get().await?, |maybe_keys| { + maybe_keys.as_ref().map(|k| k.keys.verification_key_ref()) }) .ok() } diff --git a/nym-api/src/ecash/keys/persistence.rs b/nym-api/src/ecash/keys/persistence.rs new file mode 100644 index 0000000000..976be0dfd6 --- /dev/null +++ b/nym-api/src/ecash/keys/persistence.rs @@ -0,0 +1,77 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::keys::{KeyPairWithEpoch, LegacyCoconutKeyWithEpoch}; +use nym_coconut_dkg_common::types::EpochId; +use nym_compact_ecash::{error::CompactEcashError, scheme::keygen::SecretKeyAuth, KeyPairAuth}; +use nym_pemstore::traits::PemStorableKey; +use std::mem; + +impl PemStorableKey for KeyPairWithEpoch { + // that's not the best error for this, but it felt like an overkill to define a dedicated struct just for this purpose + type Error = CompactEcashError; + + fn pem_type() -> &'static str { + "ECASH KEY WITH EPOCH" + } + + fn to_bytes(&self) -> Vec { + let mut bytes = self.issued_for_epoch.to_be_bytes().to_vec(); + bytes.append(&mut self.keys.secret_key().to_bytes()); + bytes + } + + fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() <= mem::size_of::() { + return Err(CompactEcashError::DeserializationMinLength { + min: mem::size_of::(), + actual: bytes.len(), + }); + } + let epoch_id = EpochId::from_be_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + ]); + + let sk = SecretKeyAuth::from_bytes(&bytes[mem::size_of::()..])?; + let vk = sk.verification_key(); + + Ok(KeyPairWithEpoch { + keys: KeyPairAuth::from_keys(sk, vk), + issued_for_epoch: epoch_id, + }) + } +} + +impl PemStorableKey for LegacyCoconutKeyWithEpoch { + // that's not the best error for this, but it felt like an overkill to define a dedicated struct just for this purpose + type Error = nym_coconut::CoconutError; + + fn pem_type() -> &'static str { + "COCONUT KEY WITH EPOCH" + } + + fn to_bytes(&self) -> Vec { + let mut bytes = self.issued_for_epoch.to_be_bytes().to_vec(); + bytes.append(&mut self.secret_key.to_bytes()); + bytes + } + + fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() <= mem::size_of::() { + return Err(nym_coconut::CoconutError::DeserializationMinLength { + min: mem::size_of::(), + actual: bytes.len(), + }); + } + let epoch_id = EpochId::from_be_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + ]); + + let sk = nym_coconut::SecretKey::from_bytes(&bytes[mem::size_of::()..])?; + + Ok(LegacyCoconutKeyWithEpoch { + secret_key: sk, + issued_for_epoch: epoch_id, + }) + } +} diff --git a/nym-api/src/ecash/mod.rs b/nym-api/src/ecash/mod.rs new file mode 100644 index 0000000000..662822114f --- /dev/null +++ b/nym-api/src/ecash/mod.rs @@ -0,0 +1,47 @@ +// Copyright 2021-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use okapi::openapi3::OpenApi; +use rocket::Route; +use rocket_okapi::openapi_get_routes_spec; +use rocket_okapi::settings::OpenApiSettings; + +pub(crate) mod api_routes; +pub(crate) mod client; +pub(crate) mod comm; +mod deposit; +pub(crate) mod dkg; +pub(crate) mod error; +pub(crate) mod helpers; +pub(crate) mod keys; +pub(crate) mod state; +pub(crate) mod storage; +#[cfg(test)] +pub(crate) mod tests; + +// equivalent of 100nym +pub(crate) const MINIMUM_BALANCE: u128 = 100_000000; + +pub(crate) fn routes_open_api(settings: &OpenApiSettings, enabled: bool) -> (Vec, OpenApi) { + if enabled { + openapi_get_routes_spec![ + settings: + api_routes::partial_signing::post_blind_sign, + api_routes::partial_signing::partial_expiration_date_signatures, + api_routes::partial_signing::partial_coin_indices_signatures, + api_routes::spending::verify_ticket, + api_routes::spending::batch_redeem_tickets, + api_routes::spending::double_spending_filter_v1, + api_routes::issued::epoch_credentials, + api_routes::issued::issued_credential, + api_routes::issued::issued_credentials, + api_routes::aggregation::master_verification_key, + api_routes::aggregation::coin_indices_signatures, + api_routes::aggregation::expiration_date_signatures + ] + } else { + openapi_get_routes_spec![ + settings: + ] + } +} diff --git a/nym-api/src/ecash/state/auxiliary.rs b/nym-api/src/ecash/state/auxiliary.rs new file mode 100644 index 0000000000..d96713a336 --- /dev/null +++ b/nym-api/src/ecash/state/auxiliary.rs @@ -0,0 +1,45 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::client::Client as LocalClient; +use crate::ecash::comm::APICommunicationChannel; +use crate::ecash::error::{EcashError, Result}; +use crate::support::storage::NymApiStorage; +use nym_coconut_dkg_common::types::EpochId; + +pub(crate) struct AuxiliaryEcashState { + pub(crate) client: Box, + pub(crate) comm_channel: Box, + pub(crate) storage: NymApiStorage, +} + +impl AuxiliaryEcashState { + pub(crate) fn new(client: C, comm_channel: D, storage: NymApiStorage) -> Self + where + C: LocalClient + Send + Sync + 'static, + D: APICommunicationChannel + Send + Sync + 'static, + { + AuxiliaryEcashState { + client: Box::new(client), + comm_channel: Box::new(comm_channel), + storage, + } + } + + pub(crate) async fn current_epoch(&self) -> Result { + self.comm_channel.current_epoch().await + } + + pub(crate) async fn ensure_not_blacklisted(&self, encoded_pubkey_bs58: &str) -> Result<()> { + let res = self + .client + .get_blacklisted_account(encoded_pubkey_bs58.to_string()) + .await?; + + if res.account.is_some() { + return Err(EcashError::BlacklistedAccount); + } + + Ok(()) + } +} diff --git a/nym-api/src/ecash/state/bloom.rs b/nym-api/src/ecash/state/bloom.rs new file mode 100644 index 0000000000..939f19b3a9 --- /dev/null +++ b/nym-api/src/ecash/state/bloom.rs @@ -0,0 +1,2 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only diff --git a/nym-api/src/ecash/state/global.rs b/nym-api/src/ecash/state/global.rs new file mode 100644 index 0000000000..24d1ff00ff --- /dev/null +++ b/nym-api/src/ecash/state/global.rs @@ -0,0 +1,33 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::helpers::{ + CachedImmutableEpochItem, CachedImmutableItems, IssuedCoinIndicesSignatures, + IssuedExpirationDateSignatures, +}; +use nym_compact_ecash::VerificationKeyAuth; +use nym_validator_client::nyxd::AccountId; +use time::Date; + +pub(crate) struct GlobalEcachState { + pub(crate) contract_address: AccountId, + + pub(crate) master_verification_key: CachedImmutableEpochItem, + + // maybe we should use arrays here instead? + pub(crate) coin_index_signatures: CachedImmutableEpochItem, + + pub(crate) expiration_date_signatures: + CachedImmutableItems, +} + +impl GlobalEcachState { + pub(crate) fn new(contract_address: AccountId) -> Self { + GlobalEcachState { + contract_address, + master_verification_key: Default::default(), + coin_index_signatures: Default::default(), + expiration_date_signatures: Default::default(), + } + } +} diff --git a/nym-api/src/ecash/state/helpers.rs b/nym-api/src/ecash/state/helpers.rs new file mode 100644 index 0000000000..2fcfd18a31 --- /dev/null +++ b/nym-api/src/ecash/state/helpers.rs @@ -0,0 +1,160 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::error::EcashError; +use crate::ecash::state::local::TicketDoubleSpendingFilter; +use crate::ecash::storage::EcashStorageExt; +use crate::support::storage::NymApiStorage; +use futures::{stream, StreamExt}; +use nym_compact_ecash::constants; +use nym_config::defaults::BloomfilterParameters; +use nym_dkg::Threshold; +use nym_ecash_double_spending::{DoubleSpendingFilter, DoubleSpendingFilterBuilder}; +use nym_ecash_time::{cred_exp_date, ecash_today}; +use nym_validator_client::EcashApiClient; +use std::future::Future; +use time::ext::NumericalDuration; +use time::Date; +use tokio::sync::Mutex; + +// attempt to completely rebuild the bloomfilter data for given day +async fn try_rebuild_today_bloomfilter( + today: Date, + params: BloomfilterParameters, + storage: &NymApiStorage, +) -> Result { + log::info!("rebuilding bloomfilter for {today}"); + + let tickets = storage.get_all_spent_tickets_on(today).await?; + log::debug!( + "there are {} tickets to insert into the filter", + tickets.len() + ); + + let mut filter = DoubleSpendingFilter::new_empty(params); + for ticket in tickets { + filter.set(&ticket.serial_number) + } + Ok(filter) +} + +pub(crate) async fn prepare_partial_bloomfilter_builder( + storage: &NymApiStorage, + params: BloomfilterParameters, + params_id: i64, + start: Date, + days: i64, +) -> Result { + log::info!( + "attempting to rebuild partial bloomfilter starting at {start} which includes {days} days" + ); + + let mut filter_builder = DoubleSpendingFilter::builder(params); + for i in 0..days { + let date = start - i.days(); + let Some(bitmap) = storage + .try_load_partial_bloomfilter_bitmap(date, params_id) + .await? + else { + log::warn!("missing double spending bloomfilter bitmap for {date} (if this API hasn't been running for at least {days} day(s) since 'ecash'-based zk-nyms were introduced this is expected)"); + continue; + }; + if !filter_builder.add_bytes(&bitmap) { + log::error!( + "failed to add bitmap from {date} to the global bloomfilter. it may be malformed!" + ); + } + } + Ok(filter_builder) +} + +pub(super) async fn try_rebuild_bloomfilter( + storage: &NymApiStorage, +) -> Result { + log::info!("attempting to rebuild the double spending bloomfilter..."); + let today = ecash_today().date(); + + let (params_id, params) = storage.get_double_spending_filter_params().await?; + log::info!("will use the following parameters: {params:?}"); + + // we're never going to have persisted data for 'today'. we need to rebuild it from scratch + let today_filter = try_rebuild_today_bloomfilter(today, params, storage).await?; + + log::info!("attempting to rebuild the global filter"); + let mut global_filter = prepare_partial_bloomfilter_builder( + storage, + params, + params_id, + today.previous_day().unwrap(), + constants::CRED_VALIDITY_PERIOD_DAYS as i64 - 1, + ) + .await?; + + if !global_filter.add_bytes(&today_filter.dump_bitmap()) { + log::error!( + "failed to add bitmap from {today} to the global bloomfilter. it may be malformed!" + ); + } + + Ok(TicketDoubleSpendingFilter::new( + today, + params_id, + global_filter.build(), + today_filter, + )) +} + +pub(crate) fn ensure_sane_expiration_date(expiration_date: Date) -> Result<(), EcashError> { + let today = ecash_today(); + + if expiration_date < today.date() { + // what's the point of signatures with expiration in the past? + return Err(EcashError::ExpirationDateTooEarly); + } + + // SAFETY: we're nowhere near MAX date + #[allow(clippy::unwrap_used)] + if expiration_date > cred_exp_date().date().next_day().unwrap() { + // don't allow issuing signatures too far in advance (1 day beyond current value is fine) + return Err(EcashError::ExpirationDateTooLate); + } + + Ok(()) +} + +pub(crate) async fn query_all_threshold_apis( + all_apis: Vec, + threshold: Threshold, + f: F, +) -> Result, EcashError> +where + F: Fn(EcashApiClient) -> U, + U: Future>, +{ + let shares = Mutex::new(Vec::with_capacity(all_apis.len())); + + stream::iter(all_apis) + .for_each_concurrent(8, |api| async { + // can't be bothered to restructure the code to appease the borrow checker properly, + // so just assign this to a variable + let disp = api.to_string(); + match f(api).await { + Ok(partial_share) => shares.lock().await.push(partial_share), + Err(err) => { + log::warn!("failed to obtain partial threshold data from API: {disp}: {err}") + } + } + }) + .await; + + let shares = shares.into_inner(); + + if shares.len() < threshold as usize { + return Err(EcashError::InsufficientNumberOfShares { + threshold, + shares: shares.len(), + }); + } + + Ok(shares) +} diff --git a/nym-api/src/ecash/state/local.rs b/nym-api/src/ecash/state/local.rs new file mode 100644 index 0000000000..fd3b5080b1 --- /dev/null +++ b/nym-api/src/ecash/state/local.rs @@ -0,0 +1,155 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::helpers::{ + CachedImmutableEpochItem, CachedImmutableItems, IssuedCoinIndicesSignatures, + IssuedExpirationDateSignatures, +}; +use crate::ecash::keys::KeyPair; +use nym_config::defaults::BloomfilterParameters; +use nym_crypto::asymmetric::identity; +use nym_ecash_double_spending::DoubleSpendingFilter; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use time::{Date, OffsetDateTime}; +use tokio::sync::RwLock; + +pub(crate) struct TicketDoubleSpendingFilter { + built_on: Date, + params_id: i64, + + today_filter: DoubleSpendingFilter, + global_filter: DoubleSpendingFilter, +} + +impl TicketDoubleSpendingFilter { + pub(crate) fn new( + built_on: Date, + params_id: i64, + global_filter: DoubleSpendingFilter, + today_filter: DoubleSpendingFilter, + ) -> TicketDoubleSpendingFilter { + TicketDoubleSpendingFilter { + built_on, + params_id, + today_filter, + global_filter, + } + } + + pub(crate) fn built_on(&self) -> Date { + self.built_on + } + + pub(crate) fn params(&self) -> BloomfilterParameters { + self.today_filter.params() + } + + pub(crate) fn params_id(&self) -> i64 { + self.params_id + } + + pub(crate) fn check(&self, sn: &Vec) -> bool { + self.global_filter.check(sn) + } + + /// Returns boolean to indicate if the entry was already present + pub(crate) fn insert_both(&mut self, sn: &Vec) -> bool { + self.today_filter.set(sn); + self.insert_global_only(sn) + } + + /// Returns boolean to indicate if the entry was already present + pub(crate) fn insert_global_only(&mut self, sn: &Vec) -> bool { + let existed = self.global_filter.check(sn); + self.global_filter.set(sn); + existed + } + + pub(crate) fn export_today_bitmap(&self) -> Vec { + self.today_filter.dump_bitmap() + } + + pub(crate) fn export_global_bitmap(&self) -> Vec { + self.global_filter.dump_bitmap() + } + + pub(crate) fn advance_day(&mut self, date: Date, new_global: DoubleSpendingFilter) { + self.built_on = date; + self.global_filter = new_global; + self.today_filter.reset(); + } +} + +pub(crate) struct ExportedDoubleSpendingFilterData { + pub(crate) last_exported_at: OffsetDateTime, + pub(crate) bytes: Vec, +} + +#[derive(Clone)] +pub(crate) struct ExportedDoubleSpendingFilter { + pub(crate) being_exported: Arc, + pub(crate) data: Arc>, +} + +pub(crate) struct LocalEcashState { + pub(crate) ecash_keypair: KeyPair, + pub(crate) identity_keypair: identity::KeyPair, + + pub(crate) partial_coin_index_signatures: CachedImmutableEpochItem, + pub(crate) partial_expiration_date_signatures: + CachedImmutableItems, + + // the actual, up to date, bloomfilter + pub(crate) double_spending_filter: Arc>, + + // the cached byte representation of the bloomfilter to be used by the clients + pub(crate) exported_double_spending_filter: ExportedDoubleSpendingFilter, +} + +impl LocalEcashState { + pub(crate) fn new( + ecash_keypair: KeyPair, + identity_keypair: identity::KeyPair, + double_spending_filter: TicketDoubleSpendingFilter, + ) -> Self { + LocalEcashState { + ecash_keypair, + identity_keypair, + partial_coin_index_signatures: Default::default(), + partial_expiration_date_signatures: Default::default(), + exported_double_spending_filter: ExportedDoubleSpendingFilter { + being_exported: Arc::new(Default::default()), + data: Arc::new(RwLock::new(ExportedDoubleSpendingFilterData { + last_exported_at: OffsetDateTime::now_utc(), + bytes: double_spending_filter.export_global_bitmap(), + })), + }, + double_spending_filter: Arc::new(RwLock::new(double_spending_filter)), + } + } + + pub(crate) fn maybe_background_update_exported_bloomfilter(&self) { + // make sure another query hasn't already spawned an exporting task + let Ok(should_export) = self + .exported_double_spending_filter + .being_exported + .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + else { + return; + }; + + let filter = self.double_spending_filter.clone(); + let exported = self.exported_double_spending_filter.clone(); + + if should_export { + tokio::spawn(async move { + log::debug!("exporting bloomfilter bitmap"); + let new = filter.read().await.export_global_bitmap(); + let mut exported_guard = exported.data.write().await; + exported_guard.last_exported_at = OffsetDateTime::now_utc(); + exported_guard.bytes = new; + }); + } + } +} diff --git a/nym-api/src/ecash/state/mod.rs b/nym-api/src/ecash/state/mod.rs new file mode 100644 index 0000000000..b55117ea1e --- /dev/null +++ b/nym-api/src/ecash/state/mod.rs @@ -0,0 +1,855 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::client::Client as LocalClient; +use crate::ecash::comm::APICommunicationChannel; +use crate::ecash::deposit::validate_deposit; +use crate::ecash::error::{EcashError, RedemptionError, Result}; +use crate::ecash::helpers::{IssuedCoinIndicesSignatures, IssuedExpirationDateSignatures}; +use crate::ecash::keys::KeyPair; +use crate::ecash::state::auxiliary::AuxiliaryEcashState; +use crate::ecash::state::global::GlobalEcachState; +use crate::ecash::state::helpers::{ + ensure_sane_expiration_date, prepare_partial_bloomfilter_builder, query_all_threshold_apis, + try_rebuild_bloomfilter, +}; +use crate::ecash::state::local::LocalEcashState; +use crate::ecash::storage::models::{SerialNumberWrapper, TicketProvider}; +use crate::ecash::storage::EcashStorageExt; +use crate::support::storage::NymApiStorage; +use cosmwasm_std::{from_binary, CosmosMsg, WasmMsg}; +use cw3::Status; +use nym_api_requests::ecash::helpers::issued_credential_plaintext; +use nym_api_requests::ecash::models::BatchRedeemTicketsBody; +use nym_api_requests::ecash::BlindSignRequestBody; +use nym_coconut_dkg_common::types::EpochId; +use nym_compact_ecash::scheme::coin_indices_signatures::{ + aggregate_annotated_indices_signatures, sign_coin_indices, CoinIndexSignatureShare, +}; +use nym_compact_ecash::scheme::expiration_date_signatures::{ + aggregate_annotated_expiration_signatures, ExpirationDateSignatureShare, +}; +use nym_compact_ecash::{ + constants, scheme::expiration_date_signatures::sign_expiration_date, BlindedSignature, + SecretKeyAuth, VerificationKeyAuth, +}; +use nym_config::defaults::BloomfilterParameters; +use nym_credentials::ecash::utils::EcashTime; +use nym_credentials::{aggregate_verification_keys, CredentialSpendingData}; +use nym_crypto::asymmetric::identity; +use nym_ecash_contract_common::deposit::{Deposit, DepositId}; +use nym_ecash_contract_common::msg::ExecuteMsg; +use nym_ecash_contract_common::redeem_credential::BATCH_REDEMPTION_PROPOSAL_TITLE; +use nym_ecash_double_spending::DoubleSpendingFilter; +use nym_ecash_time::cred_exp_date; +use nym_validator_client::nyxd::AccountId; +use nym_validator_client::EcashApiClient; +use time::ext::NumericalDuration; +use time::{Date, Duration, OffsetDateTime}; +use tokio::sync::RwLockReadGuard; + +pub(crate) mod auxiliary; +pub(crate) mod bloom; +pub(crate) mod global; +mod helpers; +pub(crate) mod local; + +pub struct EcashState { + // state global to the system, like aggregated keys, addresses, etc. + pub(crate) global: GlobalEcachState, + + // state local to the api instance, like partial signatures, keys, etc. + pub(crate) local: LocalEcashState, + + // auxiliary data used for resolving requests like clients, storage, etc. + pub(crate) aux: AuxiliaryEcashState, +} + +impl EcashState { + pub(crate) async fn new( + contract_address: AccountId, + client: C, + identity_keypair: identity::KeyPair, + key_pair: KeyPair, + comm_channel: D, + storage: NymApiStorage, + ) -> Result + where + C: LocalClient + Send + Sync + 'static, + D: APICommunicationChannel + Send + Sync + 'static, + { + let double_spending_filter = try_rebuild_bloomfilter(&storage).await?; + + Ok(Self { + global: GlobalEcachState::new(contract_address), + local: LocalEcashState::new(key_pair, identity_keypair, double_spending_filter), + aux: AuxiliaryEcashState::new(client, comm_channel, storage), + }) + } + + pub(crate) async fn ecash_signing_key(&self) -> Result> { + self.local.ecash_keypair.signing_key().await + } + + #[allow(dead_code)] + pub(crate) async fn current_master_verification_key( + &self, + ) -> Result> { + self.master_verification_key(None).await + } + + pub(crate) async fn master_verification_key( + &self, + epoch_id: Option, + ) -> Result> { + let epoch_id = match epoch_id { + Some(id) => id, + None => self.aux.current_epoch().await?, + }; + + self.global + .master_verification_key + .get_or_init(epoch_id, || async { + // 1. check the storage + if let Some(stored) = self + .aux + .storage + .get_master_verification_key(epoch_id) + .await? + { + return Ok(stored); + } + + // 2. perform actual aggregation + let all_apis = self.aux.comm_channel.ecash_clients(epoch_id).await?; + let threshold = self.aux.comm_channel.ecash_threshold(epoch_id).await?; + + if all_apis.len() < threshold as usize { + return Err(EcashError::InsufficientNumberOfShares { + threshold, + shares: all_apis.len(), + }); + } + + let master_key = aggregate_verification_keys(&all_apis)?; + + // 3. save the key in the storage for when we reboot + self.aux + .storage + .insert_master_verification_key(epoch_id, &master_key) + .await?; + + Ok(master_key) + }) + .await + } + + pub(crate) async fn master_coin_index_signatures( + &self, + epoch_id: Option, + ) -> Result> { + let epoch_id = match epoch_id { + Some(id) => id, + None => self.aux.current_epoch().await?, + }; + + self.global + .coin_index_signatures + .get_or_init(epoch_id, || async { + // 1. check the storage + if let Some(master_sigs) = self + .aux + .storage + .get_master_coin_index_signatures(epoch_id) + .await? + { + return Ok(IssuedCoinIndicesSignatures { + epoch_id, + signatures: master_sigs, + }); + } + + log::info!( + "attempting to establish master coin index signatures for epoch {epoch_id}..." + ); + + // 2. go around APIs and attempt to aggregate the data + let master_vk = self.master_verification_key(Some(epoch_id)).await?; + let all_apis = self.aux.comm_channel.ecash_clients(epoch_id).await?; + let threshold = self.aux.comm_channel.ecash_threshold(epoch_id).await?; + + // let mut shares = Mutex::new(Vec::with_capacity(all_apis.len())); + let cosmos_address = self.aux.client.address().await; + + let get_partial_signatures = |api: EcashApiClient| async { + // move the api into the closure + let api = api; + let node_index = api.node_id; + let partial_vk = api.verification_key; + + // check if we're attempting to query ourselves, in that case just get local signature + // rather than making the http query + let partial = if api.cosmos_address == cosmos_address { + self.partial_coin_index_signatures(Some(epoch_id)) + .await? + .signatures + .clone() + } else { + api.api_client + .partial_coin_indices_signatures(Some(epoch_id)) + .await? + .signatures + }; + Ok(CoinIndexSignatureShare { + index: node_index, + key: partial_vk, + signatures: partial, + }) + }; + + let shares = + query_all_threshold_apis(all_apis, threshold, get_partial_signatures).await?; + + let aggregated = aggregate_annotated_indices_signatures( + nym_credentials_interface::ecash_parameters(), + &master_vk, + &shares, + )?; + + // 3. save the signatures in the storage for when we reboot + self.aux + .storage + .insert_master_coin_index_signatures(epoch_id, &aggregated) + .await?; + + Ok(IssuedCoinIndicesSignatures { + epoch_id, + signatures: aggregated, + }) + }) + .await + } + + pub(crate) async fn partial_coin_index_signatures( + &self, + epoch_id: Option, + ) -> Result> { + let epoch_id = match epoch_id { + Some(id) => id, + None => self.aux.current_epoch().await?, + }; + + self.local + .partial_coin_index_signatures + .get_or_init(epoch_id, || async { + // 1. check the storage + if let Some(partial_sigs) = self + .aux + .storage + .get_partial_coin_index_signatures(epoch_id) + .await? + { + return Ok(IssuedCoinIndicesSignatures { + epoch_id, + signatures: partial_sigs, + }); + } + + + // 2. perform actual issuance + let signing_keys = self.local.ecash_keypair.keys().await?; + if signing_keys.issued_for_epoch != epoch_id { + // TODO: this should get handled at some point, + // because if it was a past epoch we **do** have those keys. + // they're just archived + + log::error!("received partial coin index signature request for an invalid epoch ({epoch_id}). our key was derived for epoch {}", signing_keys.issued_for_epoch); + return Err(EcashError::InvalidSigningKeyEpoch { + requested: epoch_id, + available: signing_keys.issued_for_epoch, + }) + } + let master_vk = self.master_verification_key(Some(epoch_id)).await?; + let signatures = sign_coin_indices( + nym_compact_ecash::ecash_parameters(), + &master_vk, + signing_keys.keys.secret_key(), + )?; + + // 3. save the signatures in the storage for when we reboot + self.aux.storage.insert_partial_coin_index_signatures(epoch_id, &signatures).await?; + + Ok(IssuedCoinIndicesSignatures { + epoch_id, + signatures, + }) + }) + .await + } + + pub(crate) async fn master_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result> { + self.global + .expiration_date_signatures + .get_or_init(expiration_date, || async { + // 1. sanity check to see if the expiration_date is not nonsense + ensure_sane_expiration_date(expiration_date)?; + + // 2. check the storage + if let Some(master_sigs) = self + .aux + .storage + .get_master_expiration_date_signatures(expiration_date) + .await? + { + return Ok(master_sigs); + } + + // 3. go around APIs and attempt to aggregate the data + let epoch_id = self.aux.comm_channel.current_epoch().await?; + let master_vk = self.master_verification_key(Some(epoch_id)).await?; + let all_apis = self.aux.comm_channel.ecash_clients(epoch_id).await?; + let threshold = self.aux.comm_channel.ecash_threshold(epoch_id).await?; + + let cosmos_address = self.aux.client.address().await; + + let get_partial_signatures = |api: EcashApiClient| async { + // move the api into the closure + let api = api; + let node_index = api.node_id; + let partial_vk = api.verification_key; + + // check if we're attempting to query ourselves, in that case just get local signature + // rather than making the http query + let partial = if api.cosmos_address == cosmos_address { + self.partial_expiration_date_signatures(expiration_date) + .await? + .signatures + .clone() + } else { + api.api_client + .partial_expiration_date_signatures(Some(expiration_date)) + .await? + .signatures + }; + Ok(ExpirationDateSignatureShare { + index: node_index, + key: partial_vk, + signatures: partial, + }) + }; + + let shares = + query_all_threshold_apis(all_apis, threshold, get_partial_signatures).await?; + + let aggregated = aggregate_annotated_expiration_signatures( + &master_vk, + expiration_date.ecash_unix_timestamp(), + &shares, + )?; + + let issued = IssuedExpirationDateSignatures { + epoch_id, + signatures: aggregated, + }; + + // 4. save the signatures in the storage for when we reboot + self.aux + .storage + .insert_master_expiration_date_signatures(expiration_date, &issued) + .await?; + + Ok(issued) + }) + .await + } + + pub(crate) async fn partial_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result> { + self.local + .partial_expiration_date_signatures + .get_or_init(expiration_date, || async { + // 1. sanity check to see if the expiration_date is not nonsense + ensure_sane_expiration_date(expiration_date)?; + + // 2. check the storage + if let Some(partial_sigs) = self + .aux + .storage + .get_partial_expiration_date_signatures(expiration_date) + .await? + { + return Ok(partial_sigs); + } + + // 3. perform actual issuance + let signing_keys = self.local.ecash_keypair.keys().await?; + + let signatures = sign_expiration_date( + signing_keys.keys.secret_key(), + expiration_date.ecash_unix_timestamp(), + )?; + + let issued = IssuedExpirationDateSignatures { + epoch_id: signing_keys.issued_for_epoch, + signatures, + }; + + // 4. save the signatures in the storage for when we reboot + self.aux + .storage + .insert_partial_expiration_date_signatures(expiration_date, &issued) + .await?; + + Ok(issued) + }) + .await + } + + pub(crate) async fn ensure_dkg_not_in_progress(&self) -> Result<()> { + if self.aux.comm_channel.dkg_in_progress().await? { + return Err(EcashError::DkgInProgress); + } + Ok(()) + } + + /// Check if this nym-api has already issued a credential for the provided deposit id. + /// If so, return it. + pub async fn already_issued(&self, deposit_id: DepositId) -> Result> { + self.aux + .storage + .get_issued_bandwidth_credential_by_deposit_id(deposit_id) + .await? + .map(|cred| cred.try_into()) + .transpose() + } + + pub async fn get_deposit(&self, deposit_id: DepositId) -> Result { + self.aux + .client + .get_deposit(deposit_id) + .await? + .deposit + .ok_or(EcashError::NonExistentDeposit { deposit_id }) + } + + pub async fn validate_request( + &self, + request: &BlindSignRequestBody, + deposit: Deposit, + ) -> Result<()> { + validate_deposit(request, deposit).await + } + + pub(crate) async fn validate_redemption_proposal( + &self, + request: &BatchRedeemTicketsBody, + ) -> std::result::Result<(), RedemptionError> { + let proposal_id = request.proposal_id; + + // retrieve the proposal itself + let mut proposal = self + .aux + .client + .get_proposal(proposal_id) + .await + .map_err(|_| RedemptionError::ProposalRetrievalFailure { proposal_id })?; + + if proposal.title != BATCH_REDEMPTION_PROPOSAL_TITLE { + return Err(RedemptionError::InvalidProposalTitle { + proposal_id, + received: proposal.title, + }); + } + + // make sure you can still vote on it + match proposal.status { + Status::Pending => return Err(RedemptionError::StillPending { proposal_id }), + Status::Open => {} + Status::Rejected => return Err(RedemptionError::AlreadyRejected { proposal_id }), + + // TODO: need to double check with the multisig whether it wouldn't always be thrown on threshold + // i.e. whether after the 2+/3 vote, the remaining 1-/3 would return this error + Status::Passed => return Err(RedemptionError::AlreadyPassed { proposal_id }), + Status::Executed => return Err(RedemptionError::AlreadyExecuted { proposal_id }), + } + + let encoded_digest = bs58::encode(&request.digest).into_string(); + + // check if the description matches the expected digest + if encoded_digest != proposal.description { + return Err(RedemptionError::InvalidProposalDescription { + proposal_id, + received: proposal.description, + expected: encoded_digest, + }); + } + + // check if it was actually created by the ecash contract + if proposal.proposer != self.global.contract_address.as_ref() { + return Err(RedemptionError::InvalidProposer { + proposal_id, + received: proposal.proposer.into_string(), + expected: self.global.contract_address.clone(), + }); + } + + // check if contains exactly the content we expect, + // i.e. single `RedeemTickets` message with no funds, etc. + if proposal.msgs.len() != 1 { + return Err(RedemptionError::TooManyMessages { proposal_id }); + } + + // SAFETY: we just checked we have exactly one message + #[allow(clippy::unwrap_used)] + let msg = proposal.msgs.pop().unwrap(); + let CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr, + msg, + funds, + }) = msg + else { + return Err(RedemptionError::InvalidMessage { proposal_id }); + }; + + if !funds.is_empty() { + return Err(RedemptionError::InvalidMessage { proposal_id }); + } + + if contract_addr != self.global.contract_address.as_ref() { + return Err(RedemptionError::InvalidContract { proposal_id }); + } + + let Ok(ExecuteMsg::RedeemTickets { n, gw }) = from_binary(&msg) else { + return Err(RedemptionError::InvalidMessage { proposal_id }); + }; + + if gw != request.gateway_cosmos_addr.as_ref() { + return Err(RedemptionError::InvalidRedemptionTarget { + proposal_id, + proposed: gw, + received: request.gateway_cosmos_addr.to_string(), + }); + } + + if n as usize != request.included_serial_numbers.len() { + return Err(RedemptionError::InvalidRedemptionTicketCount { + proposal_id, + proposed: n, + received: request.included_serial_numbers.len() as u16, + }); + } + + Ok(()) + } + + pub(crate) async fn accept_proposal(&self, proposal_id: u64) -> Result<()> { + //SW NOTE: What to do if this fails + if let Err(err) = self.aux.client.vote_proposal(proposal_id, true, None).await { + log::debug!("failed to vote on proposal {proposal_id}: {err}"); + } + + Ok(()) + } + + // pub(crate) async fn blacklist(&self, public_key: String) { + // let client = self.aux.client.clone(); + // tokio::spawn(async move { + // //SW TODO error handling with one log at the end + // let response = client.propose_for_blacklist(public_key.clone()).await?; + // let proposal_id = find_proposal_id(&response.logs)?; + // + // let proposal = client.get_proposal(proposal_id).await?; + // if proposal.status == Status::Open { + // if public_key != proposal.description { + // return Err(EcashError::IncorrectProposal { + // reason: String::from("incorrect publickey in description"), + // }); + // } + // let ret = client.vote_proposal(proposal_id, true, None).await; + // + // accepted_vote_err(ret)?; + // + // if let Ok(proposal) = client.get_proposal(proposal_id).await { + // if proposal.status == Status::Passed { + // client.execute_proposal(proposal_id).await? + // } + // } + // } + // Ok(()) + // }); + // } + + pub(crate) async fn sign_and_store_credential( + &self, + current_epoch: EpochId, + request_body: BlindSignRequestBody, + blinded_signature: &BlindedSignature, + ) -> Result { + let encoded_commitments = request_body.encode_commitments(); + + let plaintext = issued_credential_plaintext( + current_epoch as u32, + request_body.deposit_id, + blinded_signature, + &encoded_commitments, + request_body.expiration_date, + request_body.ticketbook_type, + ); + + let signature = self.local.identity_keypair.private_key().sign(plaintext); + + // note: we have a UNIQUE constraint on the deposit_id column of the credential + // and so if the api is processing request for the same deposit at the same time, + // only one of them will be successfully inserted to the database + let credential_id = self + .aux + .storage + .store_issued_credential( + current_epoch as u32, + request_body.deposit_id, + blinded_signature, + signature, + encoded_commitments, + request_body.expiration_date, + request_body.ticketbook_type, + ) + .await?; + + Ok(credential_id) + } + + pub async fn store_issued_credential( + &self, + request_body: BlindSignRequestBody, + blinded_signature: &BlindedSignature, + ) -> Result<()> { + let current_epoch = self.aux.current_epoch().await?; + + // note: we have a UNIQUE constraint on the tx_hash column of the credential + // and so if the api is processing request for the same hash at the same time, + // only one of them will be successfully inserted to the database + let credential_id = self + .sign_and_store_credential(current_epoch, request_body, blinded_signature) + .await?; + self.aux + .storage + .update_epoch_credentials_entry(current_epoch, credential_id) + .await?; + debug!("the stored credential has id {credential_id}"); + + Ok(()) + } + + pub async fn store_verified_ticket( + &self, + ticket_data: &CredentialSpendingData, + gateway_addr: &AccountId, + ) -> Result<()> { + self.aux + .storage + .store_verified_ticket(ticket_data, gateway_addr) + .await + .map_err(Into::into) + } + + pub async fn get_ticket_provider( + &self, + gateway_address: &str, + ) -> Result> { + self.aux + .storage + .get_ticket_provider(gateway_address) + .await + .map_err(Into::into) + } + + pub async fn get_redeemable_tickets( + &self, + provider_info: TicketProvider, + ) -> Result> { + let since = provider_info + .last_batch_verification + .unwrap_or(OffsetDateTime::UNIX_EPOCH); + + self.aux + .storage + .get_verified_tickets_since(provider_info.id, since) + .await + .map_err(Into::into) + } + + pub async fn get_ticket_data_by_serial_number( + &self, + serial_number: &[u8], + ) -> Result> { + self.aux + .storage + .get_credential_data(serial_number) + .await + .map_err(Into::into) + } + + pub async fn check_bloomfilter(&self, serial_number: &Vec) -> bool { + self.local + .double_spending_filter + .read() + .await + .check(serial_number) + } + + async fn update_archived_partial_bloomfilter( + &self, + date: Date, + params_id: i64, + params: BloomfilterParameters, + sn: &Vec, + ) -> Result<(), EcashError> { + let mut filter = match self + .aux + .storage + .try_load_partial_bloomfilter_bitmap(date, params_id) + .await? + { + Some(bitmap) => DoubleSpendingFilter::from_bytes(params, &bitmap), + None => { + warn!("no existing partial bloomfilter for {date}"); + DoubleSpendingFilter::new_empty(params) + } + }; + filter.set(sn); + let updated_bitmap = filter.dump_bitmap(); + self.aux + .storage + .update_archived_partial_bloomfilter(date, &updated_bitmap) + .await?; + + Ok(()) + } + + /// Attempt to insert the provided serial number into the bloomfilter. + /// Furthermore, attempt to rotate the filter if we have advanced into a next day. + pub async fn update_bloomfilter( + &self, + serial_number: &Vec, + spending_date: Date, + today: Date, + ) -> Result { + let mut guard = self.local.double_spending_filter.write().await; + + let filter_date = guard.built_on(); + let yesterday = today.previous_day().unwrap(); + + let params_id = guard.params_id(); + let params = guard.params(); + + // if the filter is up-to-date, we just insert the entry and call it a day + if filter_date == today { + if spending_date == today { + return Ok(guard.insert_both(serial_number)); + } + // sanity check because this should NEVER happen, + // but when it inevitably does, we don't want to crash + if spending_date != yesterday { + log::error!("attempted to insert a ticket with spending date of {spending_date} while it's {today} today!!"); + } + + // this shouldn't be happening too often, so it's fine to interact with the storage + warn!("updating archived partial bloomfilter for {spending_date}. those logs have to be closely controlled to make sure they're not too frequent"); + self.update_archived_partial_bloomfilter( + spending_date, + params_id, + params, + serial_number, + ) + .await?; + + return Ok(guard.insert_global_only(serial_number)); + } + + log::info!("we need to advance our bloomfilter"); + let previous_bitmap = guard.export_today_bitmap(); + + // archive the BF for today's date + self.aux + .storage + .insert_partial_bloomfilter(filter_date, params_id, &previous_bitmap) + .await?; + + let new_global_filter = if filter_date == yesterday { + // normal case when we update filter daily + let two_days_ago = yesterday.previous_day().unwrap(); + let mut filter_builder = prepare_partial_bloomfilter_builder( + &self.aux.storage, + params, + params_id, + two_days_ago, + constants::CRED_VALIDITY_PERIOD_DAYS as i64 - 2, + ) + .await?; + // add the bitmap from 'old today', i.e. yesterday + // (we have it on hand so no point in retrieving it from storage) + filter_builder.add_bytes(&previous_bitmap); + filter_builder.build() + } else { + // initial deployment case when we don't even get tickets daily + prepare_partial_bloomfilter_builder( + &self.aux.storage, + params, + params_id, + yesterday, + constants::CRED_VALIDITY_PERIOD_DAYS as i64 - 1, + ) + .await? + .build() + }; + + guard.advance_day(today, new_global_filter); + + // drop guard so other tasks could read the filter already whilst we clean-up the storage + let res = if spending_date == today { + Ok(guard.insert_both(serial_number)) + } else { + Ok(guard.insert_global_only(serial_number)) + }; + drop(guard); + + let cutoff = cred_exp_date().ecash_date(); + + // sanity check: + assert_eq!( + cutoff, + today + (constants::CRED_VALIDITY_PERIOD_DAYS as i64 - 1).days() + ); + + // remove the data we no longer need to hold, i.e. partial bloomfilters beyond max credential validity + // and the ticket data for those + self.aux + .storage + .remove_old_partial_bloomfilters(cutoff) + .await?; + self.aux + .storage + .remove_expired_verified_tickets(cutoff) + .await?; + + res + } + + pub async fn get_bloomfilter_bytes(&self) -> Vec { + let guard = self.local.exported_double_spending_filter.data.read().await; + + let bytes = guard.bytes.clone(); + + // see if it's been > 5min since last export (that value is arbitrary) + if guard.last_exported_at + Duration::minutes(5) < OffsetDateTime::now_utc() { + self.local.maybe_background_update_exported_bloomfilter(); + } + + bytes + } +} diff --git a/nym-api/src/ecash/storage/helpers.rs b/nym-api/src/ecash/storage/helpers.rs new file mode 100644 index 0000000000..c0be8a9183 --- /dev/null +++ b/nym-api/src/ecash/storage/helpers.rs @@ -0,0 +1,52 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::models::NymApiStorageError; +use bincode::Options; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize)] +struct StorageBorrowedSerdeWrapper<'a, T>(&'a T); + +#[derive(Serialize, Deserialize)] +struct StorageSerdeWrapper(T); + +pub(crate) fn serialise_coin_index_signatures(sigs: &[AnnotatedCoinIndexSignature]) -> Vec { + storage_serialiser() + .serialize(&StorageBorrowedSerdeWrapper(&sigs)) + .unwrap() +} + +pub(crate) fn deserialise_coin_index_signatures( + raw: &[u8], +) -> Result, NymApiStorageError> { + let de: StorageSerdeWrapper<_> = storage_serialiser().deserialize(raw).map_err(|_| { + NymApiStorageError::database_inconsistency("malformed stored coin index signatures") + })?; + Ok(de.0) +} + +pub(crate) fn serialise_expiration_date_signatures( + sigs: &[AnnotatedExpirationDateSignature], +) -> Vec { + storage_serialiser() + .serialize(&StorageBorrowedSerdeWrapper(&sigs)) + .unwrap() +} + +pub(crate) fn deserialise_expiration_date_signatures( + raw: &[u8], +) -> Result, NymApiStorageError> { + let de: StorageSerdeWrapper<_> = storage_serialiser().deserialize(raw).map_err(|_| { + NymApiStorageError::database_inconsistency("malformed expiration date signatures") + })?; + Ok(de.0) +} + +pub(crate) fn storage_serialiser() -> impl bincode::Options { + bincode::DefaultOptions::new() + .with_big_endian() + .with_varint_encoding() +} diff --git a/nym-api/src/ecash/storage/manager.rs b/nym-api/src/ecash/storage/manager.rs new file mode 100644 index 0000000000..a77b4dbb20 --- /dev/null +++ b/nym-api/src/ecash/storage/manager.rs @@ -0,0 +1,837 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::storage::models::{ + EpochCredentials, IssuedTicketbook, RawExpirationDateSignatures, SerialNumberWrapper, + StoredBloomfilterParams, TicketProvider, VerifiedTicket, +}; +use crate::support::storage::manager::StorageManager; +use nym_coconut_dkg_common::types::EpochId; +use nym_ecash_contract_common::deposit::DepositId; +use time::{Date, OffsetDateTime}; + +#[async_trait] +pub trait EcashStorageManagerExt { + /// Gets the information about all issued partial credentials in this (coconut) epoch. + /// + /// # Arguments + /// + /// * `epoch_id`: Id of the (coconut) epoch in question. + async fn get_epoch_credentials( + &self, + epoch_id: EpochId, + ) -> Result, sqlx::Error>; + + /// Creates new entry for EpochCredentials for this (coconut) epoch. + /// + /// # Arguments + /// + /// * `epoch_id`: Id of the (coconut) epoch in question. + #[allow(dead_code)] + async fn create_epoch_credentials_entry(&self, epoch_id: EpochId) -> Result<(), sqlx::Error>; + + /// Update the EpochCredentials by incrementing the total number of issued credentials, + /// and setting `start_id` if unset (i.e. this is the first credential issued this epoch) + /// + /// # Arguments + /// * `epoch_id`: Id of the (coconut) epoch in question. + /// * `credential_id`: (database) Id of the coconut credential that triggered the update. + async fn update_epoch_credentials_entry( + &self, + epoch_id: EpochId, + credential_id: i64, + ) -> Result<(), sqlx::Error>; + + /// Attempts to retrieve an issued credential from the data store. + /// + /// # Arguments + /// + /// * `credential_id`: (database) id of the issued credential + async fn get_issued_credential( + &self, + credential_id: i64, + ) -> Result, sqlx::Error>; + + /// Attempts to retrieve an issued credential from the data store. + /// + /// # Arguments + /// + /// * `deposit_id`: id the deposit used in the issued bandwidth credential + async fn get_issued_bandwidth_credential_by_deposit_id( + &self, + deposit_id: DepositId, + ) -> Result, sqlx::Error>; + + /// Store the provided issued credential information and return its (database) id. + #[allow(clippy::too_many_arguments)] + async fn store_issued_ticketbook( + &self, + epoch_id: u32, + deposit_id: DepositId, + partial_credential: &[u8], + signature: &[u8], + joined_private_commitments: &[u8], + expiration_date: Date, + ticketbook_type_repr: u8, + ) -> Result; + + /// Attempts to retrieve issued credentials from the data store using provided ids. + /// + /// # Arguments + /// + /// * `credential_ids`: (database) ids of the issued credentials + async fn get_issued_ticketbooks( + &self, + credential_ids: Vec, + ) -> Result, sqlx::Error>; + + /// Attempts to retrieve issued credentials from the data store using pagination specification. + /// + /// # Arguments + /// + /// * `start_after`: the value preceding the first retrieved result + /// * `limit`: the maximum number of entries to retrieve + async fn get_issued_ticketbooks_paged( + &self, + start_after: i64, + limit: u32, + ) -> Result, sqlx::Error>; + + async fn insert_ticket_provider(&self, gateway_address: &str) -> Result; + + async fn get_ticket_provider( + &self, + gateway_address: &str, + ) -> Result, sqlx::Error>; + + async fn insert_verified_ticket( + &self, + provider_id: i64, + spending_date: Date, + verified_at: OffsetDateTime, + ticket_data: Vec, + serial_number: Vec, + ) -> Result<(), sqlx::Error>; + + async fn get_ticket(&self, serial_number: &[u8]) + -> Result, sqlx::Error>; + + async fn get_provider_ticket_serial_numbers( + &self, + provider_id: i64, + since: OffsetDateTime, + ) -> Result, sqlx::Error>; + + async fn get_spent_tickets_on( + &self, + date: Date, + ) -> Result, sqlx::Error>; + + async fn get_master_verification_key( + &self, + epoch_id: i64, + ) -> Result>, sqlx::Error>; + async fn insert_master_verification_key( + &self, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error>; + + async fn get_partial_coin_index_signatures( + &self, + epoch_id: i64, + ) -> Result>, sqlx::Error>; + async fn insert_partial_coin_index_signatures( + &self, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error>; + + async fn get_master_coin_index_signatures( + &self, + epoch_id: i64, + ) -> Result>, sqlx::Error>; + async fn insert_master_coin_index_signatures( + &self, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error>; + + async fn get_partial_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, sqlx::Error>; + async fn insert_partial_expiration_date_signatures( + &self, + epoch_id: i64, + expiration_date: Date, + data: &[u8], + ) -> Result<(), sqlx::Error>; + + async fn get_master_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, sqlx::Error>; + async fn insert_master_expiration_date_signatures( + &self, + epoch_id: i64, + expiration_date: Date, + data: &[u8], + ) -> Result<(), sqlx::Error>; + + async fn insert_double_spending_filter_params( + &self, + num_hashes: u32, + bitmap_size: u32, + sip0_key0: &[u8], + sip0_key1: &[u8], + sip1_key0: &[u8], + sip1_key1: &[u8], + ) -> Result; + + async fn get_latest_double_spending_filter_params( + &self, + ) -> Result, sqlx::Error>; + + async fn update_archived_partial_bloomfilter( + &self, + date: Date, + new_bitmap: &[u8], + ) -> Result<(), sqlx::Error>; + + async fn try_load_partial_bloomfilter_bitmap( + &self, + date: Date, + params_id: i64, + ) -> Result>, sqlx::Error>; + + async fn insert_partial_bloomfilter( + &self, + date: Date, + params_id: i64, + bitmap: &[u8], + ) -> Result<(), sqlx::Error>; + + async fn remove_old_partial_bloomfilters(&self, cutoff: Date) -> Result<(), sqlx::Error>; + + async fn remove_expired_verified_tickets(&self, cutoff: Date) -> Result<(), sqlx::Error>; +} + +#[async_trait] +impl EcashStorageManagerExt for StorageManager { + /// Gets the information about all issued partial credentials in this (coconut) epoch. + /// + /// # Arguments + /// + /// * `epoch_id`: Id of the (coconut) epoch in question. + async fn get_epoch_credentials( + &self, + epoch_id: EpochId, + ) -> Result, sqlx::Error> { + // even if we were changing epochs every second, it's rather impossible to overflow here + // within any sane amount of time + assert!(epoch_id <= u32::MAX as u64); + let epoch_id_downcasted = epoch_id as u32; + + sqlx::query_as!( + EpochCredentials, + r#" + SELECT epoch_id as "epoch_id: u32", start_id, total_issued as "total_issued: u32" + FROM epoch_credentials + WHERE epoch_id = ? + "#, + epoch_id_downcasted + ) + .fetch_optional(&self.connection_pool) + .await + } + + /// Creates new entry for EpochCredentials for this (coconut) epoch. + /// + /// # Arguments + /// + /// * `epoch_id`: Id of the (coconut) epoch in question. + async fn create_epoch_credentials_entry(&self, epoch_id: EpochId) -> Result<(), sqlx::Error> { + // even if we were changing epochs every second, it's rather impossible to overflow here + // within any sane amount of time + assert!(epoch_id <= u32::MAX as u64); + let epoch_id_downcasted = epoch_id as u32; + + sqlx::query!( + r#" + INSERT INTO epoch_credentials + (epoch_id, start_id, total_issued) + VALUES (?, ?, ?); + "#, + epoch_id_downcasted, + -1, + 0 + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + // the logic in this function can be summarised with: + // 1. get the current EpochCredentials for this epoch + // 2. if it exists -> increment `total_issued` + // 3. it has invalid (i.e. -1) `start_id` set it to the provided value + // 4. if it didn't exist, create new entry + /// Update the EpochCredentials by incrementing the total number of issued credentials, + /// and setting `start_id` if unset (i.e. this is the first credential issued this epoch) + /// + /// # Arguments + /// * `epoch_id`: Id of the (coconut) epoch in question. + /// * `credential_id`: (database) Id of the coconut credential that triggered the update. + async fn update_epoch_credentials_entry( + &self, + epoch_id: EpochId, + credential_id: i64, + ) -> Result<(), sqlx::Error> { + // even if we were changing epochs every second, it's rather impossible to overflow here + // within any sane amount of time + assert!(epoch_id <= u32::MAX as u64); + let epoch_id_downcasted = epoch_id as u32; + + // make the atomic transaction in case other tasks are attempting to use the pool + let mut tx = self.connection_pool.begin().await?; + + if let Some(existing) = sqlx::query_as!( + EpochCredentials, + r#" + SELECT epoch_id as "epoch_id: u32", start_id, total_issued as "total_issued: u32" + FROM epoch_credentials + WHERE epoch_id = ? + "#, + epoch_id_downcasted + ) + .fetch_optional(&mut tx) + .await? + { + // the entry has existed before -> update it + if existing.total_issued == 0 { + // no credentials has been issued -> we have to set the `start_id` + sqlx::query!( + r#" + UPDATE epoch_credentials + SET total_issued = 1, start_id = ? + WHERE epoch_id = ? + "#, + credential_id, + epoch_id_downcasted + ) + .execute(&mut tx) + .await?; + } else { + // we have issued credentials in this epoch before -> just increment `total_issued` + sqlx::query!( + r#" + UPDATE epoch_credentials + SET total_issued = total_issued + 1 + WHERE epoch_id = ? + "#, + epoch_id_downcasted + ) + .execute(&mut tx) + .await?; + } + } else { + // the entry has never been created -> probably some race condition; create it instead + sqlx::query!( + r#" + INSERT INTO epoch_credentials + (epoch_id, start_id, total_issued) + VALUES (?, ?, ?); + "#, + epoch_id_downcasted, + credential_id, + 1 + ) + .execute(&mut tx) + .await?; + } + + // finally commit the transaction + tx.commit().await + } + + /// Attempts to retrieve an issued credential from the data store. + /// + /// # Arguments + /// + /// * `credential_id`: (database) id of the issued credential + async fn get_issued_credential( + &self, + credential_id: i64, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + IssuedTicketbook, + r#" + SELECT + id, + epoch_id as "epoch_id: u32", + deposit_id as "deposit_id: DepositId", + partial_credential, + signature, + joined_private_commitments, + expiration_date as "expiration_date: Date", + ticketbook_type_repr as "ticketbook_type_repr: u8" + FROM issued_ticketbook + WHERE id = ? + "#, + credential_id + ) + .fetch_optional(&self.connection_pool) + .await + } + + /// Attempts to retrieve an issued credential from the data store. + /// + /// # Arguments + /// + /// * `deposit_id`: id the deposit used in the issued bandwidth credential + async fn get_issued_bandwidth_credential_by_deposit_id( + &self, + deposit_id: DepositId, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + IssuedTicketbook, + r#" + SELECT + id, + epoch_id as "epoch_id: u32", + deposit_id as "deposit_id: DepositId", + partial_credential, + signature, + joined_private_commitments, + expiration_date as "expiration_date: Date", + ticketbook_type_repr as "ticketbook_type_repr: u8" + FROM issued_ticketbook + WHERE deposit_id = ? + "#, + deposit_id + ) + .fetch_optional(&self.connection_pool) + .await + } + + /// Store the provided issued credential information and return its (database) id. + async fn store_issued_ticketbook( + &self, + epoch_id: u32, + deposit_id: DepositId, + partial_credential: &[u8], + signature: &[u8], + joined_private_commitments: &[u8], + expiration_date: Date, + ticketbook_type_repr: u8, + ) -> Result { + let row_id = sqlx::query!( + r#" + INSERT INTO issued_ticketbook + (epoch_id, deposit_id, partial_credential, signature, joined_private_commitments, expiration_date, ticketbook_type_repr) + VALUES + (?, ?, ?, ?, ?, ?, ?) + "#, + epoch_id, deposit_id, partial_credential, signature, joined_private_commitments, expiration_date, ticketbook_type_repr + ).execute(&self.connection_pool).await?.last_insert_rowid(); + + Ok(row_id) + } + + /// Attempts to retrieve issued credentials from the data store using provided ids. + /// + /// # Arguments + /// + /// * `credential_ids`: (database) ids of the issued credentials + async fn get_issued_ticketbooks( + &self, + credential_ids: Vec, + ) -> Result, sqlx::Error> { + // that sucks : ( + // https://stackoverflow.com/a/70032524 + let params = format!("?{}", ", ?".repeat(credential_ids.len() - 1)); + let query_str = format!("SELECT * FROM issued_ticketbook WHERE id IN ( {params} )"); + let mut query = sqlx::query_as(&query_str); + for id in credential_ids { + query = query.bind(id) + } + + query.fetch_all(&self.connection_pool).await + } + + /// Attempts to retrieve issued credentials from the data store using pagination specification. + /// + /// # Arguments + /// + /// * `start_after`: the value preceding the first retrieved result + /// * `limit`: the maximum number of entries to retrieve + async fn get_issued_ticketbooks_paged( + &self, + start_after: i64, + limit: u32, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + IssuedTicketbook, + r#" + SELECT + id, + epoch_id as "epoch_id: u32", + deposit_id as "deposit_id: DepositId", + partial_credential, + signature, + joined_private_commitments, + expiration_date as "expiration_date: Date", + ticketbook_type_repr as "ticketbook_type_repr: u8" + FROM issued_ticketbook + WHERE id > ? + ORDER BY id + LIMIT ? + "#, + start_after, + limit + ) + .fetch_all(&self.connection_pool) + .await + } + + async fn insert_ticket_provider(&self, gateway_address: &str) -> Result { + let id = sqlx::query!( + "INSERT INTO ticket_providers(gateway_address) VALUES (?)", + gateway_address + ) + .execute(&self.connection_pool) + .await? + .last_insert_rowid(); + Ok(id) + } + + async fn get_ticket_provider( + &self, + gateway_address: &str, + ) -> Result, sqlx::Error> { + sqlx::query_as("SELECT * FROM ticket_providers WHERE gateway_address = ?") + .bind(gateway_address) + .fetch_optional(&self.connection_pool) + .await + } + async fn insert_verified_ticket( + &self, + provider_id: i64, + spending_date: Date, + verified_at: OffsetDateTime, + ticket_data: Vec, + serial_number: Vec, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO verified_tickets(ticket_data, serial_number, spending_date, verified_at, gateway_id) + VALUES (?, ?, ?, ?, ?) + "#, + ticket_data, + serial_number, + spending_date, + verified_at, + provider_id + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + async fn get_ticket( + &self, + serial_number: &[u8], + ) -> Result, sqlx::Error> { + sqlx::query_as("SELECT * FROM verified_tickets WHERE serial_number = ?") + .bind(serial_number) + .fetch_optional(&self.connection_pool) + .await + } + + async fn get_provider_ticket_serial_numbers( + &self, + provider_id: i64, + since: OffsetDateTime, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + SerialNumberWrapper, + r#" + SELECT serial_number + FROM verified_tickets + WHERE gateway_id = ? + AND verified_at > ? + ORDER BY verified_at ASC + LIMIT 65535 + "#, + provider_id, + since + ) + .fetch_all(&self.connection_pool) + .await + } + + async fn get_spent_tickets_on( + &self, + date: Date, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + SerialNumberWrapper, + r#" + SELECT serial_number + FROM verified_tickets + WHERE spending_date = ? + "#, + date + ) + .fetch_all(&self.connection_pool) + .await + } + + async fn get_master_verification_key( + &self, + epoch_id: i64, + ) -> Result>, sqlx::Error> { + sqlx::query!( + "SELECT serialised_key FROM master_verification_key WHERE epoch_id = ?", + epoch_id + ) + .fetch_optional(&self.connection_pool) + .await + .map(|maybe_record| maybe_record.map(|r| r.serialised_key)) + } + + async fn insert_master_verification_key( + &self, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO master_verification_key(epoch_id, serialised_key) VALUES (?, ?)", + epoch_id, + data + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn get_partial_coin_index_signatures( + &self, + epoch_id: i64, + ) -> Result>, sqlx::Error> { + sqlx::query!( + "SELECT serialised_signatures FROM partial_coin_index_signatures WHERE epoch_id = ?", + epoch_id + ) + .fetch_optional(&self.connection_pool) + .await + .map(|maybe_record| maybe_record.map(|r| r.serialised_signatures)) + } + + async fn insert_partial_coin_index_signatures( + &self, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO partial_coin_index_signatures(epoch_id, serialised_signatures) VALUES (?, ?)", + epoch_id, + data + ).execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn get_master_coin_index_signatures( + &self, + epoch_id: i64, + ) -> Result>, sqlx::Error> { + sqlx::query!( + "SELECT serialised_signatures FROM global_coin_index_signatures WHERE epoch_id = ?", + epoch_id + ) + .fetch_optional(&self.connection_pool) + .await + .map(|maybe_record| maybe_record.map(|r| r.serialised_signatures)) + } + + async fn insert_master_coin_index_signatures( + &self, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO global_coin_index_signatures(epoch_id, serialised_signatures) VALUES (?, ?)", + epoch_id, + data + ).execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn get_partial_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + RawExpirationDateSignatures, + r#" + SELECT epoch_id as "epoch_id: u32", serialised_signatures + FROM partial_expiration_date_signatures + WHERE expiration_date = ? + "#, + expiration_date + ) + .fetch_optional(&self.connection_pool) + .await + } + + async fn insert_partial_expiration_date_signatures( + &self, + epoch_id: i64, + expiration_date: Date, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO partial_expiration_date_signatures(expiration_date, epoch_id, serialised_signatures) VALUES (?, ?, ?)", + expiration_date, + epoch_id, + data + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn get_master_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + RawExpirationDateSignatures, + r#" + SELECT epoch_id as "epoch_id: u32", serialised_signatures + FROM global_expiration_date_signatures + WHERE expiration_date = ? + "#, + expiration_date + ) + .fetch_optional(&self.connection_pool) + .await + } + + async fn insert_master_expiration_date_signatures( + &self, + epoch_id: i64, + expiration_date: Date, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO global_expiration_date_signatures(expiration_date, epoch_id, serialised_signatures) VALUES (?, ?, ?)", + expiration_date, + epoch_id, + data + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn insert_double_spending_filter_params( + &self, + num_hashes: u32, + bitmap_size: u32, + sip0_key0: &[u8], + sip0_key1: &[u8], + sip1_key0: &[u8], + sip1_key1: &[u8], + ) -> Result { + let row_id = sqlx::query!( + r#" + INSERT INTO bloomfilter_parameters(num_hashes, bitmap_size,sip0_key0, sip0_key1, sip1_key0, sip1_key1) + VALUES (?, ?, ?, ?, ?, ?) + "#, + num_hashes, + bitmap_size, + sip0_key0, + sip0_key1, + sip1_key0, + sip1_key1 + ).execute(&self.connection_pool).await?.last_insert_rowid(); + Ok(row_id) + } + + async fn get_latest_double_spending_filter_params( + &self, + ) -> Result, sqlx::Error> { + sqlx::query_as("SELECT * FROM bloomfilter_parameters ORDER BY id DESC LIMIT 1") + .fetch_optional(&self.connection_pool) + .await + } + + async fn update_archived_partial_bloomfilter( + &self, + date: Date, + new_bitmap: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE partial_bloomfilter SET bitmap = ? WHERE date = ?", + new_bitmap, + date + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn try_load_partial_bloomfilter_bitmap( + &self, + date: Date, + params_id: i64, + ) -> Result>, sqlx::Error> { + sqlx::query!( + "SELECT bitmap FROM partial_bloomfilter WHERE date = ? AND parameters = ?", + date, + params_id + ) + .fetch_optional(&self.connection_pool) + .await + .map(|maybe_record| maybe_record.map(|r| r.bitmap)) + } + + async fn insert_partial_bloomfilter( + &self, + date: Date, + params_id: i64, + bitmap: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO partial_bloomfilter(date, parameters, bitmap) VALUES (?, ?, ?)", + date, + params_id, + bitmap + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn remove_old_partial_bloomfilters(&self, cutoff: Date) -> Result<(), sqlx::Error> { + sqlx::query!("DELETE FROM partial_bloomfilter WHERE date > ?", cutoff) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn remove_expired_verified_tickets(&self, cutoff: Date) -> Result<(), sqlx::Error> { + sqlx::query!( + "DELETE FROM verified_tickets WHERE spending_date > ?", + cutoff + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } +} diff --git a/nym-api/src/ecash/storage/mod.rs b/nym-api/src/ecash/storage/mod.rs new file mode 100644 index 0000000000..cd7fc26f40 --- /dev/null +++ b/nym-api/src/ecash/storage/mod.rs @@ -0,0 +1,641 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::helpers::IssuedExpirationDateSignatures; +use crate::ecash::storage::helpers::{ + deserialise_coin_index_signatures, deserialise_expiration_date_signatures, + serialise_coin_index_signatures, serialise_expiration_date_signatures, +}; +use crate::ecash::storage::manager::EcashStorageManagerExt; +use crate::ecash::storage::models::{ + join_attributes, EpochCredentials, IssuedTicketbook, SerialNumberWrapper, TicketProvider, +}; +use crate::node_status_api::models::NymApiStorageError; +use crate::support::storage::NymApiStorage; +use nym_api_requests::ecash::models::Pagination; +use nym_coconut_dkg_common::types::EpochId; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::BlindedSignature; +use nym_compact_ecash::VerificationKeyAuth; +use nym_config::defaults::BloomfilterParameters; +use nym_credentials::CredentialSpendingData; +use nym_credentials_interface::TicketType; +use nym_crypto::asymmetric::identity; +use nym_ecash_contract_common::deposit::DepositId; +use nym_validator_client::nyxd::AccountId; +use time::{Date, OffsetDateTime}; + +mod helpers; +pub(crate) mod manager; +pub(crate) mod models; + +const DEFAULT_CREDENTIALS_PAGE_LIMIT: u32 = 100; + +#[async_trait] +pub trait EcashStorageExt { + async fn get_double_spending_filter_params( + &self, + ) -> Result<(i64, BloomfilterParameters), NymApiStorageError>; + + async fn update_archived_partial_bloomfilter( + &self, + date: Date, + new_bitmap: &[u8], + ) -> Result<(), NymApiStorageError>; + + async fn try_load_partial_bloomfilter_bitmap( + &self, + date: Date, + params_id: i64, + ) -> Result>, NymApiStorageError>; + + async fn insert_partial_bloomfilter( + &self, + date: Date, + params_id: i64, + bitmap: &[u8], + ) -> Result<(), NymApiStorageError>; + + async fn remove_old_partial_bloomfilters(&self, cutoff: Date) + -> Result<(), NymApiStorageError>; + + async fn remove_expired_verified_tickets(&self, cutoff: Date) + -> Result<(), NymApiStorageError>; + + async fn get_epoch_credentials( + &self, + epoch_id: EpochId, + ) -> Result, NymApiStorageError>; + + #[allow(dead_code)] + async fn create_epoch_credentials_entry( + &self, + epoch_id: EpochId, + ) -> Result<(), NymApiStorageError>; + + async fn update_epoch_credentials_entry( + &self, + epoch_id: EpochId, + credential_id: i64, + ) -> Result<(), NymApiStorageError>; + + async fn get_issued_credential( + &self, + credential_id: i64, + ) -> Result, NymApiStorageError>; + + async fn get_issued_bandwidth_credential_by_deposit_id( + &self, + deposit_id: DepositId, + ) -> Result, NymApiStorageError>; + + #[allow(clippy::too_many_arguments)] + async fn store_issued_credential( + &self, + epoch_id: u32, + deposit_id: DepositId, + partial_credential: &BlindedSignature, + signature: identity::Signature, + private_commitments: Vec>, + expiration_date: Date, + ticketbook_type: TicketType, + ) -> Result; + + async fn get_issued_credentials( + &self, + credential_ids: Vec, + ) -> Result, NymApiStorageError>; + + async fn get_issued_credentials_paged( + &self, + pagination: Pagination, + ) -> Result, NymApiStorageError>; + // + // async fn insert_credential( + // &self, + // credential: &CredentialSpendingData, + // serial_number_bs58: String, + // gateway_addr: &AccountId, + // proposal_id: u64, + // ) -> Result<(), NymApiStorageError>; + // + async fn get_credential_data( + &self, + serial_number: &[u8], + ) -> Result, NymApiStorageError>; + + async fn store_verified_ticket( + &self, + ticket_data: &CredentialSpendingData, + gateway_addr: &AccountId, + ) -> Result<(), NymApiStorageError>; + + async fn get_ticket_provider( + &self, + gateway_address: &str, + ) -> Result, NymApiStorageError>; + + async fn get_verified_tickets_since( + &self, + provider_id: i64, + since: OffsetDateTime, + ) -> Result, NymApiStorageError>; + + async fn get_all_spent_tickets_on( + &self, + date: Date, + ) -> Result, NymApiStorageError>; + + async fn get_or_create_ticket_provider_with_id( + &self, + gateway_address: &str, + ) -> Result; + + async fn get_master_verification_key( + &self, + epoch_id: EpochId, + ) -> Result, NymApiStorageError>; + async fn insert_master_verification_key( + &self, + epoch_id: EpochId, + key: &VerificationKeyAuth, + ) -> Result<(), NymApiStorageError>; + + async fn get_partial_coin_index_signatures( + &self, + epoch_id: EpochId, + ) -> Result>, NymApiStorageError>; + async fn insert_partial_coin_index_signatures( + &self, + epoch_id: EpochId, + sigs: &[AnnotatedCoinIndexSignature], + ) -> Result<(), NymApiStorageError>; + + async fn get_master_coin_index_signatures( + &self, + epoch_id: EpochId, + ) -> Result>, NymApiStorageError>; + async fn insert_master_coin_index_signatures( + &self, + epoch_id: EpochId, + sigs: &[AnnotatedCoinIndexSignature], + ) -> Result<(), NymApiStorageError>; + + async fn get_partial_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, NymApiStorageError>; + async fn insert_partial_expiration_date_signatures( + &self, + expiration_date: Date, + sigs: &IssuedExpirationDateSignatures, + ) -> Result<(), NymApiStorageError>; + + async fn get_master_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, NymApiStorageError>; + async fn insert_master_expiration_date_signatures( + &self, + expiration_date: Date, + sigs: &IssuedExpirationDateSignatures, + ) -> Result<(), NymApiStorageError>; +} + +#[async_trait] +impl EcashStorageExt for NymApiStorage { + async fn get_double_spending_filter_params( + &self, + ) -> Result<(i64, BloomfilterParameters), NymApiStorageError> { + match self + .manager + .get_latest_double_spending_filter_params() + .await? + { + Some(raw) => Ok((raw.id, (&raw).try_into()?)), + None => { + let default = BloomfilterParameters::default_ecash(); + info!("using default bloomfilter parameters: {default:?}"); + let id = self + .manager + .insert_double_spending_filter_params( + default.num_hashes, + default.bitmap_size as u32, + &default.sip_keys[0].0.to_be_bytes(), + &default.sip_keys[0].1.to_be_bytes(), + &default.sip_keys[1].0.to_be_bytes(), + &default.sip_keys[1].1.to_be_bytes(), + ) + .await?; + Ok((id, default)) + } + } + } + + async fn update_archived_partial_bloomfilter( + &self, + date: Date, + new_bitmap: &[u8], + ) -> Result<(), NymApiStorageError> { + Ok(self + .manager + .update_archived_partial_bloomfilter(date, new_bitmap) + .await?) + } + + async fn try_load_partial_bloomfilter_bitmap( + &self, + date: Date, + params_id: i64, + ) -> Result>, NymApiStorageError> { + Ok(self + .manager + .try_load_partial_bloomfilter_bitmap(date, params_id) + .await?) + } + + async fn insert_partial_bloomfilter( + &self, + date: Date, + params_id: i64, + bitmap: &[u8], + ) -> Result<(), NymApiStorageError> { + Ok(self + .manager + .insert_partial_bloomfilter(date, params_id, bitmap) + .await?) + } + + async fn remove_old_partial_bloomfilters( + &self, + cutoff: Date, + ) -> Result<(), NymApiStorageError> { + Ok(self.manager.remove_old_partial_bloomfilters(cutoff).await?) + } + + async fn remove_expired_verified_tickets( + &self, + cutoff: Date, + ) -> Result<(), NymApiStorageError> { + Ok(self.manager.remove_expired_verified_tickets(cutoff).await?) + } + + async fn get_epoch_credentials( + &self, + epoch_id: EpochId, + ) -> Result, NymApiStorageError> { + Ok(self.manager.get_epoch_credentials(epoch_id).await?) + } + + async fn create_epoch_credentials_entry( + &self, + epoch_id: EpochId, + ) -> Result<(), NymApiStorageError> { + Ok(self + .manager + .create_epoch_credentials_entry(epoch_id) + .await?) + } + + async fn update_epoch_credentials_entry( + &self, + epoch_id: EpochId, + credential_id: i64, + ) -> Result<(), NymApiStorageError> { + Ok(self + .manager + .update_epoch_credentials_entry(epoch_id, credential_id) + .await?) + } + + async fn get_issued_credential( + &self, + credential_id: i64, + ) -> Result, NymApiStorageError> { + Ok(self.manager.get_issued_credential(credential_id).await?) + } + + async fn get_issued_bandwidth_credential_by_deposit_id( + &self, + deposit_id: DepositId, + ) -> Result, NymApiStorageError> { + Ok(self + .manager + .get_issued_bandwidth_credential_by_deposit_id(deposit_id) + .await?) + } + + #[allow(clippy::too_many_arguments)] + async fn store_issued_credential( + &self, + epoch_id: u32, + deposit_id: DepositId, + partial_credential: &BlindedSignature, + signature: identity::Signature, + private_commitments: Vec>, + expiration_date: Date, + ticketbook_type: TicketType, + ) -> Result { + Ok(self + .manager + .store_issued_ticketbook( + epoch_id, + deposit_id, + &partial_credential.to_bytes(), + &signature.to_bytes(), + &join_attributes(private_commitments), + expiration_date, + ticketbook_type.encode(), + ) + .await?) + } + + async fn get_issued_credentials( + &self, + credential_ids: Vec, + ) -> Result, NymApiStorageError> { + Ok(self.manager.get_issued_ticketbooks(credential_ids).await?) + } + + async fn get_issued_credentials_paged( + &self, + pagination: Pagination, + ) -> Result, NymApiStorageError> { + // rows start at 1 + let start_after = pagination.last_key.unwrap_or(0); + let limit = match pagination.limit { + Some(v) => { + if v == 0 || v > DEFAULT_CREDENTIALS_PAGE_LIMIT { + DEFAULT_CREDENTIALS_PAGE_LIMIT + } else { + v + } + } + None => DEFAULT_CREDENTIALS_PAGE_LIMIT, + }; + + Ok(self + .manager + .get_issued_ticketbooks_paged(start_after, limit) + .await?) + } + + // async fn insert_credential( + // &self, + // credential: &CredentialSpendingData, + // serial_number_bs58: String, + // gateway_addr: &AccountId, + // proposal_id: u64, + // ) -> Result<(), NymApiStorageError> { + // self.manager + // .insert_credential( + // credential.to_bs58(), + // serial_number_bs58, + // gateway_addr.to_string(), + // proposal_id as i64, + // ) + // .await + // .map_err(|err| err.into()) + // } + + async fn get_credential_data( + &self, + serial_number: &[u8], + ) -> Result, NymApiStorageError> { + let ticket = self.manager.get_ticket(serial_number).await?; + ticket + .map(|ticket| { + CredentialSpendingData::try_from_bytes(&ticket.ticket_data).map_err(|_| { + NymApiStorageError::DatabaseInconsistency { + reason: "impossible to deserialize verified ticket".to_string(), + } + }) + }) + .transpose() + } + + async fn store_verified_ticket( + &self, + ticket_data: &CredentialSpendingData, + gateway_addr: &AccountId, + ) -> Result<(), NymApiStorageError> { + let provider_id = self + .get_or_create_ticket_provider_with_id(gateway_addr.as_ref()) + .await?; + + let now = OffsetDateTime::now_utc(); + + let ticket_bytes = ticket_data.to_bytes(); + let encoded_serial_number = ticket_data.encoded_serial_number(); + self.manager + .insert_verified_ticket( + provider_id, + ticket_data.spend_date, + now, + ticket_bytes, + encoded_serial_number, + ) + .await + .map_err(Into::into) + } + + async fn get_ticket_provider( + &self, + gateway_address: &str, + ) -> Result, NymApiStorageError> { + self.manager + .get_ticket_provider(gateway_address) + .await + .map_err(Into::into) + } + + async fn get_verified_tickets_since( + &self, + provider_id: i64, + since: OffsetDateTime, + ) -> Result, NymApiStorageError> { + self.manager + .get_provider_ticket_serial_numbers(provider_id, since) + .await + .map_err(Into::into) + } + + async fn get_all_spent_tickets_on( + &self, + date: Date, + ) -> Result, NymApiStorageError> { + self.manager + .get_spent_tickets_on(date) + .await + .map_err(Into::into) + } + + async fn get_or_create_ticket_provider_with_id( + &self, + gateway_address: &str, + ) -> Result { + if let Some(provider) = self.get_ticket_provider(gateway_address).await? { + Ok(provider.id) + } else { + Ok(self.manager.insert_ticket_provider(gateway_address).await?) + } + } + + async fn get_master_verification_key( + &self, + epoch_id: EpochId, + ) -> Result, NymApiStorageError> { + let Some(raw) = self + .manager + .get_master_verification_key(epoch_id as i64) + .await? + else { + return Ok(None); + }; + + let master_vk = VerificationKeyAuth::from_bytes(&raw).map_err(|_| { + NymApiStorageError::database_inconsistency("malformed stored master verification key") + })?; + + Ok(Some(master_vk)) + } + + async fn insert_master_verification_key( + &self, + epoch_id: EpochId, + key: &VerificationKeyAuth, + ) -> Result<(), NymApiStorageError> { + Ok(self + .manager + .insert_master_verification_key(epoch_id as i64, &key.to_bytes()) + .await?) + } + + async fn get_partial_coin_index_signatures( + &self, + epoch_id: EpochId, + ) -> Result>, NymApiStorageError> { + let Some(raw) = self + .manager + .get_partial_coin_index_signatures(epoch_id as i64) + .await? + else { + return Ok(None); + }; + + Ok(Some(deserialise_coin_index_signatures(&raw)?)) + } + + async fn insert_partial_coin_index_signatures( + &self, + epoch_id: EpochId, + sigs: &[AnnotatedCoinIndexSignature], + ) -> Result<(), NymApiStorageError> { + self.manager + .insert_partial_coin_index_signatures( + epoch_id as i64, + &serialise_coin_index_signatures(sigs), + ) + .await?; + Ok(()) + } + + async fn get_master_coin_index_signatures( + &self, + epoch_id: EpochId, + ) -> Result>, NymApiStorageError> { + let Some(raw) = self + .manager + .get_master_coin_index_signatures(epoch_id as i64) + .await? + else { + return Ok(None); + }; + + Ok(Some(deserialise_coin_index_signatures(&raw)?)) + } + + async fn insert_master_coin_index_signatures( + &self, + epoch_id: EpochId, + sigs: &[AnnotatedCoinIndexSignature], + ) -> Result<(), NymApiStorageError> { + self.manager + .insert_master_coin_index_signatures( + epoch_id as i64, + &serialise_coin_index_signatures(sigs), + ) + .await?; + Ok(()) + } + + async fn get_partial_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, NymApiStorageError> { + let Some(raw) = self + .manager + .get_partial_expiration_date_signatures(expiration_date) + .await? + else { + return Ok(None); + }; + + let signatures = deserialise_expiration_date_signatures(&raw.serialised_signatures)?; + + Ok(Some(IssuedExpirationDateSignatures { + epoch_id: raw.epoch_id as u64, + signatures, + })) + } + + async fn insert_partial_expiration_date_signatures( + &self, + expiration_date: Date, + sigs: &IssuedExpirationDateSignatures, + ) -> Result<(), NymApiStorageError> { + self.manager + .insert_partial_expiration_date_signatures( + sigs.epoch_id as i64, + expiration_date, + &serialise_expiration_date_signatures(&sigs.signatures), + ) + .await?; + Ok(()) + } + + async fn get_master_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, NymApiStorageError> { + let Some(raw) = self + .manager + .get_master_expiration_date_signatures(expiration_date) + .await? + else { + return Ok(None); + }; + + let signatures = deserialise_expiration_date_signatures(&raw.serialised_signatures)?; + + Ok(Some(IssuedExpirationDateSignatures { + epoch_id: raw.epoch_id as u64, + signatures, + })) + } + + async fn insert_master_expiration_date_signatures( + &self, + expiration_date: Date, + sigs: &IssuedExpirationDateSignatures, + ) -> Result<(), NymApiStorageError> { + self.manager + .insert_master_expiration_date_signatures( + sigs.epoch_id as i64, + expiration_date, + &serialise_expiration_date_signatures(&sigs.signatures), + ) + .await?; + Ok(()) + } +} diff --git a/nym-api/src/ecash/storage/models.rs b/nym-api/src/ecash/storage/models.rs new file mode 100644 index 0000000000..6f150f238f --- /dev/null +++ b/nym-api/src/ecash/storage/models.rs @@ -0,0 +1,203 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::error::EcashError; +use crate::node_status_api::models::NymApiStorageError; +use nym_api_requests::ecash::models::{ + EpochCredentialsResponse, IssuedTicketbook as ApiIssuedCredential, + IssuedTicketbookBody as ApiIssuedCredentialInner, +}; +use nym_api_requests::ecash::BlindedSignatureResponse; +use nym_compact_ecash::BlindedSignature; +use nym_config::defaults::BloomfilterParameters; +use nym_credentials_interface::TicketType; +use nym_crypto::asymmetric::ed25519; +use nym_ecash_contract_common::deposit::DepositId; +use sqlx::FromRow; +use std::ops::Deref; +use time::{Date, OffsetDateTime}; + +pub struct EpochCredentials { + pub epoch_id: u32, + pub start_id: i64, + pub total_issued: u32, +} + +impl From for EpochCredentialsResponse { + fn from(value: EpochCredentials) -> Self { + let first_epoch_credential_id = if value.start_id == -1 { + None + } else { + Some(value.start_id) + }; + + EpochCredentialsResponse { + epoch_id: value.epoch_id as u64, + first_epoch_credential_id, + total_issued: value.total_issued, + } + } +} + +#[derive(FromRow)] +#[allow(unused)] +pub struct TicketProvider { + pub(crate) id: i64, + pub(crate) gateway_address: String, + pub(crate) last_batch_verification: Option, +} + +#[derive(FromRow)] +pub struct SerialNumberWrapper { + pub serial_number: Vec, +} + +impl Deref for SerialNumberWrapper { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.serial_number + } +} + +#[derive(FromRow)] +#[allow(unused)] +pub struct VerifiedTicket { + pub(crate) id: i64, + pub(crate) ticket_data: Vec, + pub(crate) serial_number: Vec, + pub(crate) spending_date: Date, + pub(crate) verified_at: OffsetDateTime, + pub(crate) gateway_id: i64, +} + +#[derive(FromRow)] +pub struct IssuedTicketbook { + pub id: i64, + pub epoch_id: u32, + pub deposit_id: DepositId, + + pub partial_credential: Vec, + + /// signature on the issued credential (and the attributes) + pub signature: Vec, + + // i.e. "'attr1','attr2',..." + pub joined_private_commitments: Vec, + + pub expiration_date: Date, + + pub ticketbook_type_repr: u8, +} + +#[derive(FromRow)] +pub struct RawExpirationDateSignatures { + pub epoch_id: u32, + pub serialised_signatures: Vec, +} + +#[derive(FromRow)] +pub(crate) struct StoredBloomfilterParams { + pub(crate) id: i64, + pub(crate) num_hashes: u32, + pub(crate) bitmap_size: u32, + + pub(crate) sip0_key0: Vec, + pub(crate) sip0_key1: Vec, + + pub(crate) sip1_key0: Vec, + pub(crate) sip1_key1: Vec, +} + +impl<'a> TryFrom<&'a StoredBloomfilterParams> for BloomfilterParameters { + type Error = NymApiStorageError; + fn try_from(value: &'a StoredBloomfilterParams) -> Result { + let Ok(sip0_key0) = <[u8; 8]>::try_from(value.sip0_key0.as_ref()) else { + return Err(NymApiStorageError::database_inconsistency( + "malformed sip0 key0", + )); + }; + let Ok(sip0_key1) = <[u8; 8]>::try_from(value.sip0_key1.as_ref()) else { + return Err(NymApiStorageError::database_inconsistency( + "malformed sip0 key1", + )); + }; + let Ok(sip1_key0) = <[u8; 8]>::try_from(value.sip1_key0.as_ref()) else { + return Err(NymApiStorageError::database_inconsistency( + "malformed sip1 key0", + )); + }; + let Ok(sip1_key1) = <[u8; 8]>::try_from(value.sip1_key1.as_ref()) else { + return Err(NymApiStorageError::database_inconsistency( + "malformed sip1 key1", + )); + }; + Ok(BloomfilterParameters { + num_hashes: value.num_hashes, + bitmap_size: value.bitmap_size as u64, + sip_keys: [ + (u64::from_be_bytes(sip0_key0), u64::from_be_bytes(sip0_key1)), + (u64::from_be_bytes(sip1_key0), u64::from_be_bytes(sip1_key1)), + ], + }) + } +} + +impl TryFrom for ApiIssuedCredentialInner { + type Error = EcashError; + + fn try_from(value: IssuedTicketbook) -> Result { + Ok(ApiIssuedCredentialInner { + credential: ApiIssuedCredential { + id: value.id, + epoch_id: value.epoch_id, + deposit_id: value.deposit_id, + blinded_partial_credential: BlindedSignature::from_bytes( + &value.partial_credential, + )?, + encoded_private_attributes_commitments: split_attributes( + value.joined_private_commitments, + ), + expiration_date: value.expiration_date, + ticketbook_type: TicketType::try_from_encoded(value.ticketbook_type_repr)?, + }, + signature: ed25519::Signature::from_bytes(&value.signature)?, + }) + } +} + +impl TryFrom for BlindedSignatureResponse { + type Error = EcashError; + + fn try_from(value: IssuedTicketbook) -> Result { + Ok(BlindedSignatureResponse { + blinded_signature: BlindedSignature::from_bytes(&value.partial_credential)?, + }) + } +} + +impl TryFrom for BlindedSignature { + type Error = EcashError; + + fn try_from(value: IssuedTicketbook) -> Result { + Ok(BlindedSignature::from_bytes(&value.partial_credential)?) + } +} + +pub(crate) fn join_attributes(attrs: Vec>) -> Vec { + // note: 48 is length of encoded G1 element + let mut out = Vec::with_capacity(48 * attrs.len()); + for mut attr in attrs { + // since this is called internally only, we expect valid attributes here! + assert_eq!(attr.len(), 48); + + out.append(&mut attr) + } + + out +} + +pub(crate) fn split_attributes(attrs: Vec) -> Vec> { + assert_eq!(attrs.len() % 48, 0, "database corruption"); + attrs.chunks_exact(48).map(|c| c.to_vec()).collect() +} diff --git a/nym-api/src/coconut/tests/fixtures.rs b/nym-api/src/ecash/tests/fixtures.rs similarity index 93% rename from nym-api/src/coconut/tests/fixtures.rs rename to nym-api/src/ecash/tests/fixtures.rs index d883d122ba..80ee791770 100644 --- a/nym-api/src/coconut/tests/fixtures.rs +++ b/nym-api/src/ecash/tests/fixtures.rs @@ -1,17 +1,17 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg; -use crate::coconut::dkg::client::DkgClient; -use crate::coconut::dkg::controller::keys::persist_coconut_keypair; -use crate::coconut::dkg::controller::DkgController; -use crate::coconut::dkg::state::State; -use crate::coconut::keys::KeyPair; -use crate::coconut::tests::{DummyClient, SharedFakeChain}; +use crate::ecash::dkg; +use crate::ecash::dkg::client::DkgClient; +use crate::ecash::dkg::controller::keys::persist_coconut_keypair; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::dkg::state::State; +use crate::ecash::keys::KeyPair; +use crate::ecash::tests::{DummyClient, SharedFakeChain}; use cosmwasm_std::Addr; -use nym_coconut::VerificationKey; use nym_coconut_dkg_common::dealer::DealerRegistrationDetails; use nym_coconut_dkg_common::types::{DealerDetails, EpochId}; +use nym_compact_ecash::VerificationKeyAuth; use nym_crypto::asymmetric::identity; use nym_dkg::bte::keys::KeyPair as DkgKeyPair; use nym_dkg::{NodeIndex, Threshold}; @@ -170,7 +170,11 @@ impl TestingDkgControllerBuilder { state_guard.dkg_contract.epoch.epoch_id = epoch_id; } if let Some(threshold) = self.threshold { - state_guard.dkg_contract.threshold = Some(threshold) + let epoch_id = state_guard.dkg_contract.epoch.epoch_id; + state_guard + .dkg_contract + .threshold + .insert(epoch_id, threshold); } let epoch_id = state_guard.dkg_contract.epoch.epoch_id; @@ -266,7 +270,7 @@ impl TestingDkgController { Addr::unchecked(self.address().await.as_ref()) } - pub(crate) async fn unchecked_coconut_vk(&self) -> VerificationKey { + pub(crate) async fn unchecked_coconut_vk(&self) -> VerificationKeyAuth { self.state .unchecked_coconut_keypair() .await diff --git a/nym-api/src/coconut/tests/helpers.rs b/nym-api/src/ecash/tests/helpers.rs similarity index 96% rename from nym-api/src/coconut/tests/helpers.rs rename to nym-api/src/ecash/tests/helpers.rs index a92a448502..5cd410056f 100644 --- a/nym-api/src/coconut/tests/helpers.rs +++ b/nym-api/src/ecash/tests/helpers.rs @@ -1,8 +1,8 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::tests::fixtures::{TestingDkgController, TestingDkgControllerBuilder}; -use crate::coconut::tests::SharedFakeChain; +use crate::ecash::tests::fixtures::{TestingDkgController, TestingDkgControllerBuilder}; +use crate::ecash::tests::SharedFakeChain; use nym_coconut_dkg_common::types::EpochState; use nym_dkg::bte::PublicKeyWithProof; @@ -80,7 +80,7 @@ pub(crate) async fn submit_public_keys(controllers: &mut [TestingDkgController], let mut guard = controllers[0].chain_state.lock().unwrap(); guard.dkg_contract.epoch.state = EpochState::DealingExchange { resharing }; - guard.dkg_contract.threshold = Some(threshold) + guard.dkg_contract.threshold.insert(epoch, threshold); } pub(crate) async fn exchange_dealings(controllers: &mut [TestingDkgController], resharing: bool) { diff --git a/nym-api/src/coconut/tests/issued_credentials.rs b/nym-api/src/ecash/tests/issued_credentials.rs similarity index 80% rename from nym-api/src/coconut/tests/issued_credentials.rs rename to nym-api/src/ecash/tests/issued_credentials.rs index fadfdad4e8..5a86d70923 100644 --- a/nym-api/src/coconut/tests/issued_credentials.rs +++ b/nym-api/src/ecash/tests/issued_credentials.rs @@ -1,21 +1,20 @@ -// Copyright 2023 - Nym Technologies SA +// Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::tests::{voucher_fixture, TestFixture}; -use cosmwasm_std::coin; -use nym_api_requests::coconut::models::{ +use crate::ecash::tests::{voucher_fixture, TestFixture}; +use nym_api_requests::ecash::models::{ EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse, Pagination, }; -use nym_api_requests::coconut::CredentialsRequestBody; -use nym_validator_client::nym_api::routes::{API_VERSION, BANDWIDTH, COCONUT_ROUTES}; +use nym_api_requests::ecash::CredentialsRequestBody; +use nym_validator_client::nym_api::routes::{API_VERSION, ECASH_ROUTES}; use rocket::http::Status; use std::collections::BTreeMap; #[tokio::test] async fn epoch_credentials() { - let route_epoch1 = format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/epoch-credentials/1"); - let route_epoch2 = format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/epoch-credentials/2"); - let route_epoch42 = format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/epoch-credentials/42"); + let route_epoch1 = format!("/{API_VERSION}/{ECASH_ROUTES}/epoch-credentials/1"); + let route_epoch2 = format!("/{API_VERSION}/{ECASH_ROUTES}/epoch-credentials/2"); + let route_epoch42 = format!("/{API_VERSION}/{ECASH_ROUTES}/epoch-credentials/42"); let test_fixture = TestFixture::new().await; @@ -94,27 +93,25 @@ async fn epoch_credentials() { #[tokio::test] async fn issued_credential() { fn route(id: i64) -> String { - format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/issued-credential/{id}") + format!("/{API_VERSION}/{ECASH_ROUTES}/issued-credential/{id}") } // let test_fixture = TestFixture::new() - let hash1 = "6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E".to_string(); - let hash2 = "9F4DF28B36189B4410BC23D97FD757FC74B919122E80534CC2CA6F3D646F6518".to_string(); + let deposit_id1 = 123; + let deposit_id2 = 321; - let voucher1 = voucher_fixture(coin(1234, "unym"), Some(hash1.clone())); - let voucher2 = voucher_fixture(coin(1234, "unym"), Some(hash2.clone())); + let voucher1 = voucher_fixture(Some(deposit_id1)); + let voucher2 = voucher_fixture(Some(deposit_id2)); let signing_data1 = voucher1.prepare_for_signing(); - let voucher_data1 = voucher1.get_variant_data().voucher_data().unwrap(); - let request1 = voucher_data1.create_blind_sign_request_body(&signing_data1); + let request1 = voucher1.create_blind_sign_request_body(&signing_data1); let signing_data2 = voucher2.prepare_for_signing(); - let voucher_data2 = voucher2.get_variant_data().voucher_data().unwrap(); - let request2 = voucher_data2.create_blind_sign_request_body(&signing_data2); + let request2 = voucher2.create_blind_sign_request_body(&signing_data2); let test_fixture = TestFixture::new().await; - test_fixture.add_deposit_tx(voucher_data1); - test_fixture.add_deposit_tx(voucher_data2); + test_fixture.add_deposit(&voucher1); + test_fixture.add_deposit(&voucher2); // random credential that was never issued let response = test_fixture.rocket.get(route(42)).dispatch().await; @@ -143,44 +140,42 @@ async fn issued_credential() { // TODO: currently we have no signature checks assert_eq!(1, issued1.credential.id); assert_eq!(1, issued1.credential.epoch_id); - assert_eq!(voucher_data1.tx_hash(), issued1.credential.tx_hash); + assert_eq!(voucher1.deposit_id(), issued1.credential.deposit_id); assert_eq!( cred1.to_bytes(), issued1.credential.blinded_partial_credential.to_bytes() ); + assert_eq!( request1.encode_commitments(), - issued1 - .credential - .bs58_encoded_private_attributes_commitments + issued1.credential.encoded_private_attributes_commitments ); assert_eq!( - voucher1.get_plain_public_attributes(), - issued1.credential.public_attributes + voucher1.expiration_date(), + issued1.credential.expiration_date ); assert_eq!(2, issued2.credential.id); assert_eq!(3, issued2.credential.epoch_id); - assert_eq!(voucher_data2.tx_hash(), issued2.credential.tx_hash); + assert_eq!(voucher2.deposit_id(), issued2.credential.deposit_id); assert_eq!( cred2.to_bytes(), issued2.credential.blinded_partial_credential.to_bytes() ); + assert_eq!( request2.encode_commitments(), - issued2 - .credential - .bs58_encoded_private_attributes_commitments + issued2.credential.encoded_private_attributes_commitments ); assert_eq!( - voucher2.get_plain_public_attributes(), - issued2.credential.public_attributes + voucher2.expiration_date(), + issued2.credential.expiration_date ); } #[tokio::test] async fn issued_credentials() { - let route = format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/issued-credentials"); + let route = format!("/{API_VERSION}/{ECASH_ROUTES}/issued-credentials"); let test_fixture = TestFixture::new().await; diff --git a/nym-api/src/coconut/tests/mod.rs b/nym-api/src/ecash/tests/mod.rs similarity index 59% rename from nym-api/src/coconut/tests/mod.rs rename to nym-api/src/ecash/tests/mod.rs index 43f3de399e..fb037a85f0 100644 --- a/nym-api/src/coconut/tests/mod.rs +++ b/nym-api/src/ecash/tests/mod.rs @@ -1,26 +1,21 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::error::{CoconutError, Result}; -use crate::coconut::keys::KeyPairWithEpoch; -use crate::coconut::state::State; -use crate::coconut::storage::CoconutStorageExt; +use crate::ecash; +use crate::ecash::error::{EcashError, Result}; +use crate::ecash::keys::KeyPairWithEpoch; +use crate::ecash::state::EcashState; +use crate::ecash::storage::EcashStorageExt; use crate::support::storage::NymApiStorage; use async_trait::async_trait; use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::{ - coin, from_binary, to_binary, Addr, Binary, BlockInfo, CosmosMsg, Decimal, MessageInfo, WasmMsg, + from_binary, to_binary, Addr, Binary, BlockInfo, CosmosMsg, Decimal, MessageInfo, WasmMsg, }; use cw3::{Proposal, ProposalResponse, Vote, VoteInfo, VoteResponse, Votes}; use cw4::{Cw4Contract, MemberResponse}; -use nym_api_requests::coconut::models::{IssuedCredentialBody, IssuedCredentialResponse}; -use nym_api_requests::coconut::{BlindSignRequestBody, BlindedSignatureResponse}; -use nym_coconut::{BlindedSignature, Parameters, VerificationKey}; -use nym_coconut_bandwidth_contract_common::events::{ - DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ENCRYPTION_KEY, DEPOSIT_IDENTITY_KEY, DEPOSIT_INFO, - DEPOSIT_VALUE, -}; -use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; +use nym_api_requests::ecash::models::{IssuedCredentialResponse, IssuedTicketbookBody}; +use nym_api_requests::ecash::{BlindSignRequestBody, BlindedSignatureResponse}; use nym_coconut_dkg_common::dealer::{ DealerDetails, DealerDetailsResponse, DealerType, RegisteredDealerDetails, }; @@ -34,21 +29,20 @@ use nym_coconut_dkg_common::types::{ EpochId, EpochState, PartialContractDealingData, State as ContractState, }; use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; +use nym_compact_ecash::BlindedSignature; +use nym_compact_ecash::{ttp_keygen, VerificationKeyAuth}; use nym_contracts_common::IdentityKey; -use nym_credentials::coconut::bandwidth::voucher::BandwidthVoucherIssuanceData; -use nym_credentials::coconut::bandwidth::CredentialType; -use nym_credentials::IssuanceBandwidthCredential; -use nym_crypto::asymmetric::{encryption, identity}; +use nym_credentials::IssuanceTicketBook; +use nym_credentials_interface::TicketType; +use nym_crypto::asymmetric::identity; use nym_dkg::{NodeIndex, Threshold}; -use nym_validator_client::nym_api::routes::{ - API_VERSION, BANDWIDTH, COCONUT_BLIND_SIGN, COCONUT_ROUTES, -}; +use nym_ecash_contract_common::blacklist::{BlacklistedAccountResponse, Blacklisting}; +use nym_ecash_contract_common::deposit::{Deposit, DepositId, DepositResponse}; +use nym_validator_client::nym_api::routes::{API_VERSION, ECASH_BLIND_SIGN, ECASH_ROUTES}; use nym_validator_client::nyxd::cosmwasm_client::logs::Log; use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult; -use nym_validator_client::nyxd::Coin; -use nym_validator_client::nyxd::{ - AccountId, Algorithm, Event, EventAttribute, ExecTxResult, Fee, Hash, TxResponse, -}; +use nym_validator_client::nyxd::{AccountId, ExecTxResult, Fee, Hash, TxResponse}; +use nym_validator_client::{EcashApiClient, NymApiClient}; use rand::rngs::OsRng; use rand::RngCore; use rocket::http::Status; @@ -59,6 +53,7 @@ use std::str::FromStr; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; use tempfile::{tempdir, TempDir}; +use tokio::sync::RwLock; pub(crate) mod fixtures; pub(crate) mod helpers; @@ -72,6 +67,9 @@ struct InternalCounters { node_index_counter: NodeIndex, tx_hash_counter: u64, proposal_id_counter: u64, + + #[allow(dead_code)] + deposit_id_counter: u32, } impl InternalCounters { @@ -92,6 +90,12 @@ impl InternalCounters { self.tx_hash_counter += 1; Hash::Sha256(sha2::Sha256::digest(&self.tx_hash_counter.to_be_bytes()).into()) } + + #[allow(dead_code)] + fn next_deposit_id(&mut self) -> DepositId { + self.deposit_id_counter += 1; + self.deposit_id_counter + } } #[derive(Debug)] @@ -151,7 +155,7 @@ pub(crate) struct FakeDkgContractState { pub(crate) epoch: Epoch, pub(crate) contract_state: ContractState, - pub(crate) threshold: Option, + pub(crate) threshold: HashMap, } impl FakeDkgContractState { @@ -168,9 +172,7 @@ impl FakeDkgContractState { // .collect() // } - fn reset_dkg_state(&mut self) { - self.threshold = None; - } + fn reset_dkg_state(&mut self) {} pub(crate) fn reset_epoch_in_reshare_mode(&mut self) { self.reset_dkg_state(); @@ -267,16 +269,17 @@ pub(crate) struct FakeMultisigContractState { } impl FakeMultisigContractState { + #[allow(dead_code)] pub(crate) fn reset_votes(&mut self) { self.votes = HashMap::new() } } #[derive(Debug)] -pub(crate) struct FakeBandwidthContractState { +pub(crate) struct FakeEcashContractState { pub(crate) address: Addr, - pub(crate) admin: Option, - pub(crate) spent_credentials: HashMap, + pub(crate) deposits: HashMap, + pub(crate) blacklist: HashMap, } #[derive(Debug, Clone, Default)] @@ -300,7 +303,7 @@ pub(crate) struct FakeChainState { pub(crate) dkg_contract: FakeDkgContractState, pub(crate) group_contract: FakeGroupContractState, pub(crate) multisig_contract: FakeMultisigContractState, - pub(crate) bandwidth_contract: FakeBandwidthContractState, + pub(crate) ecash_contract: FakeEcashContractState, } impl Default for FakeChainState { @@ -311,14 +314,9 @@ impl Default for FakeChainState { Addr::unchecked("n1pd7kfgvr5tpcv0xnlv46c4jsq9jg2r799xxrcwqdm4l2jhq2pjwqrmz5ju"); let dkg_contract = Addr::unchecked("n1ahg0erc2fs6xx3j5m8sfx3ryuzdjh6kf6qm9plsf865fltekyrfsesac6a"); - let bandwidth_contract = + let ecash_contract = Addr::unchecked("n16a32stm6kknhq5cc8rx77elr66pygf2hfszw7wvpq746x3uffylqkjar4l"); - let bandwidth_contract_admin = - "n1ahg0erc2fs6xx3j5m8sfx3ryuzdjh6kf6qm9plsf865fltekyrfsesac6a" - .parse() - .unwrap(); - FakeChainState { _counters: Default::default(), @@ -339,7 +337,7 @@ impl Default for FakeChainState { }, dealings: HashMap::new(), verification_shares: HashMap::new(), - threshold: None, + threshold: HashMap::new(), }, group_contract: FakeGroupContractState { address: group_contract, @@ -350,10 +348,10 @@ impl Default for FakeChainState { proposals: Default::default(), votes: Default::default(), }, - bandwidth_contract: FakeBandwidthContractState { - address: bandwidth_contract, - admin: Some(bandwidth_contract_admin), - spent_credentials: Default::default(), + ecash_contract: FakeEcashContractState { + address: ecash_contract, + deposits: Default::default(), + blacklist: Default::default(), }, } } @@ -380,6 +378,7 @@ impl FakeChainState { self.group_contract.add_member(address, weight) } + #[allow(dead_code)] pub(crate) fn reset_votes(&mut self) { self.multisig_contract.reset_votes() } @@ -429,7 +428,7 @@ impl FakeChainState { if contract == &self.multisig_contract.address { unimplemented!("multisig contract exec") } - if contract == &self.bandwidth_contract.address { + if contract == &self.ecash_contract.address { unimplemented!("bandwidth contract exec") } if contract == self.dkg_contract.address.as_ref() { @@ -504,109 +503,6 @@ impl DummyClient { pub fn chain_state(&self) -> SharedFakeChain { self.state.clone() } - - // pub fn with_tx_db(mut self, tx_db: &Arc>>) -> Self { - // todo!() - // // self.tx_db = Arc::clone(tx_db); - // // self - // } - // - // pub fn with_proposal_db( - // mut self, - // proposal_db: &Arc>>, - // ) -> Self { - // todo!() - // // self.proposal_db = Arc::clone(proposal_db); - // // self - // } - // - // pub fn with_spent_credential_db( - // mut self, - // spent_credential_db: &Arc>>, - // ) -> Self { - // todo!() - // // self.spent_credential_db = Arc::clone(spent_credential_db); - // // self - // } - // - // pub fn _with_epoch(mut self, epoch: &Arc>) -> Self { - // todo!() - // // self.epoch = Arc::clone(epoch); - // // self - // } - // - // pub fn with_dealer_details( - // mut self, - // dealer_details: &Arc>>, - // ) -> Self { - // todo!() - // // self.dealer_details = Arc::clone(dealer_details); - // // self - // } - // - // pub fn with_threshold(mut self, threshold: &Arc>>) -> Self { - // todo!() - // // self.threshold = Arc::clone(threshold); - // // self - // } - // - // // it's a really bad practice, but I'm not going to be changing it now... - // #[allow(clippy::type_complexity)] - // pub fn with_dealings( - // mut self, - // dealings: &Arc>>>>, - // ) -> Self { - // todo!() - // // self.dealings = Arc::clone(dealings); - // // self - // } - // - // pub fn with_verification_share( - // mut self, - // verification_share: &Arc>>, - // ) -> Self { - // todo!() - // // self.verification_share = Arc::clone(verification_share); - // // self - // } - // - // pub fn _with_group_db( - // mut self, - // group_db: &Arc>>, - // ) -> Self { - // todo!() - // // self.group_db = Arc::clone(group_db); - // // self - // } - // - // pub fn with_initial_dealers_db( - // mut self, - // initial_dealers: &Arc>>, - // ) -> Self { - // todo!() - // // self.initial_dealers_db = Arc::clone(initial_dealers); - // // self - // } - - // async fn get_dealer_by_address(&self, address: &str) -> Option { - // let guard = self.state.lock().unwrap(); - // for dealer in guard.dkg_contract.dealers.values() { - // if dealer.address.as_str() == address { - // return Some(dealer.clone()); - // } - // } - // None - // } - // - // async fn get_past_dealer_by_address(&self, address: &str) -> Option { - // let guard = self.state.lock().unwrap(); - // for dealer in guard.dkg_contract.past_dealers.values() { - // if dealer.address.as_str() == address { - // return Some(dealer.clone()); - // } - // } - // None - // } } #[async_trait] @@ -619,19 +515,20 @@ impl super::client::Client for DummyClient { Ok(self.state.lock().unwrap().dkg_contract.address.clone()) } - async fn bandwidth_contract_admin(&self) -> Result> { - Ok(self.state.lock().unwrap().bandwidth_contract.admin.clone()) - } - - async fn get_tx(&self, tx_hash: Hash) -> Result { - Ok(self + async fn get_deposit(&self, deposit_id: DepositId) -> Result { + let deposit = self .state .lock() .unwrap() - .txs - .get(&tx_hash) - .cloned() - .unwrap()) + .ecash_contract + .deposits + .get(&deposit_id) + .cloned(); + + Ok(DepositResponse { + id: deposit_id, + deposit, + }) } async fn get_proposal(&self, proposal_id: u64) -> Result { @@ -641,7 +538,7 @@ impl super::client::Client for DummyClient { .proposals .get(&proposal_id) .cloned() - .ok_or(CoconutError::IncorrectProposal { + .ok_or(EcashError::IncorrectProposal { reason: String::from("proposal not found"), })?; @@ -678,20 +575,19 @@ impl super::client::Client for DummyClient { Ok(VoteResponse { vote }) } - async fn get_spent_credential( + async fn get_blacklisted_account( &self, - blinded_serial_number: String, - ) -> Result { - self.state - .lock() - .unwrap() - .bandwidth_contract - .spent_credentials - .get(&blinded_serial_number) - .cloned() - .ok_or(CoconutError::InvalidCredentialStatus { - status: String::from("spent credential not found"), - }) + public_key: String, + ) -> Result { + Ok(BlacklistedAccountResponse::new( + self.state + .lock() + .unwrap() + .ecash_contract + .blacklist + .get(&public_key) + .cloned(), + )) } async fn contract_state(&self) -> Result { @@ -721,7 +617,20 @@ impl super::client::Client for DummyClient { } async fn get_current_epoch_threshold(&self) -> Result> { - Ok(self.state.lock().unwrap().dkg_contract.threshold) + let guard = self.state.lock().unwrap(); + let current_epoch = guard.dkg_contract.epoch.epoch_id; + Ok(guard.dkg_contract.threshold.get(¤t_epoch).cloned()) + } + + async fn get_epoch_threshold(&self, epoch_id: EpochId) -> Result> { + Ok(self + .state + .lock() + .unwrap() + .dkg_contract + .threshold + .get(&epoch_id) + .cloned()) } async fn get_self_registered_dealer_details(&self) -> Result { @@ -915,6 +824,15 @@ impl super::client::Client for DummyClient { } } + async fn get_registered_ecash_clients(&self, epoch_id: EpochId) -> Result> { + Ok(self + .get_verification_key_shares(epoch_id) + .await? + .into_iter() + .map(|s| s.try_into().unwrap()) + .collect()) + } + async fn vote_proposal( &self, proposal_id: u64, @@ -924,7 +842,7 @@ impl super::client::Client for DummyClient { let voter = self.validator_address.to_string(); let mut chain = self.state.lock().unwrap(); if !chain.multisig_contract.proposals.contains_key(&proposal_id) { - return Err(CoconutError::IncorrectProposal { + return Err(EcashError::IncorrectProposal { reason: String::from("proposal not found"), }); } @@ -970,7 +888,7 @@ impl super::client::Client for DummyClient { let multisig_address: AccountId = chain.multisig_contract.address.as_str().parse().unwrap(); let Some(proposal) = chain.multisig_contract.proposals.get_mut(&proposal_id) else { - return Err(CoconutError::ProposalIdError { + return Err(EcashError::ProposalIdError { reason: String::from("proposal id not found"), }); }; @@ -1034,8 +952,8 @@ impl super::client::Client for DummyClient { events: vec![cosmwasm_std::Event::new("wasm") .add_attribute(NODE_INDEX, assigned_index.to_string())], }], + msg_responses: Default::default(), events: Default::default(), - data: Default::default(), transaction_hash, gas_info: Default::default(), }) @@ -1068,8 +986,8 @@ impl super::client::Client for DummyClient { Ok(ExecuteResult { logs: vec![], + msg_responses: Default::default(), events: Default::default(), - data: Default::default(), transaction_hash, gas_info: Default::default(), }) @@ -1103,8 +1021,8 @@ impl super::client::Client for DummyClient { Ok(ExecuteResult { logs: vec![], + msg_responses: Default::default(), events: Default::default(), - data: Default::default(), transaction_hash, gas_info: Default::default(), }) @@ -1121,7 +1039,7 @@ impl super::client::Client for DummyClient { let epoch_id = chain.dkg_contract.epoch.epoch_id; let Some(dealer_details) = chain.dkg_contract.get_dealer_details(&address, epoch_id) else { // Just throw some error, not really the correct one - return Err(CoconutError::DepositEncrKeyNotFound); + return Err(EcashError::DepositInfoNotFound); }; let dkg_contract = chain.dkg_contract.address.clone(); @@ -1180,28 +1098,43 @@ impl super::client::Client for DummyClient { events: vec![cosmwasm_std::Event::new("wasm") .add_attribute(DKG_PROPOSAL_ID, proposal_id.to_string())], }], + msg_responses: Default::default(), events: Default::default(), - data: Default::default(), transaction_hash, gas_info: Default::default(), }) } } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct DummyCommunicationChannel { current_epoch: Arc, - aggregated_verification_key: VerificationKey, + ecash_clients: Arc>>>, } impl DummyCommunicationChannel { - pub fn new(aggregated_verification_key: VerificationKey) -> Self { + pub fn new(ecash_clients: Vec) -> Self { + let epoch_id = 1; + let mut ecash_clients_map = HashMap::new(); + ecash_clients_map.insert(epoch_id, ecash_clients); DummyCommunicationChannel { - current_epoch: Arc::new(AtomicU64::new(1)), - aggregated_verification_key, + current_epoch: Arc::new(AtomicU64::new(epoch_id)), + ecash_clients: Arc::new(RwLock::new(ecash_clients_map)), } } + pub fn new_single_dummy(aggregated_verification_key: VerificationKeyAuth) -> Self { + let client = EcashApiClient { + api_client: NymApiClient::new("http://localhost:1234".parse().unwrap()), + verification_key: aggregated_verification_key, + node_id: 1, + cosmos_address: "n16a32stm6kknhq5cc8rx77elr66pygf2hfszw7wvpq746x3uffylqkjar4l" + .parse() + .unwrap(), + }; + Self::new(vec![client]) + } + pub fn with_epoch(mut self, current_epoch: Arc) -> Self { self.current_epoch = current_epoch; self @@ -1214,11 +1147,37 @@ impl super::comm::APICommunicationChannel for DummyCommunicationChannel { Ok(self.current_epoch.load(Ordering::Relaxed)) } - async fn aggregated_verification_key(&self, _epoch_id: EpochId) -> Result { - Ok(self.aggregated_verification_key.clone()) + async fn dkg_in_progress(&self) -> Result { + // deal with this later lol + Ok(false) + } + + async fn ecash_clients(&self, epoch_id: EpochId) -> Result> { + Ok(self + .ecash_clients + .read() + .await + .get(&epoch_id) + .cloned() + .unwrap_or_default()) + } + + async fn ecash_threshold(&self, _epoch_id: EpochId) -> Result { + todo!() } } +#[allow(dead_code)] +pub fn deposit_fixture() -> Deposit { + let mut rng = OsRng; + let identity_keypair = identity::KeyPair::new(&mut rng); + + Deposit { + bs58_encoded_ed25519_pubkey: identity_keypair.public_key().to_base58_string(), + } +} + +#[allow(dead_code)] pub fn tx_entry_fixture(hash: Hash) -> TxResponse { TxResponse { hash, @@ -1239,53 +1198,6 @@ pub fn tx_entry_fixture(hash: Hash) -> TxResponse { } } -pub fn deposit_tx_fixture(voucher_data: &BandwidthVoucherIssuanceData) -> TxResponse { - TxResponse { - hash: voucher_data.tx_hash(), - height: Default::default(), - index: 0, - tx_result: ExecTxResult { - code: Default::default(), - data: Default::default(), - log: "".to_string(), - info: "".to_string(), - gas_wanted: 0, - gas_used: 0, - events: vec![Event { - kind: format!("wasm-{}", DEPOSITED_FUNDS_EVENT_TYPE), - attributes: vec![ - EventAttribute { - key: DEPOSIT_VALUE.to_string(), - value: voucher_data.value_plain(), - index: false, - }, - EventAttribute { - key: DEPOSIT_INFO.to_string(), - value: CredentialType::Voucher.to_string(), - index: false, - }, - EventAttribute { - key: DEPOSIT_IDENTITY_KEY.to_string(), - value: voucher_data.identity_key().public_key().to_base58_string(), - index: false, - }, - EventAttribute { - key: DEPOSIT_ENCRYPTION_KEY.parse().unwrap(), - value: voucher_data - .encryption_key() - .public_key() - .to_base58_string(), - index: false, - }, - ], - }], - codespace: "".to_string(), - }, - tx: vec![], - proof: None, - } -} - pub fn blinded_signature_fixture() -> BlindedSignature { let gen1_bytes = [ 151u8, 241, 211, 167, 49, 151, 215, 148, 38, 149, 99, 140, 79, 169, 172, 15, 195, 104, 140, @@ -1302,25 +1214,17 @@ pub fn blinded_signature_fixture() -> BlindedSignature { BlindedSignature::from_bytes(&dummy_bytes).unwrap() } -pub fn voucher_fixture>( - amount: C, - tx_hash: Option, -) -> IssuanceBandwidthCredential { +pub fn voucher_fixture(deposit_id: Option) -> IssuanceTicketBook { let mut rng = OsRng; - let tx_hash = if let Some(provided) = &tx_hash { - provided.parse().unwrap() - } else { - Hash::from_str("6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E").unwrap() - }; + let deposit_id = deposit_id.unwrap_or(69); let identity_keypair = identity::KeyPair::new(&mut rng); - let encryption_keypair = encryption::KeyPair::new(&mut rng); + let id_priv = identity::PrivateKey::from_bytes(&identity_keypair.private_key().to_bytes()).unwrap(); - let enc_priv = - encryption::PrivateKey::from_bytes(&encryption_keypair.private_key().to_bytes()).unwrap(); - - IssuanceBandwidthCredential::new_voucher(amount.into(), tx_hash, id_priv, enc_priv) + let identifier = [44u8; 32]; + // (voucher, request) + IssuanceTicketBook::new(deposit_id, identifier, id_priv, TicketType::V1MixnetEntry) } fn dummy_signature() -> identity::Signature { @@ -1340,13 +1244,12 @@ struct TestFixture { impl TestFixture { async fn new() -> Self { - let mut rng = crate::coconut::tests::fixtures::test_rng([69u8; 32]); - let params = Parameters::new(4).unwrap(); - let coconut_keypair = nym_coconut::ttp_keygen(¶ms, 1, 1).unwrap().remove(0); + let mut rng = crate::ecash::tests::fixtures::test_rng([69u8; 32]); + let coconut_keypair = ttp_keygen(1, 1).unwrap().remove(0); let identity = identity::KeyPair::new(&mut rng); let epoch = Arc::new(AtomicU64::new(1)); let comm_channel = - DummyCommunicationChannel::new(coconut_keypair.verification_key().clone()) + DummyCommunicationChannel::new_single_dummy(coconut_keypair.verification_key().clone()) .with_epoch(epoch.clone()); // TODO: it's AWFUL to test with actual storage, we should somehow abstract it away @@ -1355,7 +1258,7 @@ impl TestFixture { .await .unwrap(); - let staged_key_pair = crate::coconut::KeyPair::new(); + let staged_key_pair = crate::ecash::keys::KeyPair::new(); staged_key_pair .set(KeyPairWithEpoch { keys: coconut_keypair, @@ -1370,14 +1273,33 @@ impl TestFixture { chain_state.clone(), ); - let rocket = rocket::build().attach(crate::coconut::stage( - nyxd_client, - TEST_COIN_DENOM.to_string(), - identity, - staged_key_pair, - comm_channel, - storage.clone(), - )); + let ecash_contract = chain_state + .lock() + .unwrap() + .ecash_contract + .address + .clone() + .as_str() + .parse() + .unwrap(); + + let rocket = rocket::build() + .manage( + EcashState::new( + ecash_contract, + nyxd_client, + identity, + staged_key_pair, + comm_channel, + storage.clone(), + ) + .await + .unwrap(), + ) + .mount( + "/v1/ecash", + ecash::routes_open_api(&Default::default(), true).0, + ); TestFixture { rocket: Client::tracked(rocket) @@ -1394,39 +1316,43 @@ impl TestFixture { self.epoch.store(epoch, Ordering::Relaxed) } + #[allow(dead_code)] fn add_tx(&self, hash: Hash, tx: TxResponse) { self.chain_state.lock().unwrap().txs.insert(hash, tx); } - fn add_deposit_tx(&self, voucher: &BandwidthVoucherIssuanceData) { - let mut guard = self.chain_state.lock().unwrap(); - let fixture = deposit_tx_fixture(voucher); - - guard.txs.insert(voucher.tx_hash(), fixture); + fn add_deposit(&self, voucher_data: &IssuanceTicketBook) { + let mut chain = self.chain_state.lock().unwrap(); + let deposit = Deposit { + bs58_encoded_ed25519_pubkey: voucher_data + .identity_key() + .public_key() + .to_base58_string(), + }; + let existing = chain + .ecash_contract + .deposits + .insert(voucher_data.deposit_id(), deposit); + assert!(existing.is_none()); } async fn issue_dummy_credential(&self) { let mut rng = OsRng; - let mut tx_hash = [0u8; 32]; - rng.fill_bytes(&mut tx_hash); - let tx_hash = Hash::from_bytes(Algorithm::Sha256, &tx_hash).unwrap(); + let deposit_id = rng.next_u32(); - let voucher = voucher_fixture(coin(1234, "unym"), Some(tx_hash.to_string())); + let voucher = voucher_fixture(Some(deposit_id)); let signing_data = voucher.prepare_for_signing(); - let voucher_data = voucher.get_variant_data().voucher_data().unwrap(); - let req = voucher_data.create_blind_sign_request_body(&signing_data); + let req = voucher.create_blind_sign_request_body(&signing_data); - self.add_deposit_tx(voucher_data); + self.add_deposit(&voucher); self.issue_credential(req).await; } async fn issue_credential(&self, req: BlindSignRequestBody) -> BlindedSignatureResponse { let response = self .rocket - .post(format!( - "/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/{COCONUT_BLIND_SIGN}", - )) + .post(format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}",)) .json(&req) .dispatch() .await; @@ -1439,7 +1365,7 @@ impl TestFixture { let response = self .rocket .get(format!( - "/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/issued-credential/{id}" + "/{API_VERSION}/{ECASH_ROUTES}/issued-credential/{id}" )) .dispatch() .await; @@ -1447,7 +1373,7 @@ impl TestFixture { serde_json::from_str(&response.into_string().await.unwrap()).unwrap() } - async fn issued_unchecked(&self, id: i64) -> IssuedCredentialBody { + async fn issued_unchecked(&self, id: i64) -> IssuedTicketbookBody { self.issued_credential(id) .await .unwrap() @@ -1459,41 +1385,39 @@ impl TestFixture { #[cfg(test)] mod credential_tests { use super::*; - use crate::coconut::tests::helpers::init_chain; - use nym_api_requests::coconut::{VerifyCredentialBody, VerifyCredentialResponse}; - use nym_coconut::{blind_sign, hash_to_scalar, ttp_keygen}; - use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredential; - use nym_credentials::coconut::bandwidth::bandwidth_credential_params; - use nym_validator_client::nym_api::routes::COCONUT_VERIFY_BANDWIDTH_CREDENTIAL; + use nym_compact_ecash::ttp_keygen; #[tokio::test] async fn already_issued() { - let voucher = voucher_fixture(coin(1234, TEST_COIN_DENOM), None); + let voucher = voucher_fixture(None); let signing_data = voucher.prepare_for_signing(); - let voucher_data = voucher.get_variant_data().voucher_data().unwrap(); - let request_body = voucher_data.create_blind_sign_request_body(&signing_data); + let request_body = voucher.create_blind_sign_request_body(&signing_data); - let tx_hash = request_body.tx_hash; - let tx_entry = tx_entry_fixture(tx_hash); + let deposit_id = request_body.deposit_id; let test_fixture = TestFixture::new().await; - test_fixture.add_tx(tx_hash, tx_entry); + test_fixture.add_deposit(&voucher); let sig = blinded_signature_fixture(); let commitments = request_body.encode_commitments(); - let public = request_body.public_attributes_plain.clone(); + let expiration_date = request_body.expiration_date; test_fixture .storage - .store_issued_credential(42, tx_hash, &sig, dummy_signature(), commitments, public) + .store_issued_credential( + 42, + deposit_id, + &sig, + dummy_signature(), + commitments, + expiration_date, + voucher.ticketbook_type(), + ) .await .unwrap(); let response = test_fixture .rocket - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_BLIND_SIGN - )) + .post(format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}",)) .json(&request_body) .dispatch() .await; @@ -1524,15 +1448,15 @@ mod credential_tests { AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(), Default::default(), ); - let params = Parameters::new(4).unwrap(); - let key_pair = ttp_keygen(¶ms, 1, 1).unwrap().remove(0); + let key_pair = ttp_keygen(1, 1).unwrap().remove(0); let tmp_dir = tempdir().unwrap(); let storage = NymApiStorage::init(tmp_dir.path().join("storage.db")) .await .unwrap(); - let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key().clone()); - let staged_key_pair = crate::coconut::KeyPair::new(); + let comm_channel = + DummyCommunicationChannel::new_single_dummy(key_pair.verification_key().clone()); + let staged_key_pair = crate::ecash::keys::KeyPair::new(); staged_key_pair .set(KeyPairWithEpoch { keys: key_pair, @@ -1541,43 +1465,45 @@ mod credential_tests { .await; staged_key_pair.validate(); - let state = State::new( + let state = EcashState::new( + "n16a32stm6kknhq5cc8rx77elr66pygf2hfszw7wvpq746x3uffylqkjar4l" + .parse() + .unwrap(), nyxd_client, - TEST_COIN_DENOM.to_string(), identity, staged_key_pair, comm_channel, storage.clone(), - ); + ) + .await + .unwrap(); - let tx_hash = "6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E" - .parse() - .unwrap(); - assert!(state.already_issued(tx_hash).await.unwrap().is_none()); + let deposit_id = 42; + assert!(state.already_issued(deposit_id).await.unwrap().is_none()); - let voucher = voucher_fixture(coin(1234, TEST_COIN_DENOM), None); + let voucher = voucher_fixture(None); let signing_data = voucher.prepare_for_signing(); - let voucher_data = voucher.get_variant_data().voucher_data().unwrap(); - let request_body = voucher_data.create_blind_sign_request_body(&signing_data); + let request_body = voucher.create_blind_sign_request_body(&signing_data); let commitments = request_body.encode_commitments(); - let public = request_body.public_attributes_plain.clone(); + let expiration_date = request_body.expiration_date; let sig = blinded_signature_fixture(); storage .store_issued_credential( 42, - tx_hash, + deposit_id, &sig, dummy_signature(), commitments.clone(), - public.clone(), + expiration_date, + voucher.ticketbook_type(), ) .await .unwrap(); assert_eq!( state - .already_issued(tx_hash) + .already_issued(deposit_id) .await .unwrap() .unwrap() @@ -1599,28 +1525,28 @@ mod credential_tests { let storage_err = storage .store_issued_credential( 42, - tx_hash, + deposit_id, &blinded_signature, dummy_signature(), commitments.clone(), - public.clone(), + expiration_date, + voucher.ticketbook_type(), ) .await; assert!(storage_err.is_err()); - // And use a new hash to store a new signature - let tx_hash = "97D64C38D6601B1F0FD3A82E20D252685CB7A210AFB0261018590659AB82B0BF" - .parse() - .unwrap(); + // And use a new deposit to store a new signature + let deposit_id = 69; storage .store_issued_credential( 42, - tx_hash, + deposit_id, &blinded_signature, dummy_signature(), commitments.clone(), - public.clone(), + expiration_date, + voucher.ticketbook_type(), ) .await .unwrap(); @@ -1628,7 +1554,7 @@ mod credential_tests { // Check that the same value for tx_hash is returned assert_eq!( state - .already_issued(tx_hash) + .already_issued(deposit_id) .await .unwrap() .unwrap() @@ -1639,76 +1565,39 @@ mod credential_tests { #[tokio::test] async fn blind_sign_correct() { - let tx_hash = - Hash::from_str("7C41AF8266D91DE55E1C8F4712E6A952A165ED3D8C27C7B00428CBD0DE00A52B") - .unwrap(); + let deposit_id = 42; - let params = Parameters::new(4).unwrap(); let mut rng = OsRng; - let nym_api_identity = identity::KeyPair::new(&mut rng); - let identity_keypair = identity::KeyPair::new(&mut rng); - let encryption_keypair = encryption::KeyPair::new(&mut rng); - let voucher = IssuanceBandwidthCredential::new_voucher( - coin(1234, "unym"), - tx_hash, + let identifier = [42u8; 32]; + let voucher = IssuanceTicketBook::new( + deposit_id, + identifier, identity::PrivateKey::from_base58_string( identity_keypair.private_key().to_base58_string(), ) .unwrap(), - encryption::PrivateKey::from_bytes(&encryption_keypair.private_key().to_bytes()) - .unwrap(), + TicketType::V1MixnetEntry, ); - let key_pair = ttp_keygen(¶ms, 1, 1).unwrap().remove(0); - let tmp_dir = tempdir().unwrap(); - let storage = NymApiStorage::init(tmp_dir.path().join("storage.db")) - .await - .unwrap(); + let deposit = Deposit { + bs58_encoded_ed25519_pubkey: voucher.identity_key().public_key().to_base58_string(), + }; - let chain = init_chain(); - - let voucher_data = voucher.get_variant_data().voucher_data().unwrap(); - let tx_entry = deposit_tx_fixture(voucher_data); - - chain.lock().unwrap().txs.insert(tx_hash, tx_entry.clone()); - - let nyxd_client = DummyClient::new( - AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(), - chain.clone(), - ); - - let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key().clone()); - let staged_key_pair = crate::coconut::KeyPair::new(); - staged_key_pair - .set(KeyPairWithEpoch { - keys: key_pair, - issued_for_epoch: 1, - }) - .await; - staged_key_pair.validate(); - - let rocket = rocket::build().attach(crate::coconut::stage( - nyxd_client, - TEST_COIN_DENOM.to_string(), - nym_api_identity, - staged_key_pair, - comm_channel, - storage.clone(), - )); - let client = Client::tracked(rocket) - .await - .expect("valid rocket instance"); + let test = TestFixture::new().await; + test.chain_state + .lock() + .unwrap() + .ecash_contract + .deposits + .insert(voucher.deposit_id(), deposit); let signing_data = voucher.prepare_for_signing(); - let voucher_data = voucher.get_variant_data().voucher_data().unwrap(); - let request_body = voucher_data.create_blind_sign_request_body(&signing_data); + let request_body = voucher.create_blind_sign_request_body(&signing_data); - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_BLIND_SIGN - )) + let response = test + .rocket + .post(format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}")) .json(&request_body) .dispatch() .await; @@ -1722,410 +1611,16 @@ mod credential_tests { assert!(blinded_signature_response.is_ok()); } - #[tokio::test] - async fn verification_of_bandwidth_credential() { - // Setup variables - let chain = init_chain(); - let validator_address = AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(); - chain - .lock() - .unwrap() - .add_member(validator_address.as_ref(), 100); + #[test] + fn blind_sign_request_body_serde() { + let deposit_id = 123; + let issuance = voucher_fixture(Some(deposit_id)); + let signing_data = issuance.prepare_for_signing(); + let request = issuance.create_blind_sign_request_body(&signing_data); - let nyxd_client = DummyClient::new(validator_address.clone(), chain.clone()); - let db_dir = tempdir().unwrap(); + let json_bytes = serde_json::to_vec(&request).unwrap(); + let recovered: BlindSignRequestBody = serde_json::from_slice(&json_bytes).unwrap(); - // generate all the credential requests - let params = bandwidth_credential_params(); - let key_pair = nym_coconut::keygen(params); - let epoch = 1; - - let voucher_amount = coin(1234, "unym"); - let issuance = voucher_fixture(coin(1234, "unym"), None); - let sig_req = issuance.prepare_for_signing(); - let pub_attrs_hashed = sig_req - .public_attributes_plain - .iter() - .map(hash_to_scalar) - .collect::>(); - let pub_attrs = pub_attrs_hashed.iter().collect::>(); - let blind_sig = blind_sign( - params, - key_pair.secret_key(), - &sig_req.blind_sign_request, - &pub_attrs, - ) - .unwrap(); - let sig = blind_sig.unblind( - key_pair.verification_key(), - &sig_req.pedersen_commitments_openings, - ); - - let issued = issuance.into_issued_credential(sig, epoch); - let spending = issued - .prepare_for_spending(key_pair.verification_key()) - .unwrap(); - - let storage1 = NymApiStorage::init(db_dir.path().join("storage.db")) - .await - .unwrap(); - let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key().clone()); - let staged_key_pair = crate::coconut::KeyPair::new(); - staged_key_pair - .set(KeyPairWithEpoch { - keys: key_pair, - issued_for_epoch: epoch, - }) - .await; - staged_key_pair.validate(); - let mut rng = OsRng; - let identity = identity::KeyPair::new(&mut rng); - - let rocket = rocket::build().attach(crate::coconut::stage( - nyxd_client.clone(), - TEST_COIN_DENOM.to_string(), - identity, - staged_key_pair, - comm_channel.clone(), - storage1.clone(), - )); - - let client = Client::tracked(rocket) - .await - .expect("valid rocket instance"); - - let proposal_id = 42; - // The address is not used, so we can use a duplicate - let gateway_cosmos_addr = validator_address.clone(); - let req = - VerifyCredentialBody::new(spending.clone(), proposal_id, gateway_cosmos_addr.clone()); - - // Test endpoint with not proposal for the proposal id - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - )) - .json(&req) - .dispatch() - .await; - assert_eq!(response.status(), Status::BadRequest); - assert_eq!( - response.into_string().await.unwrap(), - CoconutError::IncorrectProposal { - reason: "proposal not found".to_string() - } - .to_string() - ); - - let mut proposal = Proposal { - title: String::new(), - description: String::from("25mnnoCcUfeizfC85avvroFg2prpEZBgJbJM2SLtkgyyUkoAU3cqJiqWmg8cMHEPjfFf5sQF92SMAM2vbEoLZvUjenvXhadTLdA4TqMYArJpihyqirW2AhGoNehtcdcK5gnH"), - msgs: vec![], - status: cw3::Status::Open, - expires: cw_utils::Expiration::Never {}, - threshold: cw_utils::Threshold::AbsolutePercentage { - percentage: Decimal::from_ratio(2u32, 3u32), - }, - total_weight: chain.lock().unwrap().total_group_weight(), - votes: Votes::yes(0), - proposer: Addr::unchecked("proposer"), - deposit: None, - start_height: 0 - }; - - // Test the endpoint with a different blinded serial number in the description - - chain - .lock() - .unwrap() - .multisig_contract - .proposals - .insert(proposal_id, proposal.clone()); - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - )) - .json(&req) - .dispatch() - .await; - assert_eq!(response.status(), Status::BadRequest); - assert_eq!( - response.into_string().await.unwrap(), - CoconutError::IncorrectProposal { - reason: "incorrect blinded serial number in description".to_string() - } - .to_string() - ); - - // Test the endpoint with no msg in the proposal action - proposal.description = spending - .verify_credential_request - .blinded_serial_number_bs58(); - chain - .lock() - .unwrap() - .multisig_contract - .proposals - .insert(proposal_id, proposal.clone()); - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - )) - .json(&req) - .dispatch() - .await; - assert_eq!(response.status(), Status::BadRequest); - assert_eq!( - response.into_string().await.unwrap(), - CoconutError::IncorrectProposal { - reason: "action is not to release funds".to_string() - } - .to_string() - ); - - // Test the endpoint without any credential recorded in the Coconut Bandwidth Contract - let funds = voucher_amount.clone(); - let msg = nym_coconut_bandwidth_contract_common::msg::ExecuteMsg::ReleaseFunds { - funds: funds.clone(), - }; - let cosmos_msg = CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::new(), - msg: to_binary(&msg).unwrap(), - funds: vec![], - }); - proposal.msgs = vec![cosmos_msg]; - chain - .lock() - .unwrap() - .multisig_contract - .proposals - .insert(proposal_id, proposal.clone()); - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - )) - .json(&req) - .dispatch() - .await; - assert_eq!(response.status(), Status::BadRequest); - assert_eq!( - response.into_string().await.unwrap(), - CoconutError::InvalidCredentialStatus { - status: "spent credential not found".to_string() - } - .to_string() - ); - - chain - .lock() - .unwrap() - .bandwidth_contract - .spent_credentials - .insert( - spending - .verify_credential_request - .blinded_serial_number_bs58(), - SpendCredentialResponse::new(None), - ); - - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - )) - .json(&req) - .dispatch() - .await; - assert_eq!(response.status(), Status::BadRequest); - assert_eq!( - response.into_string().await.unwrap(), - CoconutError::InvalidCredentialStatus { - status: "Inexistent".to_string() - } - .to_string() - ); - - // Test the endpoint with a credential that doesn't verify correctly - let mut spent_credential = SpendCredential::new( - funds.clone(), - spending - .verify_credential_request - .blinded_serial_number_bs58(), - Addr::unchecked("unimportant"), - ); - chain - .lock() - .unwrap() - .bandwidth_contract - .spent_credentials - .insert( - spending - .verify_credential_request - .blinded_serial_number_bs58(), - SpendCredentialResponse::new(Some(spent_credential.clone())), - ); - - // TODO: somehow restore that test - // let bad_credential = Credential::new( - // 4, - // theta.clone(), - // voucher_value, - // String::from("bad voucher info"), - // 0, - // ); - // let bad_req = VerifyCredentialBody::new( - // bad_credential, - // epoch_id, - // proposal_id, - // gateway_cosmos_addr.clone(), - // ); - // let response = client - // .post(format!( - // "/{}/{}/{}/{}", - // API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - // )) - // .json(&bad_req) - // .dispatch() - // .await; - // assert_eq!(response.status(), Status::Ok); - // let verify_credential_response = serde_json::from_str::( - // &response.into_string().await.unwrap(), - // ) - // .unwrap(); - // assert!(!verify_credential_response.verification_result); - // assert_eq!( - // cw3::Status::Rejected, - // chain - // .lock() - // .unwrap() - // .multisig_contract - // .proposals - // .get(&proposal_id) - // .unwrap() - // .status - // ); - - // Test the endpoint with a proposal that has a different value for the funds to be released - // then what's in the credential - let funds = Coin::new(voucher_amount.amount.u128() + 10, TEST_COIN_DENOM); - let msg = nym_coconut_bandwidth_contract_common::msg::ExecuteMsg::ReleaseFunds { - funds: funds.clone().into(), - }; - let cosmos_msg = CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::new(), - msg: to_binary(&msg).unwrap(), - funds: vec![], - }); - proposal.msgs = vec![cosmos_msg]; - chain.lock().unwrap().reset_votes(); - chain - .lock() - .unwrap() - .multisig_contract - .proposals - .insert(proposal_id, proposal.clone()); - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - )) - .json(&req) - .dispatch() - .await; - assert_eq!(response.status(), Status::Ok); - let verify_credential_response = serde_json::from_str::( - &response.into_string().await.unwrap(), - ) - .unwrap(); - assert!(!verify_credential_response.verification_result); - assert_eq!( - cw3::Status::Rejected, - chain - .lock() - .unwrap() - .multisig_contract - .proposals - .get(&proposal_id) - .unwrap() - .status - ); - - // Test the endpoint with every dependency met - let funds = voucher_amount; - let msg = nym_coconut_bandwidth_contract_common::msg::ExecuteMsg::ReleaseFunds { - funds: funds.clone(), - }; - let cosmos_msg = CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::new(), - msg: to_binary(&msg).unwrap(), - funds: vec![], - }); - proposal.msgs = vec![cosmos_msg]; - chain.lock().unwrap().reset_votes(); - chain - .lock() - .unwrap() - .multisig_contract - .proposals - .insert(proposal_id, proposal.clone()); - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - )) - .json(&req) - .dispatch() - .await; - assert_eq!(response.status(), Status::Ok); - let verify_credential_response = serde_json::from_str::( - &response.into_string().await.unwrap(), - ) - .unwrap(); - assert!(verify_credential_response.verification_result); - assert_eq!( - cw3::Status::Passed, - chain - .lock() - .unwrap() - .multisig_contract - .proposals - .get(&proposal_id) - .unwrap() - .status - ); - - // Test the endpoint with the credential marked as Spent in the Coconut Bandwidth Contract - spent_credential.mark_as_spent(); - chain - .lock() - .unwrap() - .bandwidth_contract - .spent_credentials - .insert( - spending - .verify_credential_request - .blinded_serial_number_bs58(), - SpendCredentialResponse::new(Some(spent_credential)), - ); - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - )) - .json(&req) - .dispatch() - .await; - assert_eq!(response.status(), Status::BadRequest); - assert_eq!( - response.into_string().await.unwrap(), - CoconutError::InvalidCredentialStatus { - status: "Spent".to_string() - } - .to_string() - ); + assert_eq!(recovered, request) } } diff --git a/nym-api/src/main.rs b/nym-api/src/main.rs index 5731c1ade3..0bebd493ed 100644 --- a/nym-api/src/main.rs +++ b/nym-api/src/main.rs @@ -4,8 +4,8 @@ #[macro_use] extern crate rocket; -use crate::coconut::dkg::controller::keys::{ - can_validate_coconut_keys, load_bte_keypair, load_coconut_keypair_if_exists, +use crate::ecash::dkg::controller::keys::{ + can_validate_coconut_keys, load_bte_keypair, load_ecash_keypair_if_exists, }; use crate::epoch_operations::RewardedSetUpdater; use crate::network::models::NetworkDetails; @@ -19,7 +19,7 @@ use crate::support::storage::NymApiStorage; use ::nym_config::defaults::setup_env; use circulating_supply_api::cache::CirculatingSupplyCache; use clap::Parser; -use coconut::dkg::controller::DkgController; +use ecash::dkg::controller::DkgController; use node_status_api::NodeStatusCache; use nym_bin_common::logging::setup_logging; use nym_config::defaults::NymNetworkDetails; @@ -30,7 +30,7 @@ use rand::rngs::OsRng; use support::{http, nyxd}; mod circulating_supply_api; -mod coconut; +mod ecash; mod epoch_operations; pub(crate) mod network; mod network_monitor; @@ -70,10 +70,10 @@ async fn start_nym_api_tasks(config: Config) -> anyhow::Result let nym_network_details = NymNetworkDetails::new_from_env(); let network_details = NetworkDetails::new(connected_nyxd.to_string(), nym_network_details); - let coconut_keypair_wrapper = coconut::keys::KeyPair::new(); + let coconut_keypair_wrapper = ecash::keys::KeyPair::new(); // if the keypair doesnt exist (because say this API is running in the caching mode), nothing will happen - if let Some(loaded_keys) = load_coconut_keypair_if_exists(&config.coconut_signer)? { + if let Some(loaded_keys) = load_ecash_keypair_if_exists(&config.coconut_signer)? { let issued_for = loaded_keys.issued_for_epoch; coconut_keypair_wrapper.set(loaded_keys).await; diff --git a/nym-api/src/network_monitor/monitor/sender.rs b/nym-api/src/network_monitor/monitor/sender.rs index d2d99e7462..531bc7be8f 100644 --- a/nym-api/src/network_monitor/monitor/sender.rs +++ b/nym-api/src/network_monitor/monitor/sender.rs @@ -13,9 +13,9 @@ use futures::task::Context; use futures::{Future, Stream}; use log::{debug, info, trace, warn}; use nym_bandwidth_controller::BandwidthController; -use nym_config::defaults::REMAINING_BANDWIDTH_THRESHOLD; use nym_credential_storage::persistent_storage::PersistentStorage; use nym_crypto::asymmetric::identity::{self, PUBLIC_KEY_LENGTH}; +use nym_gateway_client::client::config::GatewayClientConfig; use nym_gateway_client::client::GatewayConfig; use nym_gateway_client::error::GatewayClientError; use nym_gateway_client::{ @@ -205,15 +205,16 @@ impl PacketSender { ); let gateway_client = GatewayClient::new( + GatewayClientConfig::new_default() + .with_disabled_credentials_mode(fresh_gateway_client_data.disabled_credentials_mode) + .with_response_timeout(fresh_gateway_client_data.gateway_response_timeout), config, Arc::clone(&fresh_gateway_client_data.local_identity), None, gateway_packet_router, Some(fresh_gateway_client_data.bandwidth_controller.clone()), task_client, - ) - .with_disabled_credentials_mode(fresh_gateway_client_data.disabled_credentials_mode) - .with_response_timeout(fresh_gateway_client_data.gateway_response_timeout); + ); ( GatewayClientHandle::new(gateway_client), @@ -325,9 +326,9 @@ impl PacketSender { async fn check_remaining_bandwidth( client: &mut GatewayClient, ) -> Result<(), GatewayClientError> { - if client.remaining_bandwidth() < REMAINING_BANDWIDTH_THRESHOLD { + if client.remaining_bandwidth() < client.cfg.bandwidth.remaining_bandwidth_threshold { Err(GatewayClientError::NotEnoughBandwidth( - REMAINING_BANDWIDTH_THRESHOLD, + client.cfg.bandwidth.remaining_bandwidth_threshold, client.remaining_bandwidth(), )) } else { diff --git a/nym-api/src/node_status_api/local_guard.rs b/nym-api/src/node_status_api/local_guard.rs deleted file mode 100644 index 9be3a252c2..0000000000 --- a/nym-api/src/node_status_api/local_guard.rs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use rocket::http::Status; -use rocket::request::{FromRequest, Outcome}; -use rocket::Request; -use std::fmt::Debug; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; - -#[derive(Debug)] -pub struct NonLocalRequestError; - -/// Request guard that only allows requests coming from a local address -pub(crate) struct LocalRequest; - -fn is_local_address(ip: Option) -> bool { - if let Some(address) = ip { - match address { - IpAddr::V4(ip) => ip == Ipv4Addr::LOCALHOST, - IpAddr::V6(ip) => ip == Ipv6Addr::LOCALHOST, - } - } else { - false - } -} - -#[rocket::async_trait] -impl<'r> FromRequest<'r> for LocalRequest { - type Error = NonLocalRequestError; - - async fn from_request(request: &'r Request<'_>) -> Outcome { - if is_local_address(request.client_ip()) { - Outcome::Success(LocalRequest) - } else { - warn!( - "Received a request from {:?} for a local-only route", - request.client_ip() - ); - Outcome::Error((Status::Unauthorized, NonLocalRequestError)) - } - } -} diff --git a/nym-api/src/node_status_api/mod.rs b/nym-api/src/node_status_api/mod.rs index 91967ebcb8..684dac11ae 100644 --- a/nym-api/src/node_status_api/mod.rs +++ b/nym-api/src/node_status_api/mod.rs @@ -16,7 +16,6 @@ use std::time::Duration; pub(crate) mod cache; pub(crate) mod helpers; -pub(crate) mod local_guard; pub(crate) mod models; pub(crate) mod reward_estimate; pub(crate) mod routes; diff --git a/nym-api/src/node_status_api/models.rs b/nym-api/src/node_status_api/models.rs index 7d13d81522..d0c172d7a3 100644 --- a/nym-api/src/node_status_api/models.rs +++ b/nym-api/src/node_status_api/models.rs @@ -418,3 +418,11 @@ pub enum NymApiStorageError { #[error("failed to perform startup SQL migration - {0}")] StartupMigrationFailure(#[from] sqlx::migrate::MigrateError), } + +impl NymApiStorageError { + pub fn database_inconsistency>(reason: S) -> NymApiStorageError { + NymApiStorageError::DatabaseInconsistency { + reason: reason.into(), + } + } +} diff --git a/nym-api/src/nym_contract_cache/cache/refresher.rs b/nym-api/src/nym_contract_cache/cache/refresher.rs index c1ea22e825..d200a3445d 100644 --- a/nym-api/src/nym_contract_cache/cache/refresher.rs +++ b/nym-api/src/nym_contract_cache/cache/refresher.rs @@ -52,18 +52,18 @@ impl NymContractCacheRefresher { let mixnet = query_guard!(client_guard, mixnet_contract_address()); let vesting = query_guard!(client_guard, vesting_contract_address()); - let coconut_bandwidth = query_guard!(client_guard, coconut_bandwidth_contract_address()); let coconut_dkg = query_guard!(client_guard, dkg_contract_address()); let group = query_guard!(client_guard, group_contract_address()); let multisig = query_guard!(client_guard, multisig_contract_address()); + let ecash = query_guard!(client_guard, ecash_contract_address()); for (address, name) in [ (mixnet, "nym-mixnet-contract"), (vesting, "nym-vesting-contract"), - (coconut_bandwidth, "nym-coconut-bandwidth-contract"), (coconut_dkg, "nym-coconut-dkg-contract"), (group, "nym-cw4-group-contract"), (multisig, "nym-cw3-multisig-contract"), + (ecash, "nym-ecash-contract"), ] { let (cw2, build_info) = if let Some(address) = address { let cw2 = query_guard!(client_guard, try_get_cw2_contract_version(address).await); diff --git a/nym-api/src/status/mod.rs b/nym-api/src/status/mod.rs index 1a88a9ab33..053cb8ba88 100644 --- a/nym-api/src/status/mod.rs +++ b/nym-api/src/status/mod.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut; +use crate::ecash; use nym_bin_common::bin_info; use nym_bin_common::build_information::BinaryBuildInformation; use okapi::openapi3::OpenApi; @@ -26,7 +26,7 @@ pub(crate) struct SignerState { pub announce_address: String, - pub(crate) coconut_keypair: coconut::keys::KeyPair, + pub(crate) coconut_keypair: ecash::keys::KeyPair, } impl ApiStatusState { diff --git a/nym-api/src/status/routes.rs b/nym-api/src/status/routes.rs index 614785eeba..560ac765d9 100644 --- a/nym-api/src/status/routes.rs +++ b/nym-api/src/status/routes.rs @@ -5,7 +5,7 @@ use crate::node_status_api::models::ErrorResponse; use crate::status::ApiStatusState; use nym_api_requests::models::{ApiHealthResponse, SignerInformationResponse}; use nym_bin_common::build_information::BinaryBuildInformationOwned; -use nym_coconut::Base58; +use nym_compact_ecash::Base58; use rocket::http::Status; use rocket::serde::json::Json; use rocket::State; diff --git a/nym-api/src/support/config/helpers.rs b/nym-api/src/support/config/helpers.rs index 9f965e7815..6521c8f7f4 100644 --- a/nym-api/src/support/config/helpers.rs +++ b/nym-api/src/support/config/helpers.rs @@ -1,7 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::controller::keys::init_bte_keypair; +use crate::ecash::dkg::controller::keys::init_bte_keypair; use crate::support::config; use crate::support::config::{ default_config_directory, default_data_directory, upgrade_helpers, Config, diff --git a/nym-api/src/support/http/mod.rs b/nym-api/src/support/http/mod.rs index 99cd925e80..a0696d4955 100644 --- a/nym-api/src/support/http/mod.rs +++ b/nym-api/src/support/http/mod.rs @@ -2,8 +2,9 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::circulating_supply_api::cache::CirculatingSupplyCache; -use crate::coconut::client::Client; -use crate::coconut::{self, comm::QueryCommunicationChannel}; +use crate::ecash::client::Client; +use crate::ecash::state::EcashState; +use crate::ecash::{self, comm::QueryCommunicationChannel}; use crate::network::models::NetworkDetails; use crate::network::network_routes; use crate::node_describe_cache::DescribedNodes; @@ -16,7 +17,7 @@ use crate::support::caching::cache::SharedCache; use crate::support::config::Config; use crate::support::{nyxd, storage}; use crate::{circulating_supply_api, nym_contract_cache, nym_nodes::nym_node_routes}; -use anyhow::{bail, Result}; +use anyhow::{bail, Context, Result}; use nym_crypto::asymmetric::identity; use nym_validator_client::nyxd::Coin; use rocket::http::Method; @@ -33,7 +34,7 @@ pub(crate) async fn setup_rocket( network_details: NetworkDetails, nyxd_client: nyxd::Client, identity_keypair: identity::KeyPair, - coconut_keypair: coconut::keys::KeyPair, + coconut_keypair: ecash::keys::KeyPair, ) -> anyhow::Result> { let openapi_settings = rocket_okapi::settings::OpenApiSettings::default(); let mut rocket = rocket::build(); @@ -50,11 +51,11 @@ pub(crate) async fn setup_rocket( "/status" => node_status_api::node_status_routes(&openapi_settings, config.network_monitor.enabled), "/network" => network_routes(&openapi_settings), "/api-status" => api_status_routes(&openapi_settings), + "/ecash" => ecash::routes_open_api(&openapi_settings, config.coconut_signer.enabled), "" => nym_node_routes(&openapi_settings), // => when we move those routes, we'll need to add a redirection for backwards compatibility "/unstable/nym-nodes" => nym_node_routes_next(&openapi_settings) - } let rocket = rocket @@ -83,9 +84,9 @@ pub(crate) async fn setup_rocket( let rocket = if config.coconut_signer.enabled { // make sure we have some tokens to cover multisig fees let balance = nyxd_client.balance(&mix_denom).await?; - if balance.amount < coconut::MINIMUM_BALANCE { + if balance.amount < ecash::MINIMUM_BALANCE { let address = nyxd_client.address().await; - let min = Coin::new(coconut::MINIMUM_BALANCE, mix_denom); + let min = Coin::new(ecash::MINIMUM_BALANCE, mix_denom); bail!("the account ({address}) doesn't have enough funds to cover verification fees. it has {balance} while it needs at least {min}") } @@ -103,15 +104,24 @@ pub(crate) async fn setup_rocket( coconut_keypair: coconut_keypair.clone(), }); + let ecash_contract = nyxd_client + .get_ecash_contract_address() + .await + .context("e-cash contract address is required to setup the zk-nym signer")?; + let comm_channel = QueryCommunicationChannel::new(nyxd_client.clone()); - rocket.attach(coconut::stage( + + let ecash_state = EcashState::new( + ecash_contract, nyxd_client.clone(), - mix_denom, identity_keypair, coconut_keypair, comm_channel, storage.clone().unwrap(), - )) + ) + .await?; + + rocket.manage(ecash_state) } else { rocket }; diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index 833d891473..69f9a49360 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -1,14 +1,13 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::error::CoconutError; +use crate::ecash::error::EcashError; use crate::epoch_operations::MixnodeWithPerformance; use crate::support::config::Config; use anyhow::Result; use async_trait::async_trait; use cw3::{ProposalResponse, VoteResponse}; use cw4::MemberResponse; -use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; use nym_coconut_dkg_common::dealer::RegisteredDealerDetails; use nym_coconut_dkg_common::dealing::{ DealerDealingsStatusResponse, DealingChunkInfo, DealingMetadata, DealingStatusResponse, @@ -22,6 +21,9 @@ use nym_coconut_dkg_common::{ verification_key::{ContractVKShare, VerificationKeyShare}, }; use nym_config::defaults::{ChainDetails, NymNetworkDetails}; +use nym_dkg::Threshold; +use nym_ecash_contract_common::blacklist::BlacklistedAccountResponse; +use nym_ecash_contract_common::deposit::{DepositId, DepositResponse}; use nym_mixnet_contract_common::families::FamilyHead; use nym_mixnet_contract_common::mixnode::MixNodeDetails; use nym_mixnet_contract_common::reward_params::RewardingParams; @@ -29,14 +31,14 @@ use nym_mixnet_contract_common::{ CurrentIntervalResponse, EpochStatus, ExecuteMsg, GatewayBond, IdentityKey, LayerAssignment, MixId, RewardedSetNodeStatus, }; +use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nyxd::contract_traits::PagedDkgQueryClient; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::{ contract_traits::{ - CoconutBandwidthQueryClient, DkgQueryClient, DkgSigningClient, GroupQueryClient, - MixnetQueryClient, MixnetSigningClient, MultisigQueryClient, MultisigSigningClient, - NymContractsProvider, PagedMixnetQueryClient, PagedMultisigQueryClient, - PagedVestingQueryClient, + DkgQueryClient, DkgSigningClient, EcashQueryClient, GroupQueryClient, MixnetQueryClient, + MixnetSigningClient, MultisigQueryClient, MultisigSigningClient, NymContractsProvider, + PagedMixnetQueryClient, PagedMultisigQueryClient, PagedVestingQueryClient, }, cosmwasm_client::types::ExecuteResult, CosmWasmClient, Fee, @@ -45,7 +47,9 @@ use nym_validator_client::nyxd::{ hash::{Hash, SHA256_HASH_SIZE}, AccountId, Coin, TendermintTime, }; -use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient, QueryHttpRpcNyxdClient}; +use nym_validator_client::{ + nyxd, DirectSigningHttpRpcNyxdClient, EcashApiClient, QueryHttpRpcNyxdClient, +}; use nym_vesting_contract_common::AccountVestingCoins; use serde::Deserialize; use std::sync::Arc; @@ -153,6 +157,15 @@ impl Client { nyxd_query!(self, current_chain_details().clone()) } + pub(crate) async fn get_ecash_contract_address(&self) -> Result { + nyxd_query!( + self, + ecash_contract_address() + .cloned() + .ok_or_else(|| NyxdError::unavailable_contract_address("ecash contract").into()) + ) + } + pub(crate) async fn get_rewarding_validator_address(&self) -> Result { let cosmwasm_addr = nyxd_query!( self, @@ -338,12 +351,12 @@ impl Client { } #[async_trait] -impl crate::coconut::client::Client for Client { +impl crate::ecash::client::Client for Client { async fn address(&self) -> AccountId { self.client_address().await } - async fn dkg_contract_address(&self) -> Result { + async fn dkg_contract_address(&self) -> Result { nyxd_query!( self, dkg_contract_address() @@ -352,37 +365,21 @@ impl crate::coconut::client::Client for Client { ) } - async fn bandwidth_contract_admin(&self) -> crate::coconut::error::Result> { - let guard = self.inner.read().await; - - let bandwidth_contract = query_guard!( - guard, - coconut_bandwidth_contract_address() - .ok_or(CoconutError::MissingBandwidthContractAddress) - )?; - - let contract = query_guard!(guard, get_contract(bandwidth_contract)).await?; - - Ok(contract.contract_info.admin) - } - - async fn get_tx(&self, tx_hash: Hash) -> crate::coconut::error::Result { - nyxd_query!(self, get_tx(tx_hash).await).map_err(|source| { - CoconutError::TxRetrievalFailure { - tx_hash: tx_hash.to_string(), - source, - } - }) + async fn get_deposit( + &self, + deposit_id: DepositId, + ) -> crate::ecash::error::Result { + Ok(nyxd_query!(self, get_deposit(deposit_id).await?)) } async fn get_proposal( &self, proposal_id: u64, - ) -> crate::coconut::error::Result { + ) -> crate::ecash::error::Result { Ok(nyxd_query!(self, query_proposal(proposal_id).await?)) } - async fn list_proposals(&self) -> crate::coconut::error::Result> { + async fn list_proposals(&self) -> crate::ecash::error::Result> { Ok(nyxd_query!(self, get_all_proposals().await?)) } @@ -390,41 +387,58 @@ impl crate::coconut::client::Client for Client { &self, proposal_id: u64, voter: String, - ) -> crate::coconut::error::Result { + ) -> crate::ecash::error::Result { Ok(nyxd_query!(self, query_vote(proposal_id, voter).await?)) } - async fn get_spent_credential( + // async fn propose_for_blacklist( + // &self, + // public_key: String, + // ) -> crate::ecash::error::Result { + // Ok(nyxd_signing!( + // self, + // propose_for_blacklist(public_key, None).await? + // )) + // } + + async fn get_blacklisted_account( &self, - blinded_serial_number: String, - ) -> crate::coconut::error::Result { + public_key: String, + ) -> crate::ecash::error::Result { Ok(nyxd_query!( self, - get_spent_credential(blinded_serial_number).await? + get_blacklisted_account(public_key).await? )) } - async fn contract_state(&self) -> crate::coconut::error::Result { + async fn contract_state(&self) -> crate::ecash::error::Result { Ok(nyxd_query!(self, get_state().await?)) } - async fn get_current_epoch(&self) -> crate::coconut::error::Result { + async fn get_current_epoch(&self) -> crate::ecash::error::Result { Ok(nyxd_query!(self, get_current_epoch().await?)) } - async fn group_member(&self, addr: String) -> crate::coconut::error::Result { + async fn group_member(&self, addr: String) -> crate::ecash::error::Result { Ok(nyxd_query!(self, member(addr, None).await?)) } async fn get_current_epoch_threshold( &self, - ) -> crate::coconut::error::Result> { + ) -> crate::ecash::error::Result> { Ok(nyxd_query!(self, get_current_epoch_threshold().await?)) } + async fn get_epoch_threshold( + &self, + epoch_id: EpochId, + ) -> crate::ecash::error::Result> { + Ok(nyxd_query!(self, get_epoch_threshold(epoch_id).await?)) + } + async fn get_self_registered_dealer_details( &self, - ) -> crate::coconut::error::Result { + ) -> crate::ecash::error::Result { let self_address = &self.address().await; Ok(nyxd_query!(self, get_dealer_details(self_address).await?)) } @@ -433,7 +447,7 @@ impl crate::coconut::client::Client for Client { &self, epoch_id: EpochId, dealer: String, - ) -> crate::coconut::error::Result { + ) -> crate::ecash::error::Result { let dealer = dealer .as_str() .parse() @@ -448,7 +462,7 @@ impl crate::coconut::client::Client for Client { &self, epoch_id: EpochId, dealer: String, - ) -> crate::coconut::error::Result { + ) -> crate::ecash::error::Result { Ok(nyxd_query!( self, get_dealer_dealings_status(epoch_id, dealer).await? @@ -460,14 +474,14 @@ impl crate::coconut::client::Client for Client { epoch_id: EpochId, dealer: String, dealing_index: DealingIndex, - ) -> crate::coconut::error::Result { + ) -> crate::ecash::error::Result { Ok(nyxd_query!( self, get_dealing_status(epoch_id, dealer, dealing_index).await? )) } - async fn get_current_dealers(&self) -> crate::coconut::error::Result> { + async fn get_current_dealers(&self) -> crate::ecash::error::Result> { Ok(nyxd_query!(self, get_all_current_dealers().await?)) } @@ -476,7 +490,7 @@ impl crate::coconut::client::Client for Client { epoch_id: EpochId, dealer: String, dealing_index: DealingIndex, - ) -> crate::coconut::error::Result> { + ) -> crate::ecash::error::Result> { Ok(nyxd_query!( self, get_dealings_metadata(epoch_id, dealer, dealing_index) @@ -491,7 +505,7 @@ impl crate::coconut::client::Client for Client { dealer: &str, dealing_index: DealingIndex, chunk_index: ChunkIndex, - ) -> crate::coconut::error::Result> { + ) -> crate::ecash::error::Result> { Ok(nyxd_query!( self, get_dealing_chunk(epoch_id, dealer.to_string(), dealing_index, chunk_index) @@ -504,40 +518,52 @@ impl crate::coconut::client::Client for Client { &self, epoch_id: EpochId, dealer: String, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { Ok(nyxd_query!(self, get_vk_share(epoch_id, dealer).await?).share) } async fn get_verification_key_shares( &self, epoch_id: EpochId, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { Ok(nyxd_query!( self, get_all_verification_key_shares(epoch_id).await? )) } + async fn get_registered_ecash_clients( + &self, + epoch_id: EpochId, + ) -> Result, EcashError> { + Ok(self + .get_verification_key_shares(epoch_id) + .await? + .into_iter() + .map(TryInto::try_into) + .collect::, EcashApiError>>()?) + } + async fn vote_proposal( &self, proposal_id: u64, vote_yes: bool, fee: Option, - ) -> Result<(), CoconutError> { + ) -> Result<(), EcashError> { nyxd_signing!(self, vote_proposal(proposal_id, vote_yes, fee).await?); Ok(()) } - async fn execute_proposal(&self, proposal_id: u64) -> crate::coconut::error::Result<()> { + async fn execute_proposal(&self, proposal_id: u64) -> crate::ecash::error::Result<()> { nyxd_signing!(self, execute_proposal(proposal_id, None).await?); Ok(()) } - async fn can_advance_epoch_state(&self) -> crate::coconut::error::Result { + async fn can_advance_epoch_state(&self) -> crate::ecash::error::Result { Ok(nyxd_query!(self, can_advance_state().await?.can_advance())) } - async fn advance_epoch_state(&self) -> crate::coconut::error::Result<()> { + async fn advance_epoch_state(&self) -> crate::ecash::error::Result<()> { nyxd_signing!(self, advance_dkg_epoch_state(None).await?); Ok(()) } @@ -548,7 +574,7 @@ impl crate::coconut::client::Client for Client { identity_key: IdentityKey, announce_address: String, resharing: bool, - ) -> Result { + ) -> Result { Ok(nyxd_signing!( self, register_dealer(bte_key, identity_key, announce_address, resharing, None).await? @@ -560,7 +586,7 @@ impl crate::coconut::client::Client for Client { dealing_index: DealingIndex, chunks: Vec, resharing: bool, - ) -> crate::coconut::error::Result { + ) -> crate::ecash::error::Result { Ok(nyxd_signing!( self, submit_dealing_metadata(dealing_index, chunks, resharing, None).await? @@ -570,7 +596,7 @@ impl crate::coconut::client::Client for Client { async fn submit_dealing_chunk( &self, chunk: PartialContractDealing, - ) -> Result { + ) -> Result { Ok(nyxd_signing!( self, submit_dealing_chunk(chunk, None).await? @@ -581,7 +607,7 @@ impl crate::coconut::client::Client for Client { &self, share: VerificationKeyShare, resharing: bool, - ) -> crate::coconut::error::Result { + ) -> crate::ecash::error::Result { Ok(nyxd_signing!( self, submit_verification_key_share(share, resharing, None).await? diff --git a/nym-api/tests/functional_test/status/status-mixnode.test.ts b/nym-api/tests/functional_test/status/status-mixnode.test.ts index e5046510fb..a5d6f23301 100644 --- a/nym-api/tests/functional_test/status/status-mixnode.test.ts +++ b/nym-api/tests/functional_test/status/status-mixnode.test.ts @@ -1,5 +1,5 @@ import Status from "../../src/endpoints/Status"; -import ConfigHandler from "../../../../common/api-test-utils/config/configHandler" +import ConfigHandler from "../../../../common/api-test-utils/config/configHandler"; let status: Status; let config: ConfigHandler; @@ -153,8 +153,8 @@ describe("Get mixnode data", (): void => { status = new Status(); config = ConfigHandler.getInstance(); }); - - it("with correct data", async (): Promise => { + // TODO - this test needs fixing + it.skip("with correct data", async (): Promise => { const mix_id = config.environmentConfig.mix_id; const response = await status.sendMixnodeRewardEstimatedComputation( mix_id diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index b897f2d008..f33229d4bb 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node" -version = "1.1.6" +version = "1.1.7" authors.workspace = true repository.workspace = true homepage.workspace = true @@ -17,7 +17,7 @@ license = "GPL-3.0" anyhow.workspace = true bip39 = { workspace = true, features = ["zeroize"] } bs58.workspace = true -celes = { workspace = true } # country codes +celes = { workspace = true } # country codes colored = { workspace = true } clap = { workspace = true, features = ["cargo", "env"] } humantime-serde = { workspace = true } @@ -39,7 +39,10 @@ semver = { workspace = true } cupid = { workspace = true } sysinfo = { workspace = true } -nym-bin-common = { path = "../common/bin-common", features = ["basic_tracing", "output_format"] } +nym-bin-common = { path = "../common/bin-common", features = [ + "basic_tracing", + "output_format", +] } nym-client-core-config-types = { path = "../common/client-core/config-types" } nym-config = { path = "../common/config" } nym-crypto = { path = "../common/crypto", features = ["asymmetric", "rand"] } @@ -62,6 +65,3 @@ nym-ip-packet-router = { path = "../service-providers/ip-packet-router" } [build-dependencies] # temporary bonding information v1 (to grab and parse nym-mixnode and nym-gateway package versions) cargo_metadata = { workspace = true } - -[features] -wireguard = ["nym-gateway/wireguard"] diff --git a/nym-node/src/cli/commands/migrate.rs b/nym-node/src/cli/commands/migrate.rs index f44bdc399b..08d9d0b2e0 100644 --- a/nym-node/src/cli/commands/migrate.rs +++ b/nym-node/src/cli/commands/migrate.rs @@ -17,6 +17,7 @@ use nym_gateway::GatewayError; use nym_mixnode::MixnodeError; use nym_network_requester::{CustomGatewayDetails, GatewayDetails}; use nym_node::config; +use nym_node::config::entry_gateway::ZkNymTicketHandlerDebug; use nym_node::config::mixnode::DEFAULT_VERLOC_PORT; use nym_node::config::Config; use nym_node::config::{default_config_filepath, ConfigBuilder, NodeMode}; @@ -411,6 +412,16 @@ async fn migrate_gateway(mut args: Args) -> Result<(), NymNodeError> { announce_wss_port: cfg.gateway.clients_wss_port, debug: config::entry_gateway::Debug { message_retrieval_limit: cfg.debug.message_retrieval_limit, + zk_nym_tickets: ZkNymTicketHandlerDebug { + revocation_bandwidth_penalty: + cfg.debug.zk_nym_tickets.revocation_bandwidth_penalty, + pending_poller: cfg.debug.zk_nym_tickets.pending_poller, + minimum_api_quorum: cfg.debug.zk_nym_tickets.minimum_api_quorum, + minimum_redemption_tickets: + cfg.debug.zk_nym_tickets.minimum_redemption_tickets, + maximum_time_between_redemption: + cfg.debug.zk_nym_tickets.maximum_time_between_redemption, + }, }, }, )) @@ -454,6 +465,7 @@ async fn migrate_gateway(mut args: Args) -> Result<(), NymNodeError> { .unwrap_or_default(), }, }, + debug: Default::default(), }), ) .build(); diff --git a/nym-node/src/cli/commands/sign.rs b/nym-node/src/cli/commands/sign.rs index 67c44977cf..d22b3c9e28 100644 --- a/nym-node/src/cli/commands/sign.rs +++ b/nym-node/src/cli/commands/sign.rs @@ -69,8 +69,6 @@ fn print_signed_contract_msg( } pub async fn execute(args: Args) -> Result<(), NymNodeError> { - println!("args: {args:?}"); - let config = try_load_current_config(args.config.config_path()).await?; let identity_keypair = load_ed25519_identity_keypair(config.storage_paths.keys.ed25519_identity_storage_paths())?; diff --git a/nym-node/src/config/entry_gateway.rs b/nym-node/src/config/entry_gateway.rs index d4ef188da9..6807e55d58 100644 --- a/nym-node/src/config/entry_gateway.rs +++ b/nym-node/src/config/entry_gateway.rs @@ -5,13 +5,14 @@ use crate::config::helpers::ephemeral_gateway_config; use crate::config::persistence::EntryGatewayPaths; use crate::config::Config; use crate::error::EntryGatewayError; -use nym_config::defaults::DEFAULT_CLIENT_LISTENING_PORT; +use nym_config::defaults::{DEFAULT_CLIENT_LISTENING_PORT, TICKETBOOK_VALIDITY_DAYS}; use nym_config::helpers::inaddr_any; use nym_config::serde_helpers::de_maybe_port; use nym_gateway::node::LocalAuthenticatorOpts; use serde::{Deserialize, Serialize}; use std::net::SocketAddr; use std::path::Path; +use std::time::Duration; use super::helpers::{base_client_config, EphemeralConfig}; use super::LocalWireguardOpts; @@ -48,9 +49,12 @@ pub struct EntryGatewayConfig { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] +#[serde(default)] pub struct Debug { /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. pub message_retrieval_limit: i64, + + pub zk_nym_tickets: ZkNymTicketHandlerDebug, } impl Debug { @@ -61,6 +65,73 @@ impl Default for Debug { fn default() -> Self { Debug { message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + zk_nym_tickets: Default::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct ZkNymTicketHandlerDebug { + /// Specifies the multiplier for revoking a malformed/double-spent ticket + /// (if it has to go all the way to the nym-api for verification) + /// e.g. if one ticket grants 100Mb and `revocation_bandwidth_penalty` is set to 1.5, + /// the client will lose 150Mb + pub revocation_bandwidth_penalty: f32, + + /// Specifies the interval for attempting to resolve any failed, pending operations, + /// such as ticket verification or redemption. + #[serde(with = "humantime_serde")] + pub pending_poller: Duration, + + pub minimum_api_quorum: f32, + + /// Specifies the minimum number of tickets this gateway will attempt to redeem. + pub minimum_redemption_tickets: usize, + + /// Specifies the maximum time between two subsequent tickets redemptions. + /// That's required as nym-apis will purge all ticket information for tickets older than maximum validity. + #[serde(with = "humantime_serde")] + pub maximum_time_between_redemption: Duration, +} + +impl ZkNymTicketHandlerDebug { + pub const DEFAULT_REVOCATION_BANDWIDTH_PENALTY: f32 = 10.0; + pub const DEFAULT_PENDING_POLLER: Duration = Duration::from_secs(300); + pub const DEFAULT_MINIMUM_API_QUORUM: f32 = 0.8; + pub const DEFAULT_MINIMUM_REDEMPTION_TICKETS: usize = 100; + + // use min(4/5 of max validity, validity - 1), but making sure it's no greater than 1 day + // ASSUMPTION: our validity period is AT LEAST 2 days + // + // this could have been a constant, but it's more readable as a function + pub const fn default_maximum_time_between_redemption() -> Duration { + let desired_secs = TICKETBOOK_VALIDITY_DAYS * (86400 * 4) / 5; + let desired_secs_alt = (TICKETBOOK_VALIDITY_DAYS - 1) * 86400; + + // can't use `min` in const context + let target_secs = if desired_secs < desired_secs_alt { + desired_secs + } else { + desired_secs_alt + }; + + assert!( + target_secs > 86400, + "the maximum time between redemption can't be lower than 1 day!" + ); + Duration::from_secs(target_secs as u64) + } +} + +impl Default for ZkNymTicketHandlerDebug { + fn default() -> Self { + ZkNymTicketHandlerDebug { + revocation_bandwidth_penalty: Self::DEFAULT_REVOCATION_BANDWIDTH_PENALTY, + pending_poller: Self::DEFAULT_PENDING_POLLER, + minimum_api_quorum: Self::DEFAULT_MINIMUM_API_QUORUM, + minimum_redemption_tickets: Self::DEFAULT_MINIMUM_REDEMPTION_TICKETS, + maximum_time_between_redemption: Self::default_maximum_time_between_redemption(), } } } diff --git a/nym-node/src/config/exit_gateway.rs b/nym-node/src/config/exit_gateway.rs index 1ad3e42f02..4c3c55fad2 100644 --- a/nym-node/src/config/exit_gateway.rs +++ b/nym-node/src/config/exit_gateway.rs @@ -34,6 +34,28 @@ pub struct ExitGatewayConfig { pub network_requester: NetworkRequester, pub ip_packet_router: IpPacketRouter, + + #[serde(default)] + pub debug: Debug, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Debug { + /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. + pub message_retrieval_limit: i64, +} + +impl Debug { + const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; +} + +impl Default for Debug { + fn default() -> Self { + Debug { + message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + } + } } impl ExitGatewayConfig { @@ -49,6 +71,7 @@ impl ExitGatewayConfig { .expect("invalid default exit policy URL"), network_requester: Default::default(), ip_packet_router: Default::default(), + debug: Default::default(), } } } diff --git a/nym-node/src/config/helpers.rs b/nym-node/src/config/helpers.rs index 5f3c5b7453..1f91e290e1 100644 --- a/nym-node/src/config/helpers.rs +++ b/nym-node/src/config/helpers.rs @@ -81,6 +81,25 @@ pub fn ephemeral_gateway_config( maximum_connection_buffer_size: config.mixnet.debug.maximum_connection_buffer_size, message_retrieval_limit: config.entry_gateway.debug.message_retrieval_limit, use_legacy_framed_packet_version: false, + zk_nym_tickets: nym_gateway::config::ZkNymTicketHandlerDebug { + revocation_bandwidth_penalty: config + .entry_gateway + .debug + .zk_nym_tickets + .revocation_bandwidth_penalty, + pending_poller: config.entry_gateway.debug.zk_nym_tickets.pending_poller, + minimum_api_quorum: config.entry_gateway.debug.zk_nym_tickets.minimum_api_quorum, + minimum_redemption_tickets: config + .entry_gateway + .debug + .zk_nym_tickets + .minimum_redemption_tickets, + maximum_time_between_redemption: config + .entry_gateway + .debug + .zk_nym_tickets + .maximum_time_between_redemption, + }, ..Default::default() }, )) diff --git a/nym-node/src/config/old_configs/mod.rs b/nym-node/src/config/old_configs/mod.rs index d85e583953..295cf46ed3 100644 --- a/nym-node/src/config/old_configs/mod.rs +++ b/nym-node/src/config/old_configs/mod.rs @@ -3,6 +3,8 @@ mod old_config_v1; mod old_config_v2; +mod old_config_v3; pub use old_config_v1::try_upgrade_config_v1; pub use old_config_v2::try_upgrade_config_v2; +pub use old_config_v3::try_upgrade_config_v3; diff --git a/nym-node/src/config/old_configs/old_config_v2.rs b/nym-node/src/config/old_configs/old_config_v2.rs index 5516a8d326..28e35e66bc 100644 --- a/nym-node/src/config/old_configs/old_config_v2.rs +++ b/nym-node/src/config/old_configs/old_config_v2.rs @@ -4,9 +4,6 @@ #![allow(dead_code)] use crate::{config::*, error::KeyIOFailure}; -use entry_gateway::Debug as EntryGatewayConfigDebug; -use exit_gateway::{IpPacketRouter, IpPacketRouterDebug, NetworkRequester, NetworkRequesterDebug}; -use mixnode::{Verloc, VerlocDebug}; use nym_client_core_config_types::DebugConfig as ClientDebugConfig; use nym_config::serde_helpers::de_maybe_port; use nym_crypto::asymmetric::{ed25519, x25519}; @@ -16,10 +13,7 @@ use nym_network_requester::{ }; use nym_pemstore::{load_key, store_key, store_keypair}; use nym_sphinx_acknowledgements::AckKey; -use persistence::{ - AuthenticatorPaths, EntryGatewayPaths, ExitGatewayPaths, IpPacketRouterPaths, KeysPaths, - MixnodePaths, NetworkRequesterPaths, WireguardPaths, -}; +use old_configs::old_config_v3::*; use rand::rngs::OsRng; use serde::{Deserialize, Serialize}; @@ -90,12 +84,12 @@ pub enum NodeModeV2 { ExitGateway, } -impl From for NodeMode { +impl From for NodeModeV3 { fn from(config: NodeModeV2) -> Self { match config { - NodeModeV2::Mixnode => NodeMode::Mixnode, - NodeModeV2::EntryGateway => NodeMode::EntryGateway, - NodeModeV2::ExitGateway => NodeMode::ExitGateway, + NodeModeV2::Mixnode => NodeModeV3::Mixnode, + NodeModeV2::EntryGateway => NodeModeV3::EntryGateway, + NodeModeV2::ExitGateway => NodeModeV3::ExitGateway, } } } @@ -754,7 +748,7 @@ impl ConfigV2 { } pub async fn initialise( - paths: &AuthenticatorPaths, + paths: &AuthenticatorPathsV3, public_key: nym_crypto::asymmetric::identity::PublicKey, ) -> Result<(), NymNodeError> { let mut rng = OsRng; @@ -795,7 +789,7 @@ pub async fn initialise( pub async fn try_upgrade_config_v2>( path: P, prev_config: Option, -) -> Result { +) -> Result { tracing::debug!("Updating from 1.1.3"); let old_cfg = if let Some(prev_config) = prev_config { prev_config @@ -803,7 +797,7 @@ pub async fn try_upgrade_config_v2>( ConfigV2::read_from_path(&path)? }; - let authenticator_paths = AuthenticatorPaths::new( + let authenticator_paths = AuthenticatorPathsV3::new( old_cfg .exit_gateway .storage_paths @@ -813,20 +807,20 @@ pub async fn try_upgrade_config_v2>( .ok_or(NymNodeError::DataDirDerivationFailure)?, ); - let cfg = Config { + let cfg = ConfigV3 { save_path: old_cfg.save_path, id: old_cfg.id, mode: old_cfg.mode.into(), - host: Host { + host: HostV3 { public_ips: old_cfg.host.public_ips, hostname: old_cfg.host.hostname, location: old_cfg.host.location, }, - mixnet: Mixnet { + mixnet: MixnetV3 { bind_address: old_cfg.mixnet.bind_address, nym_api_urls: old_cfg.mixnet.nym_api_urls, nyxd_urls: old_cfg.mixnet.nyxd_urls, - debug: MixnetDebug { + debug: MixnetDebugV3 { packet_forwarding_initial_backoff: old_cfg .mixnet .debug @@ -840,8 +834,8 @@ pub async fn try_upgrade_config_v2>( unsafe_disable_noise: old_cfg.mixnet.debug.unsafe_disable_noise, }, }, - storage_paths: NymNodePaths { - keys: KeysPaths { + storage_paths: NymNodePathsV3 { + keys: KeysPathsV3 { private_ed25519_identity_key_file: old_cfg .storage_paths .keys @@ -869,7 +863,7 @@ pub async fn try_upgrade_config_v2>( }, description: old_cfg.storage_paths.description, }, - http: Http { + http: HttpV3 { bind_address: old_cfg.http.bind_address, landing_page_assets_path: old_cfg.http.landing_page_assets_path, access_token: old_cfg.http.access_token, @@ -877,13 +871,13 @@ pub async fn try_upgrade_config_v2>( expose_system_hardware: old_cfg.http.expose_system_hardware, expose_crypto_hardware: old_cfg.http.expose_crypto_hardware, }, - wireguard: Wireguard { + wireguard: WireguardV3 { enabled: old_cfg.wireguard.enabled, bind_address: old_cfg.wireguard.bind_address, private_ip: old_cfg.wireguard.private_ip, announced_port: old_cfg.wireguard.announced_port, private_network_prefix: old_cfg.wireguard.private_network_prefix, - storage_paths: WireguardPaths { + storage_paths: WireguardPathsV3 { private_diffie_hellman_key_file: old_cfg .wireguard .storage_paths @@ -894,11 +888,11 @@ pub async fn try_upgrade_config_v2>( .public_diffie_hellman_key_file, }, }, - mixnode: MixnodeConfig { - storage_paths: MixnodePaths {}, - verloc: Verloc { + mixnode: MixnodeConfigV3 { + storage_paths: MixnodePathsV3 {}, + verloc: VerlocV3 { bind_address: old_cfg.mixnode.verloc.bind_address, - debug: VerlocDebug { + debug: VerlocDebugV3 { packets_per_node: old_cfg.mixnode.verloc.debug.packets_per_node, connection_timeout: old_cfg.mixnode.verloc.debug.connection_timeout, packet_timeout: old_cfg.mixnode.verloc.debug.packet_timeout, @@ -908,13 +902,13 @@ pub async fn try_upgrade_config_v2>( retry_timeout: old_cfg.mixnode.verloc.debug.retry_timeout, }, }, - debug: mixnode::Debug { + debug: DebugV3 { node_stats_logging_delay: old_cfg.mixnode.debug.node_stats_logging_delay, node_stats_updating_delay: old_cfg.mixnode.debug.node_stats_updating_delay, }, }, - entry_gateway: EntryGatewayConfig { - storage_paths: EntryGatewayPaths { + entry_gateway: EntryGatewayConfigV3 { + storage_paths: EntryGatewayPathsV3 { clients_storage: old_cfg.entry_gateway.storage_paths.clients_storage, cosmos_mnemonic: old_cfg.entry_gateway.storage_paths.cosmos_mnemonic, authenticator: authenticator_paths.clone(), @@ -923,13 +917,13 @@ pub async fn try_upgrade_config_v2>( bind_address: old_cfg.entry_gateway.bind_address, announce_ws_port: old_cfg.entry_gateway.announce_ws_port, announce_wss_port: old_cfg.entry_gateway.announce_wss_port, - debug: EntryGatewayConfigDebug { + debug: EntryGatewayConfigDebugV3 { message_retrieval_limit: old_cfg.entry_gateway.debug.message_retrieval_limit, }, }, - exit_gateway: ExitGatewayConfig { - storage_paths: ExitGatewayPaths { - network_requester: NetworkRequesterPaths { + exit_gateway: ExitGatewayConfigV3 { + storage_paths: ExitGatewayPathsV3 { + network_requester: NetworkRequesterPathsV3 { private_ed25519_identity_key_file: old_cfg .exit_gateway .storage_paths @@ -966,7 +960,7 @@ pub async fn try_upgrade_config_v2>( .network_requester .gateway_registrations, }, - ip_packet_router: IpPacketRouterPaths { + ip_packet_router: IpPacketRouterPathsV3 { private_ed25519_identity_key_file: old_cfg .exit_gateway .storage_paths @@ -1007,8 +1001,8 @@ pub async fn try_upgrade_config_v2>( }, open_proxy: old_cfg.exit_gateway.open_proxy, upstream_exit_policy_url: old_cfg.exit_gateway.upstream_exit_policy_url, - network_requester: NetworkRequester { - debug: NetworkRequesterDebug { + network_requester: NetworkRequesterV3 { + debug: NetworkRequesterDebugV3 { enabled: old_cfg.exit_gateway.network_requester.debug.enabled, disable_poisson_rate: old_cfg .exit_gateway @@ -1018,8 +1012,8 @@ pub async fn try_upgrade_config_v2>( client_debug: old_cfg.exit_gateway.network_requester.debug.client_debug, }, }, - ip_packet_router: IpPacketRouter { - debug: IpPacketRouterDebug { + ip_packet_router: IpPacketRouterV3 { + debug: IpPacketRouterDebugV3 { enabled: old_cfg.exit_gateway.ip_packet_router.debug.enabled, disable_poisson_rate: old_cfg .exit_gateway @@ -1031,7 +1025,7 @@ pub async fn try_upgrade_config_v2>( }, }, authenticator: Default::default(), - logging: LoggingSettings {}, + logging: LoggingSettingsV3 {}, }; let public_key = load_key( diff --git a/nym-node/src/config/old_configs/old_config_v3.rs b/nym-node/src/config/old_configs/old_config_v3.rs new file mode 100644 index 0000000000..809955e5ee --- /dev/null +++ b/nym-node/src/config/old_configs/old_config_v3.rs @@ -0,0 +1,1282 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +#![allow(dead_code)] + +use crate::{config::*, error::KeyIOFailure}; +use entry_gateway::Debug as EntryGatewayConfigDebug; +use exit_gateway::{IpPacketRouter, IpPacketRouterDebug, NetworkRequester, NetworkRequesterDebug}; +use mixnode::{Verloc, VerlocDebug}; +use nym_client_core_config_types::{ + disk_persistence::{ClientKeysPaths, CommonClientPaths}, + DebugConfig as ClientDebugConfig, +}; +use nym_config::serde_helpers::de_maybe_port; +use nym_crypto::asymmetric::{ed25519, x25519}; +use nym_network_requester::{ + set_active_gateway, setup_fs_gateways_storage, store_gateway_details, CustomGatewayDetails, + GatewayDetails, +}; +use nym_pemstore::{store_key, store_keypair}; +use nym_sphinx_acknowledgements::AckKey; +use persistence::*; +use rand::rngs::OsRng; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct WireguardPathsV3 { + pub private_diffie_hellman_key_file: PathBuf, + pub public_diffie_hellman_key_file: PathBuf, +} + +impl WireguardPathsV3 { + pub fn new>(data_dir: P) -> Self { + let data_dir = data_dir.as_ref(); + WireguardPathsV3 { + private_diffie_hellman_key_file: data_dir + .join(persistence::DEFAULT_X25519_WG_DH_KEY_FILENAME), + public_diffie_hellman_key_file: data_dir + .join(persistence::DEFAULT_X25519_WG_PUBLIC_DH_KEY_FILENAME), + } + } + + pub fn x25519_wireguard_storage_paths(&self) -> nym_pemstore::KeyPairPath { + nym_pemstore::KeyPairPath::new( + &self.private_diffie_hellman_key_file, + &self.public_diffie_hellman_key_file, + ) + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct WireguardV3 { + /// Specifies whether the wireguard service is enabled on this node. + pub enabled: bool, + + /// Socket address this node will use for binding its wireguard interface. + /// default: `0.0.0.0:51822` + pub bind_address: SocketAddr, + + /// Ip address of the private wireguard network. + /// default: `10.1.0.0` + pub private_ip: IpAddr, + + /// Port announced to external clients wishing to connect to the wireguard interface. + /// Useful in the instances where the node is behind a proxy. + pub announced_port: u16, + + /// The prefix denoting the maximum number of the clients that can be connected via Wireguard. + /// The maximum value for IPv4 is 32 and for IPv6 is 128 + pub private_network_prefix: u8, + + /// Paths for wireguard keys, client registries, etc. + pub storage_paths: WireguardPathsV3, +} + +// a temporary solution until all "types" are run at the same time +#[derive(Debug, Default, Serialize, Deserialize, ValueEnum, Clone, Copy)] +#[serde(rename_all = "snake_case")] +pub enum NodeModeV3 { + #[default] + #[clap(alias = "mix")] + Mixnode, + + #[clap(alias = "entry", alias = "gateway")] + EntryGateway, + + #[clap(alias = "exit")] + ExitGateway, +} + +impl From for NodeMode { + fn from(config: NodeModeV3) -> Self { + match config { + NodeModeV3::Mixnode => NodeMode::Mixnode, + NodeModeV3::EntryGateway => NodeMode::EntryGateway, + NodeModeV3::ExitGateway => NodeMode::ExitGateway, + } + } +} + +// TODO: this is very much a WIP. we need proper ssl certificate support here +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)] +#[serde(default)] +#[serde(deny_unknown_fields)] +pub struct HostV3 { + /// Ip address(es) of this host, such as 1.1.1.1 that external clients will use for connections. + /// If no values are provided, when this node gets included in the network, + /// its ip addresses will be populated by whatever value is resolved by associated nym-api. + pub public_ips: Vec, + + /// Optional hostname of this node, for example nymtech.net. + // TODO: this is temporary. to be replaced by pulling the data directly from the certs. + #[serde(deserialize_with = "de_maybe_stringified")] + pub hostname: Option, + + /// Optional ISO 3166 alpha-2 two-letter country code of the node's **physical** location + #[serde(deserialize_with = "de_maybe_stringified")] + pub location: Option, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +#[serde(deny_unknown_fields)] +pub struct MixnetDebugV3 { + /// Initial value of an exponential backoff to reconnect to dropped TCP connection when + /// forwarding sphinx packets. + #[serde(with = "humantime_serde")] + pub packet_forwarding_initial_backoff: Duration, + + /// Maximum value of an exponential backoff to reconnect to dropped TCP connection when + /// forwarding sphinx packets. + #[serde(with = "humantime_serde")] + pub packet_forwarding_maximum_backoff: Duration, + + /// Timeout for establishing initial connection when trying to forward a sphinx packet. + #[serde(with = "humantime_serde")] + pub initial_connection_timeout: Duration, + + /// Maximum number of packets that can be stored waiting to get sent to a particular connection. + pub maximum_connection_buffer_size: usize, + + /// Specifies whether this node should **NOT** use noise protocol in the connections (currently not implemented) + pub unsafe_disable_noise: bool, +} + +impl MixnetDebugV3 { + const DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF: Duration = Duration::from_millis(10_000); + const DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF: Duration = Duration::from_millis(300_000); + const DEFAULT_INITIAL_CONNECTION_TIMEOUT: Duration = Duration::from_millis(1_500); + const DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE: usize = 2000; +} + +impl Default for MixnetDebugV3 { + fn default() -> Self { + MixnetDebugV3 { + packet_forwarding_initial_backoff: Self::DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF, + packet_forwarding_maximum_backoff: Self::DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF, + initial_connection_timeout: Self::DEFAULT_INITIAL_CONNECTION_TIMEOUT, + maximum_connection_buffer_size: Self::DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE, + // to be changed by @SW once the implementation is there + unsafe_disable_noise: true, + } + } +} + +impl Default for MixnetV3 { + fn default() -> Self { + // SAFETY: + // our hardcoded values should always be valid + #[allow(clippy::expect_used)] + // is if there's anything set in the environment, otherwise fallback to mainnet + let nym_api_urls = if let Ok(env_value) = env::var(var_names::NYM_API) { + parse_urls(&env_value) + } else { + vec![mainnet::NYM_API.parse().expect("Invalid default API URL")] + }; + + #[allow(clippy::expect_used)] + let nyxd_urls = if let Ok(env_value) = env::var(var_names::NYXD) { + parse_urls(&env_value) + } else { + vec![mainnet::NYXD_URL.parse().expect("Invalid default nyxd URL")] + }; + + MixnetV3 { + bind_address: SocketAddr::new(inaddr_any(), DEFAULT_MIXNET_PORT), + nym_api_urls, + nyxd_urls, + debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +#[serde(deny_unknown_fields)] +pub struct MixnetV3 { + /// Address this node will bind to for listening for mixnet packets + /// default: `0.0.0.0:1789` + pub bind_address: SocketAddr, + + /// Addresses to nym APIs from which the node gets the view of the network. + pub nym_api_urls: Vec, + + /// Addresses to nyxd which the node uses to interact with the nyx chain. + pub nyxd_urls: Vec, + + #[serde(default)] + pub debug: MixnetDebugV3, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct KeysPathsV3 { + /// Path to file containing ed25519 identity private key. + pub private_ed25519_identity_key_file: PathBuf, + + /// Path to file containing ed25519 identity public key. + pub public_ed25519_identity_key_file: PathBuf, + + /// Path to file containing x25519 sphinx private key. + pub private_x25519_sphinx_key_file: PathBuf, + + /// Path to file containing x25519 sphinx public key. + pub public_x25519_sphinx_key_file: PathBuf, + + /// Path to file containing x25519 noise private key. + pub private_x25519_noise_key_file: PathBuf, + + /// Path to file containing x25519 noise public key. + pub public_x25519_noise_key_file: PathBuf, +} + +impl KeysPathsV3 { + pub fn new>(data_dir: P) -> Self { + let data_dir = data_dir.as_ref(); + + KeysPathsV3 { + private_ed25519_identity_key_file: data_dir + .join(DEFAULT_ED25519_PRIVATE_IDENTITY_KEY_FILENAME), + public_ed25519_identity_key_file: data_dir + .join(DEFAULT_ED25519_PUBLIC_IDENTITY_KEY_FILENAME), + private_x25519_sphinx_key_file: data_dir + .join(DEFAULT_X25519_PRIVATE_SPHINX_KEY_FILENAME), + public_x25519_sphinx_key_file: data_dir.join(DEFAULT_X25519_PUBLIC_SPHINX_KEY_FILENAME), + private_x25519_noise_key_file: data_dir.join(DEFAULT_X25519_PRIVATE_NOISE_KEY_FILENAME), + public_x25519_noise_key_file: data_dir.join(DEFAULT_X25519_PUBLIC_NOISE_KEY_FILENAME), + } + } + + pub fn ed25519_identity_storage_paths(&self) -> nym_pemstore::KeyPairPath { + nym_pemstore::KeyPairPath::new( + &self.private_ed25519_identity_key_file, + &self.public_ed25519_identity_key_file, + ) + } + + pub fn x25519_sphinx_storage_paths(&self) -> nym_pemstore::KeyPairPath { + nym_pemstore::KeyPairPath::new( + &self.private_x25519_sphinx_key_file, + &self.public_x25519_sphinx_key_file, + ) + } + + pub fn x25519_noise_storage_paths(&self) -> nym_pemstore::KeyPairPath { + nym_pemstore::KeyPairPath::new( + &self.private_x25519_noise_key_file, + &self.public_x25519_noise_key_file, + ) + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct NymNodePathsV3 { + pub keys: KeysPathsV3, + + /// Path to a file containing basic node description: human-readable name, website, details, etc. + pub description: PathBuf, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +#[serde(deny_unknown_fields)] +pub struct HttpV3 { + /// Socket address this node will use for binding its http API. + /// default: `0.0.0.0:8080` + pub bind_address: SocketAddr, + + /// Path to assets directory of custom landing page of this node. + #[serde(deserialize_with = "de_maybe_stringified")] + pub landing_page_assets_path: Option, + + /// An optional bearer token for accessing certain http endpoints. + /// Currently only used for obtaining mixnode's stats. + #[serde(default)] + pub access_token: Option, + + /// Specify whether basic system information should be exposed. + /// default: true + pub expose_system_info: bool, + + /// Specify whether basic system hardware information should be exposed. + /// This option is superseded by `expose_system_info` + /// default: true + pub expose_system_hardware: bool, + + /// Specify whether detailed system crypto hardware information should be exposed. + /// This option is superseded by `expose_system_hardware` + /// default: true + pub expose_crypto_hardware: bool, +} + +impl Default for HttpV3 { + fn default() -> Self { + HttpV3 { + bind_address: SocketAddr::new(inaddr_any(), DEFAULT_HTTP_PORT), + landing_page_assets_path: None, + access_token: None, + expose_system_info: true, + expose_system_hardware: true, + expose_crypto_hardware: true, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct MixnodePathsV3 {} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct DebugV3 { + /// Delay between each subsequent node statistics being logged to the console + #[serde(with = "humantime_serde")] + pub node_stats_logging_delay: Duration, + + /// Delay between each subsequent node statistics being updated + #[serde(with = "humantime_serde")] + pub node_stats_updating_delay: Duration, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct VerlocDebugV3 { + /// Specifies number of echo packets sent to each node during a measurement run. + pub packets_per_node: usize, + + /// Specifies maximum amount of time to wait for the connection to get established. + #[serde(with = "humantime_serde")] + pub connection_timeout: Duration, + + /// Specifies maximum amount of time to wait for the reply packet to arrive before abandoning the test. + #[serde(with = "humantime_serde")] + pub packet_timeout: Duration, + + /// Specifies delay between subsequent test packets being sent (after receiving a reply). + #[serde(with = "humantime_serde")] + pub delay_between_packets: Duration, + + /// Specifies number of nodes being tested at once. + pub tested_nodes_batch_size: usize, + + /// Specifies delay between subsequent test runs. + #[serde(with = "humantime_serde")] + pub testing_interval: Duration, + + /// Specifies delay between attempting to run the measurement again if the previous run failed + /// due to being unable to get the list of nodes. + #[serde(with = "humantime_serde")] + pub retry_timeout: Duration, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct VerlocV3 { + /// Socket address this node will use for binding its verloc API. + /// default: `0.0.0.0:1790` + pub bind_address: SocketAddr, + + #[serde(default)] + pub debug: VerlocDebugV3, +} + +impl VerlocDebugV3 { + const DEFAULT_PACKETS_PER_NODE: usize = 100; + const DEFAULT_CONNECTION_TIMEOUT: Duration = Duration::from_millis(5000); + const DEFAULT_PACKET_TIMEOUT: Duration = Duration::from_millis(1500); + const DEFAULT_DELAY_BETWEEN_PACKETS: Duration = Duration::from_millis(50); + const DEFAULT_BATCH_SIZE: usize = 50; + const DEFAULT_TESTING_INTERVAL: Duration = Duration::from_secs(60 * 60 * 12); + const DEFAULT_RETRY_TIMEOUT: Duration = Duration::from_secs(60 * 30); +} + +impl Default for VerlocDebugV3 { + fn default() -> Self { + VerlocDebugV3 { + packets_per_node: Self::DEFAULT_PACKETS_PER_NODE, + connection_timeout: Self::DEFAULT_CONNECTION_TIMEOUT, + packet_timeout: Self::DEFAULT_PACKET_TIMEOUT, + delay_between_packets: Self::DEFAULT_DELAY_BETWEEN_PACKETS, + tested_nodes_batch_size: Self::DEFAULT_BATCH_SIZE, + testing_interval: Self::DEFAULT_TESTING_INTERVAL, + retry_timeout: Self::DEFAULT_RETRY_TIMEOUT, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MixnodeConfigV3 { + pub storage_paths: MixnodePathsV3, + + pub verloc: VerlocV3, + + #[serde(default)] + pub debug: DebugV3, +} + +impl DebugV3 { + const DEFAULT_NODE_STATS_LOGGING_DELAY: Duration = Duration::from_millis(60_000); + const DEFAULT_NODE_STATS_UPDATING_DELAY: Duration = Duration::from_millis(30_000); +} + +impl Default for DebugV3 { + fn default() -> Self { + DebugV3 { + node_stats_logging_delay: Self::DEFAULT_NODE_STATS_LOGGING_DELAY, + node_stats_updating_delay: Self::DEFAULT_NODE_STATS_UPDATING_DELAY, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct EntryGatewayPathsV3 { + /// Path to sqlite database containing all persistent data: messages for offline clients, + /// derived shared keys and available client bandwidths. + pub clients_storage: PathBuf, + + /// Path to file containing cosmos account mnemonic used for zk-nym redemption. + pub cosmos_mnemonic: PathBuf, + + pub authenticator: AuthenticatorPathsV3, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct EntryGatewayConfigDebugV3 { + /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. + pub message_retrieval_limit: i64, +} + +impl EntryGatewayConfigDebugV3 { + const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; +} + +impl Default for EntryGatewayConfigDebugV3 { + fn default() -> Self { + EntryGatewayConfigDebugV3 { + message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct EntryGatewayConfigV3 { + pub storage_paths: EntryGatewayPathsV3, + + /// Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet + /// or if it also accepts non-paying clients + pub enforce_zk_nyms: bool, + + /// Socket address this node will use for binding its client websocket API. + /// default: `0.0.0.0:9000` + pub bind_address: SocketAddr, + + /// Custom announced port for listening for websocket client traffic. + /// If unspecified, the value from the `bind_address` will be used instead + /// default: None + #[serde(deserialize_with = "de_maybe_port")] + pub announce_ws_port: Option, + + /// If applicable, announced port for listening for secure websocket client traffic. + /// (default: None) + #[serde(deserialize_with = "de_maybe_port")] + pub announce_wss_port: Option, + + #[serde(default)] + pub debug: EntryGatewayConfigDebugV3, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct NetworkRequesterPathsV3 { + /// Path to file containing network requester ed25519 identity private key. + pub private_ed25519_identity_key_file: PathBuf, + + /// Path to file containing network requester ed25519 identity public key. + pub public_ed25519_identity_key_file: PathBuf, + + /// Path to file containing network requester x25519 diffie hellman private key. + pub private_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing network requester x25519 diffie hellman public key. + pub public_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing key used for encrypting and decrypting the content of an + /// acknowledgement so that nobody besides the client knows which packet it refers to. + pub ack_key_file: PathBuf, + + /// Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. + pub reply_surb_database: PathBuf, + + /// Normally this is a path to the file containing information about gateways used by this client, + /// i.e. details such as their public keys, owner addresses or the network information. + /// but in this case it just has the basic information of "we're using custom gateway". + /// Due to how clients are started up, this file has to exist. + pub gateway_registrations: PathBuf, + // it's possible we might have to add credential storage here for return tickets +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct IpPacketRouterPathsV3 { + /// Path to file containing ip packet router ed25519 identity private key. + pub private_ed25519_identity_key_file: PathBuf, + + /// Path to file containing ip packet router ed25519 identity public key. + pub public_ed25519_identity_key_file: PathBuf, + + /// Path to file containing ip packet router x25519 diffie hellman private key. + pub private_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing ip packet router x25519 diffie hellman public key. + pub public_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing key used for encrypting and decrypting the content of an + /// acknowledgement so that nobody besides the client knows which packet it refers to. + pub ack_key_file: PathBuf, + + /// Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. + pub reply_surb_database: PathBuf, + + /// Normally this is a path to the file containing information about gateways used by this client, + /// i.e. details such as their public keys, owner addresses or the network information. + /// but in this case it just has the basic information of "we're using custom gateway". + /// Due to how clients are started up, this file has to exist. + pub gateway_registrations: PathBuf, + // it's possible we might have to add credential storage here for return tickets +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct AuthenticatorPathsV3 { + /// Path to file containing authenticator ed25519 identity private key. + pub private_ed25519_identity_key_file: PathBuf, + + /// Path to file containing authenticator ed25519 identity public key. + pub public_ed25519_identity_key_file: PathBuf, + + /// Path to file containing authenticator x25519 diffie hellman private key. + pub private_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing authenticator x25519 diffie hellman public key. + pub public_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing key used for encrypting and decrypting the content of an + /// acknowledgement so that nobody besides the client knows which packet it refers to. + pub ack_key_file: PathBuf, + + /// Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. + pub reply_surb_database: PathBuf, + + /// Normally this is a path to the file containing information about gateways used by this client, + /// i.e. details such as their public keys, owner addresses or the network information. + /// but in this case it just has the basic information of "we're using custom gateway". + /// Due to how clients are started up, this file has to exist. + pub gateway_registrations: PathBuf, + // it's possible we might have to add credential storage here for return tickets +} + +impl AuthenticatorPathsV3 { + pub fn new>(data_dir: P) -> Self { + let data_dir = data_dir.as_ref(); + AuthenticatorPathsV3 { + private_ed25519_identity_key_file: data_dir + .join(DEFAULT_ED25519_AUTH_PRIVATE_IDENTITY_KEY_FILENAME), + public_ed25519_identity_key_file: data_dir + .join(DEFAULT_ED25519_AUTH_PUBLIC_IDENTITY_KEY_FILENAME), + private_x25519_diffie_hellman_key_file: data_dir + .join(DEFAULT_X25519_AUTH_PRIVATE_DH_KEY_FILENAME), + public_x25519_diffie_hellman_key_file: data_dir + .join(DEFAULT_X25519_AUTH_PUBLIC_DH_KEY_FILENAME), + ack_key_file: data_dir.join(DEFAULT_AUTH_ACK_KEY_FILENAME), + reply_surb_database: data_dir.join(DEFAULT_AUTH_REPLY_SURB_DB_FILENAME), + gateway_registrations: data_dir.join(DEFAULT_AUTH_GATEWAYS_DB_FILENAME), + } + } + + pub fn to_common_client_paths(&self) -> CommonClientPaths { + CommonClientPaths { + keys: ClientKeysPaths { + private_identity_key_file: self.private_ed25519_identity_key_file.clone(), + public_identity_key_file: self.public_ed25519_identity_key_file.clone(), + private_encryption_key_file: self.private_x25519_diffie_hellman_key_file.clone(), + public_encryption_key_file: self.public_x25519_diffie_hellman_key_file.clone(), + ack_key_file: self.ack_key_file.clone(), + }, + gateway_registrations: self.gateway_registrations.clone(), + + // not needed for embedded providers + credentials_database: Default::default(), + reply_surb_database: self.reply_surb_database.clone(), + } + } + + pub fn ed25519_identity_storage_paths(&self) -> nym_pemstore::KeyPairPath { + nym_pemstore::KeyPairPath::new( + &self.private_ed25519_identity_key_file, + &self.public_ed25519_identity_key_file, + ) + } + + pub fn x25519_diffie_hellman_storage_paths(&self) -> nym_pemstore::KeyPairPath { + nym_pemstore::KeyPairPath::new( + &self.private_x25519_diffie_hellman_key_file, + &self.public_x25519_diffie_hellman_key_file, + ) + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ExitGatewayPathsV3 { + pub network_requester: NetworkRequesterPathsV3, + + pub ip_packet_router: IpPacketRouterPathsV3, + + pub authenticator: AuthenticatorPathsV3, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +pub struct AuthenticatorV3 { + #[serde(default)] + pub debug: AuthenticatorDebugV3, +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct AuthenticatorDebugV3 { + /// Specifies whether authenticator service is enabled in this process. + /// This is only here for debugging purposes as exit gateway should always run + /// the authenticator. + pub enabled: bool, + + /// Disable Poisson sending rate. + /// This is equivalent to setting client_debug.traffic.disable_main_poisson_packet_distribution = true + /// (or is it (?)) + pub disable_poisson_rate: bool, + + /// Shared detailed client configuration options + #[serde(flatten)] + pub client_debug: ClientDebugConfig, +} + +impl Default for AuthenticatorDebugV3 { + fn default() -> Self { + AuthenticatorDebugV3 { + enabled: true, + disable_poisson_rate: true, + client_debug: Default::default(), + } + } +} + +#[allow(clippy::derivable_impls)] +impl Default for AuthenticatorV3 { + fn default() -> Self { + AuthenticatorV3 { + debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct IpPacketRouterDebugV3 { + /// Specifies whether ip packet routing service is enabled in this process. + /// This is only here for debugging purposes as exit gateway should always run **both** + /// network requester and an ip packet router. + pub enabled: bool, + + /// Disable Poisson sending rate. + /// This is equivalent to setting client_debug.traffic.disable_main_poisson_packet_distribution = true + /// (or is it (?)) + pub disable_poisson_rate: bool, + + /// Shared detailed client configuration options + #[serde(flatten)] + pub client_debug: ClientDebugConfig, +} + +impl Default for IpPacketRouterDebugV3 { + fn default() -> Self { + IpPacketRouterDebugV3 { + enabled: true, + disable_poisson_rate: true, + client_debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +pub struct IpPacketRouterV3 { + #[serde(default)] + pub debug: IpPacketRouterDebugV3, +} + +#[allow(clippy::derivable_impls)] +impl Default for IpPacketRouterV3 { + fn default() -> Self { + IpPacketRouterV3 { + debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +pub struct NetworkRequesterDebugV3 { + /// Specifies whether network requester service is enabled in this process. + /// This is only here for debugging purposes as exit gateway should always run **both** + /// network requester and an ip packet router. + pub enabled: bool, + + /// Disable Poisson sending rate. + /// This is equivalent to setting client_debug.traffic.disable_main_poisson_packet_distribution = true + /// (or is it (?)) + pub disable_poisson_rate: bool, + + /// Shared detailed client configuration options + #[serde(flatten)] + pub client_debug: ClientDebugConfig, +} + +impl Default for NetworkRequesterDebugV3 { + fn default() -> Self { + NetworkRequesterDebugV3 { + enabled: true, + disable_poisson_rate: true, + client_debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +pub struct NetworkRequesterV3 { + #[serde(default)] + pub debug: NetworkRequesterDebugV3, +} + +#[allow(clippy::derivable_impls)] +impl Default for NetworkRequesterV3 { + fn default() -> Self { + NetworkRequesterV3 { + debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ExitGatewayConfigV3 { + pub storage_paths: ExitGatewayPathsV3, + + /// specifies whether this exit node should run in 'open-proxy' mode + /// and thus would attempt to resolve **ANY** request it receives. + pub open_proxy: bool, + + /// Specifies the url for an upstream source of the exit policy used by this node. + pub upstream_exit_policy_url: Url, + + pub network_requester: NetworkRequesterV3, + + pub ip_packet_router: IpPacketRouterV3, +} + +#[derive(Debug, Default, Copy, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct LoggingSettingsV3 { + // well, we need to implement something here at some point... +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigV3 { + // additional metadata holding on-disk location of this config file + #[serde(skip)] + pub(crate) save_path: Option, + + /// Human-readable ID of this particular node. + pub id: String, + + /// Current mode of this nym-node. + /// Expect this field to be changed in the future to allow running the node in multiple modes (i.e. mixnode + gateway) + pub mode: NodeModeV3, + + pub host: HostV3, + + pub mixnet: MixnetV3, + + /// Storage paths to persistent nym-node data, such as its long term keys. + pub storage_paths: NymNodePathsV3, + + #[serde(default)] + pub http: HttpV3, + + pub wireguard: WireguardV3, + + pub mixnode: MixnodeConfigV3, + + pub entry_gateway: EntryGatewayConfigV3, + + pub exit_gateway: ExitGatewayConfigV3, + + pub authenticator: AuthenticatorV3, + + #[serde(default)] + pub logging: LoggingSettingsV3, +} + +impl NymConfigTemplate for ConfigV3 { + fn template(&self) -> &'static str { + CONFIG_TEMPLATE + } +} + +impl ConfigV3 { + pub fn save(&self) -> Result<(), NymNodeError> { + let save_location = self.save_location(); + debug!( + "attempting to save config file to '{}'", + save_location.display() + ); + save_formatted_config_to_file(self, &save_location).map_err(|source| { + NymNodeError::ConfigSaveFailure { + id: self.id.clone(), + path: save_location, + source, + } + }) + } + + pub fn save_location(&self) -> PathBuf { + self.save_path + .clone() + .unwrap_or(self.default_save_location()) + } + + pub fn default_save_location(&self) -> PathBuf { + default_config_filepath(&self.id) + } + + pub fn default_data_directory>(config_path: P) -> Result { + let config_path = config_path.as_ref(); + + // we got a proper path to the .toml file + let Some(config_dir) = config_path.parent() else { + error!( + "'{}' does not have a parent directory. Have you pointed to the fs root?", + config_path.display() + ); + return Err(NymNodeError::DataDirDerivationFailure); + }; + + let Some(config_dir_name) = config_dir.file_name() else { + error!( + "could not obtain parent directory name of '{}'. Have you used relative paths?", + config_path.display() + ); + return Err(NymNodeError::DataDirDerivationFailure); + }; + + if config_dir_name != DEFAULT_CONFIG_DIR { + error!( + "the parent directory of '{}' ({}) is not {DEFAULT_CONFIG_DIR}. currently this is not supported", + config_path.display(), config_dir_name.to_str().unwrap_or("UNKNOWN") + ); + return Err(NymNodeError::DataDirDerivationFailure); + } + + let Some(node_dir) = config_dir.parent() else { + error!( + "'{}' does not have a parent directory. Have you pointed to the fs root?", + config_dir.display() + ); + return Err(NymNodeError::DataDirDerivationFailure); + }; + + Ok(node_dir.join(DEFAULT_DATA_DIR)) + } + + // simple wrapper that reads config file and assigns path location + fn read_from_path>(path: P) -> Result { + let path = path.as_ref(); + let mut loaded: ConfigV3 = + read_config_from_toml_file(path).map_err(|source| NymNodeError::ConfigLoadFailure { + path: path.to_path_buf(), + source, + })?; + loaded.save_path = Some(path.to_path_buf()); + debug!("loaded config file from {}", path.display()); + Ok(loaded) + } + + pub fn read_from_toml_file>(path: P) -> Result { + Self::read_from_path(path) + } +} + +pub async fn initialise( + paths: &AuthenticatorPaths, + public_key: nym_crypto::asymmetric::identity::PublicKey, +) -> Result<(), NymNodeError> { + let mut rng = OsRng; + let ed25519_keys = ed25519::KeyPair::new(&mut rng); + let x25519_keys = x25519::KeyPair::new(&mut rng); + let aes128ctr_key = AckKey::new(&mut rng); + let gateway_details = GatewayDetails::Custom(CustomGatewayDetails::new(public_key)).into(); + + store_keypair(&ed25519_keys, &paths.ed25519_identity_storage_paths()).map_err(|e| { + KeyIOFailure::KeyPairStoreFailure { + keys: "ed25519-identity".to_string(), + paths: paths.ed25519_identity_storage_paths(), + err: e, + } + })?; + store_keypair(&x25519_keys, &paths.x25519_diffie_hellman_storage_paths()).map_err(|e| { + KeyIOFailure::KeyPairStoreFailure { + keys: "x25519-dh".to_string(), + paths: paths.x25519_diffie_hellman_storage_paths(), + err: e, + } + })?; + store_key(&aes128ctr_key, &paths.ack_key_file).map_err(|e| KeyIOFailure::KeyStoreFailure { + key: "ack".to_string(), + path: paths.ack_key_file.clone(), + err: e, + })?; + + // insert all required information into the gateways store + // (I hate that we have to do it, but that's currently the simplest thing to do) + let storage = setup_fs_gateways_storage(&paths.gateway_registrations).await?; + store_gateway_details(&storage, &gateway_details).await?; + set_active_gateway(&storage, &gateway_details.gateway_id().to_base58_string()).await?; + + Ok(()) +} + +pub async fn try_upgrade_config_v3>( + path: P, + prev_config: Option, +) -> Result { + tracing::debug!("Updating from 1.1.4"); + let old_cfg = if let Some(prev_config) = prev_config { + prev_config + } else { + ConfigV3::read_from_path(&path)? + }; + + let exit_gateway_paths = ExitGatewayPaths::new( + old_cfg + .exit_gateway + .storage_paths + .network_requester + .public_ed25519_identity_key_file + .parent() + .ok_or(NymNodeError::DataDirDerivationFailure)?, + ); + + let cfg = Config { + save_path: old_cfg.save_path, + id: old_cfg.id, + mode: old_cfg.mode.into(), + host: Host { + public_ips: old_cfg.host.public_ips, + hostname: old_cfg.host.hostname, + location: old_cfg.host.location, + }, + mixnet: Mixnet { + bind_address: old_cfg.mixnet.bind_address, + nym_api_urls: old_cfg.mixnet.nym_api_urls, + nyxd_urls: old_cfg.mixnet.nyxd_urls, + debug: MixnetDebug { + packet_forwarding_initial_backoff: old_cfg + .mixnet + .debug + .packet_forwarding_initial_backoff, + packet_forwarding_maximum_backoff: old_cfg + .mixnet + .debug + .packet_forwarding_maximum_backoff, + initial_connection_timeout: old_cfg.mixnet.debug.initial_connection_timeout, + maximum_connection_buffer_size: old_cfg.mixnet.debug.maximum_connection_buffer_size, + unsafe_disable_noise: old_cfg.mixnet.debug.unsafe_disable_noise, + }, + }, + storage_paths: NymNodePaths { + keys: KeysPaths { + private_ed25519_identity_key_file: old_cfg + .storage_paths + .keys + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .storage_paths + .keys + .public_ed25519_identity_key_file, + private_x25519_sphinx_key_file: old_cfg + .storage_paths + .keys + .private_x25519_sphinx_key_file, + public_x25519_sphinx_key_file: old_cfg + .storage_paths + .keys + .public_x25519_sphinx_key_file, + private_x25519_noise_key_file: old_cfg + .storage_paths + .keys + .private_x25519_noise_key_file, + public_x25519_noise_key_file: old_cfg + .storage_paths + .keys + .public_x25519_noise_key_file, + }, + description: old_cfg.storage_paths.description, + }, + http: Http { + bind_address: old_cfg.http.bind_address, + landing_page_assets_path: old_cfg.http.landing_page_assets_path, + access_token: old_cfg.http.access_token, + expose_system_info: old_cfg.http.expose_system_info, + expose_system_hardware: old_cfg.http.expose_system_hardware, + expose_crypto_hardware: old_cfg.http.expose_crypto_hardware, + }, + wireguard: Wireguard { + enabled: old_cfg.wireguard.enabled, + bind_address: old_cfg.wireguard.bind_address, + private_ip: old_cfg.wireguard.private_ip, + announced_port: old_cfg.wireguard.announced_port, + private_network_prefix: old_cfg.wireguard.private_network_prefix, + storage_paths: WireguardPaths { + private_diffie_hellman_key_file: old_cfg + .wireguard + .storage_paths + .private_diffie_hellman_key_file, + public_diffie_hellman_key_file: old_cfg + .wireguard + .storage_paths + .public_diffie_hellman_key_file, + }, + }, + mixnode: MixnodeConfig { + storage_paths: MixnodePaths {}, + verloc: Verloc { + bind_address: old_cfg.mixnode.verloc.bind_address, + debug: VerlocDebug { + packets_per_node: old_cfg.mixnode.verloc.debug.packets_per_node, + connection_timeout: old_cfg.mixnode.verloc.debug.connection_timeout, + packet_timeout: old_cfg.mixnode.verloc.debug.packet_timeout, + delay_between_packets: old_cfg.mixnode.verloc.debug.delay_between_packets, + tested_nodes_batch_size: old_cfg.mixnode.verloc.debug.tested_nodes_batch_size, + testing_interval: old_cfg.mixnode.verloc.debug.testing_interval, + retry_timeout: old_cfg.mixnode.verloc.debug.retry_timeout, + }, + }, + debug: mixnode::Debug { + node_stats_logging_delay: old_cfg.mixnode.debug.node_stats_logging_delay, + node_stats_updating_delay: old_cfg.mixnode.debug.node_stats_updating_delay, + }, + }, + entry_gateway: EntryGatewayConfig { + storage_paths: EntryGatewayPaths { + clients_storage: old_cfg.entry_gateway.storage_paths.clients_storage, + cosmos_mnemonic: old_cfg.entry_gateway.storage_paths.cosmos_mnemonic, + authenticator: AuthenticatorPaths { + private_ed25519_identity_key_file: old_cfg + .entry_gateway + .storage_paths + .authenticator + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .entry_gateway + .storage_paths + .authenticator + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .entry_gateway + .storage_paths + .authenticator + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .entry_gateway + .storage_paths + .authenticator + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .entry_gateway + .storage_paths + .authenticator + .ack_key_file, + reply_surb_database: old_cfg + .entry_gateway + .storage_paths + .authenticator + .reply_surb_database, + gateway_registrations: old_cfg + .entry_gateway + .storage_paths + .authenticator + .gateway_registrations, + }, + }, + enforce_zk_nyms: old_cfg.entry_gateway.enforce_zk_nyms, + bind_address: old_cfg.entry_gateway.bind_address, + announce_ws_port: old_cfg.entry_gateway.announce_ws_port, + announce_wss_port: old_cfg.entry_gateway.announce_wss_port, + debug: EntryGatewayConfigDebug { + message_retrieval_limit: old_cfg.entry_gateway.debug.message_retrieval_limit, + // \/ ADDED + zk_nym_tickets: Default::default(), + }, + }, + exit_gateway: ExitGatewayConfig { + storage_paths: ExitGatewayPaths { + clients_storage: exit_gateway_paths.clients_storage, + network_requester: NetworkRequesterPaths { + private_ed25519_identity_key_file: old_cfg + .exit_gateway + .storage_paths + .network_requester + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .exit_gateway + .storage_paths + .network_requester + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .exit_gateway + .storage_paths + .network_requester + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .exit_gateway + .storage_paths + .network_requester + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .exit_gateway + .storage_paths + .network_requester + .ack_key_file, + reply_surb_database: old_cfg + .exit_gateway + .storage_paths + .network_requester + .reply_surb_database, + gateway_registrations: old_cfg + .exit_gateway + .storage_paths + .network_requester + .gateway_registrations, + }, + ip_packet_router: IpPacketRouterPaths { + private_ed25519_identity_key_file: old_cfg + .exit_gateway + .storage_paths + .ip_packet_router + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .exit_gateway + .storage_paths + .ip_packet_router + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .exit_gateway + .storage_paths + .ip_packet_router + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .exit_gateway + .storage_paths + .ip_packet_router + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .exit_gateway + .storage_paths + .ip_packet_router + .ack_key_file, + reply_surb_database: old_cfg + .exit_gateway + .storage_paths + .ip_packet_router + .reply_surb_database, + gateway_registrations: old_cfg + .exit_gateway + .storage_paths + .ip_packet_router + .gateway_registrations, + }, + authenticator: AuthenticatorPaths { + private_ed25519_identity_key_file: old_cfg + .exit_gateway + .storage_paths + .authenticator + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .exit_gateway + .storage_paths + .authenticator + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .exit_gateway + .storage_paths + .authenticator + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .exit_gateway + .storage_paths + .authenticator + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .exit_gateway + .storage_paths + .authenticator + .ack_key_file, + reply_surb_database: old_cfg + .exit_gateway + .storage_paths + .authenticator + .reply_surb_database, + gateway_registrations: old_cfg + .exit_gateway + .storage_paths + .authenticator + .gateway_registrations, + }, + }, + open_proxy: old_cfg.exit_gateway.open_proxy, + upstream_exit_policy_url: old_cfg.exit_gateway.upstream_exit_policy_url, + network_requester: NetworkRequester { + debug: NetworkRequesterDebug { + enabled: old_cfg.exit_gateway.network_requester.debug.enabled, + disable_poisson_rate: old_cfg + .exit_gateway + .network_requester + .debug + .disable_poisson_rate, + client_debug: old_cfg.exit_gateway.network_requester.debug.client_debug, + }, + }, + ip_packet_router: IpPacketRouter { + debug: IpPacketRouterDebug { + enabled: old_cfg.exit_gateway.ip_packet_router.debug.enabled, + disable_poisson_rate: old_cfg + .exit_gateway + .ip_packet_router + .debug + .disable_poisson_rate, + client_debug: old_cfg.exit_gateway.ip_packet_router.debug.client_debug, + }, + }, + debug: Default::default(), + }, + authenticator: Default::default(), + logging: LoggingSettings {}, + }; + + Ok(cfg) +} diff --git a/nym-node/src/config/persistence.rs b/nym-node/src/config/persistence.rs index 3861f0e7ac..3ae8595fe8 100644 --- a/nym-node/src/config/persistence.rs +++ b/nym-node/src/config/persistence.rs @@ -144,7 +144,7 @@ pub struct MixnodePaths {} #[serde(deny_unknown_fields)] pub struct EntryGatewayPaths { /// Path to sqlite database containing all persistent data: messages for offline clients, - /// derived shared keys and available client bandwidths. + /// derived shared keys, available client bandwidths and wireguard peers. pub clients_storage: PathBuf, /// Path to file containing cosmos account mnemonic used for zk-nym redemption. @@ -203,6 +203,10 @@ impl EntryGatewayPaths { #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] #[serde(deny_unknown_fields)] pub struct ExitGatewayPaths { + /// Path to sqlite database containing all persistent data: messages for offline clients, + /// derived shared keys, available client bandwidths and wireguard peers. + pub clients_storage: PathBuf, + pub network_requester: NetworkRequesterPaths, pub ip_packet_router: IpPacketRouterPaths, @@ -454,6 +458,7 @@ impl ExitGatewayPaths { pub fn new>(data_dir: P) -> Self { let data_dir = data_dir.as_ref(); ExitGatewayPaths { + clients_storage: data_dir.join(DEFAULT_CLIENTS_STORAGE_FILENAME), network_requester: NetworkRequesterPaths::new(data_dir), ip_packet_router: IpPacketRouterPaths::new(data_dir), authenticator: AuthenticatorPaths::new(data_dir), diff --git a/nym-node/src/config/template.rs b/nym-node/src/config/template.rs index 0c2cf47f51..f1576f2fe2 100644 --- a/nym-node/src/config/template.rs +++ b/nym-node/src/config/template.rs @@ -170,7 +170,7 @@ announce_wss_port = {{#if entry_gateway.announce_wss_port }} {{ entry_gateway.an [entry_gateway.storage_paths] # Path to sqlite database containing all persistent data: messages for offline clients, -# derived shared keys and available client bandwidths. +# derived shared keys, available client bandwidths and wireguard peers. clients_storage = '{{ entry_gateway.storage_paths.clients_storage }}' # Path to file containing cosmos account mnemonic used for zk-nym redemption. @@ -221,6 +221,10 @@ upstream_exit_policy_url = '{{ exit_gateway.upstream_exit_policy_url }}' [exit_gateway.storage_paths] +# Path to sqlite database containing all persistent data: messages for offline clients, +# derived shared keys, available client bandwidths and wireguard peers. +clients_storage = '{{ exit_gateway.storage_paths.clients_storage }}' + [exit_gateway.storage_paths.network_requester] # Path to file containing network requester ed25519 identity private key. private_ed25519_identity_key_file = '{{ exit_gateway.storage_paths.network_requester.private_ed25519_identity_key_file }}' diff --git a/nym-node/src/config/upgrade_helpers.rs b/nym-node/src/config/upgrade_helpers.rs index 1eb5115414..4645b1a285 100644 --- a/nym-node/src/config/upgrade_helpers.rs +++ b/nym-node/src/config/upgrade_helpers.rs @@ -9,7 +9,8 @@ use std::path::Path; // currently there are no upgrades async fn try_upgrade_config(path: &Path) -> Result<(), NymNodeError> { let cfg = try_upgrade_config_v1(path, None).await.ok(); - match try_upgrade_config_v2(path, cfg).await { + let cfg = try_upgrade_config_v2(path, cfg).await.ok(); + match try_upgrade_config_v3(path, cfg).await { Ok(cfg) => cfg.save(), Err(e) => { tracing::error!("Failed to finish upgrade - {e}"); diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index c851decf23..d0361b5057 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -110,6 +110,8 @@ pub struct ExitGatewayData { auth_ed25519: ed25519::PublicKey, auth_x25519: x25519::PublicKey, + + client_storage: nym_gateway::node::PersistentStorage, } impl ExitGatewayData { @@ -217,7 +219,7 @@ impl ExitGatewayData { Ok(()) } - fn new(config: &ExitGatewayConfig) -> Result { + async fn new(config: &ExitGatewayConfig) -> Result { let nr_paths = &config.storage_paths.network_requester; let nr_ed25519 = load_key( &nr_paths.public_ed25519_identity_key_file, @@ -251,6 +253,13 @@ impl ExitGatewayData { "authenticator x25519", )?; + let client_storage = nym_gateway::node::PersistentStorage::init( + &config.storage_paths.clients_storage, + config.debug.message_retrieval_limit, + ) + .await + .map_err(nym_gateway::GatewayError::from)?; + Ok(ExitGatewayData { nr_ed25519, nr_x25519, @@ -258,6 +267,7 @@ impl ExitGatewayData { ipr_x25519, auth_ed25519, auth_x25519, + client_storage, }) } } @@ -459,7 +469,7 @@ impl NymNode { verloc_stats: Default::default(), mixnode: MixnodeData::new(&config.mixnode)?, entry_gateway: EntryGatewayData::new(&config.entry_gateway).await?, - exit_gateway: ExitGatewayData::new(&config.exit_gateway)?, + exit_gateway: ExitGatewayData::new(&config.exit_gateway).await?, wireguard: wireguard_data, config, accepted_operator_terms_and_conditions: false, @@ -570,8 +580,9 @@ impl NymNode { ); entry_gateway.disable_http_server(); entry_gateway.set_task_client(task_client); - #[cfg(all(feature = "wireguard", target_os = "linux"))] - entry_gateway.set_wireguard_data(self.wireguard.into()); + if self.config.wireguard.enabled { + entry_gateway.set_wireguard_data(self.wireguard.into()); + } tokio::spawn(async move { if let Err(err) = entry_gateway.run().await { @@ -594,12 +605,13 @@ impl NymNode { Some(config.auth_opts), self.ed25519_identity_keys.clone(), self.x25519_sphinx_keys.clone(), - self.entry_gateway.client_storage.clone(), + self.exit_gateway.client_storage.clone(), ); exit_gateway.disable_http_server(); exit_gateway.set_task_client(task_client); - #[cfg(all(feature = "wireguard", target_os = "linux"))] - exit_gateway.set_wireguard_data(self.wireguard.into()); + if self.config.wireguard.enabled { + exit_gateway.set_wireguard_data(self.wireguard.into()); + } tokio::spawn(async move { if let Err(err) = exit_gateway.run().await { diff --git a/nym-node/src/wireguard/mod.rs b/nym-node/src/wireguard/mod.rs index 8c2b1fd10f..03d1b32e4b 100644 --- a/nym-node/src/wireguard/mod.rs +++ b/nym-node/src/wireguard/mod.rs @@ -5,4 +5,3 @@ // but let's start putting everything in here pub mod error; -pub mod types; diff --git a/nym-node/src/wireguard/types.rs b/nym-node/src/wireguard/types.rs deleted file mode 100644 index 00ddb7a663..0000000000 --- a/nym-node/src/wireguard/types.rs +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -// pub use nym_node_requests::api::v1::gateway::client_interfaces::wireguard::models::*; -// pub use nym_wireguard_types::registration::{GatewayClientRegistry, PendingRegistrations}; diff --git a/nym-outfox/tests/unittests.rs b/nym-outfox/tests/unittests.rs index 8c258c4b7f..08ffdd8a43 100644 --- a/nym-outfox/tests/unittests.rs +++ b/nym-outfox/tests/unittests.rs @@ -57,15 +57,21 @@ mod tests { ) .unwrap(); - assert!(new_buffer[mix_params.payload_range()] != buffer[mix_params.payload_range()]); - assert!(new_buffer[mix_params.routing_data_range()] != routing[..]); + assert_ne!( + new_buffer[mix_params.payload_range()], + buffer[mix_params.payload_range()] + ); + assert_ne!(new_buffer[mix_params.routing_data_range()], routing[..]); let _ = mix_params .decode_mix_layer(&mut new_buffer[..], &mix_secret) .unwrap(); - assert!(new_buffer[mix_params.payload_range()] == buffer[mix_params.payload_range()]); - assert!(new_buffer[mix_params.routing_data_range()] == routing[..]); + assert_eq!( + new_buffer[mix_params.payload_range()], + buffer[mix_params.payload_range()] + ); + assert_eq!(new_buffer[mix_params.routing_data_range()], routing[..]); } #[test] @@ -75,14 +81,14 @@ mod tests { let mut message_clone = message.clone(); lion_transform(&mut message_clone[..], &key, [1, 2, 3]).unwrap(); - assert!(message_clone[..] != message[..]); + assert_ne!(message_clone[..], message[..]); let mut message_clone_2 = message.clone(); lion_transform_encrypt(&mut message_clone_2, &key).unwrap(); assert_eq!(message_clone_2, message_clone); lion_transform(&mut message_clone[..], &key[..], [3, 2, 1]).unwrap(); - assert!(message_clone[..] == message[..]); + assert_eq!(message_clone[..], message[..]); } #[test] diff --git a/nym-validator-rewarder/Cargo.toml b/nym-validator-rewarder/Cargo.toml index 545d62b194..58a7472c43 100644 --- a/nym-validator-rewarder/Cargo.toml +++ b/nym-validator-rewarder/Cargo.toml @@ -33,7 +33,8 @@ humantime-serde.workspace = true # internal nym-bin-common = { path = "../common/bin-common", features = ["output_format", "basic_tracing"] } nym-config = { path = "../common/config" } -nym-coconut = { path = "../common/nymcoconut" } +nym-ecash-time = { path = "../common/ecash-time" } +nym-compact-ecash = { path = "../common/nym_offline_compact_ecash" } nym-crypto = { path = "../common/crypto", features = ["asymmetric"] } nym-credentials = { path = "../common/credentials" } nym-network-defaults = { path = "../common/network-defaults" } @@ -46,3 +47,8 @@ nyxd-scraper = { path = "../common/nyxd-scraper" } [build-dependencies] sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } + +[dev-dependencies] +rand_chacha = { workspace = true } +nym-credentials-interface = { path = "../common/credentials-interface" } +nym-crypto = { path = "../common/crypto", features = ["rand"] } diff --git a/nym-validator-rewarder/migrations/03_use_deposit_id.sql b/nym-validator-rewarder/migrations/03_use_deposit_id.sql new file mode 100644 index 0000000000..9c6aef5da3 --- /dev/null +++ b/nym-validator-rewarder/migrations/03_use_deposit_id.sql @@ -0,0 +1,28 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +DROP TABLE validated_deposit; +CREATE TABLE validated_deposit +( + operator_identity_bs58 TEXT NOT NULL, + credential_id INTEGER NOT NULL, + deposit_id INTEGER NOT NULL, + + signed_plaintext BLOB NOT NULL, + signature_bs58 TEXT NOT NULL +); + +-- evidence of attempting to re-use the same deposit id for multiple credentials +DROP TABLE double_signing_evidence; +CREATE TABLE double_signing_evidence +( + operator_identity_bs58 TEXT NOT NULL, + credential_id INTEGER NOT NULL, + original_credential_id INTEGER NOT NULL, + deposit_id INTEGER NOT NULL, + + signed_plaintext BLOB NOT NULL, + signature_bs58 TEXT NOT NULL +); \ No newline at end of file diff --git a/nym-validator-rewarder/src/error.rs b/nym-validator-rewarder/src/error.rs index cc7a756363..4c72c6ef37 100644 --- a/nym-validator-rewarder/src/error.rs +++ b/nym-validator-rewarder/src/error.rs @@ -2,12 +2,12 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::config::RewardingRatios; -use nym_coconut::CoconutError; +use nym_compact_ecash::error::CompactEcashError; use nym_crypto::asymmetric::ed25519; use nym_validator_client::nym_api::error::NymAPIError; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::tx::ErrorReport; -use nym_validator_client::nyxd::{AccountId, Coin, Hash}; +use nym_validator_client::nyxd::{AccountId, Coin}; use std::io; use std::path::PathBuf; use thiserror::Error; @@ -115,18 +115,17 @@ pub enum NymRewarderError { received: usize, }, - #[error("the following private attribute commitment is malformed: {raw}: {source}")] + #[error("one of the provided private attribute commitment is malformed: {source}")] MalformedCredentialCommitment { - raw: String, #[source] - source: CoconutError, + source: CompactEcashError, }, #[error("the partial verification key for runner {runner} is malformed: {source}")] MalformedPartialVerificationKey { runner: String, #[source] - source: CoconutError, + source: CompactEcashError, }, #[error("the signature on issued credential with id {credential_id} is invalid")] @@ -135,29 +134,26 @@ pub enum NymRewarderError { #[error("could not verify the blinded credential")] BlindVerificationFailure, - #[error("the same deposit transaction ({tx_hash}) has been used for multiple issued credentials! {first} and {other}")] - DuplicateDepositHash { - tx_hash: Hash, + #[error("the same deposit ({deposit_id}) has been used for multiple issued credentials! {first} and {other}")] + DuplicateDepositId { + deposit_id: u32, first: i64, other: i64, }, - #[error("could not find the deposit value in the event of transaction {tx_hash}")] - DepositValueNotFound { tx_hash: Hash }, + #[error("could not find the deposit details for deposit id {deposit_id}")] + DepositNotFound { deposit_id: u32 }, - #[error("could not find the deposit info in the event of transaction {tx_hash}")] - DepositInfoNotFound { tx_hash: Hash }, - - #[error("the provided deposit value of transaction {tx_hash} is inconsistent. got '{request:?}' while the value on chain is '{on_chain}'")] + #[error("the provided deposit value of deposit {deposit_id} is inconsistent. got '{request:?}' while the value on chain is '{on_chain}'")] InconsistentDepositValue { - tx_hash: Hash, + deposit_id: u32, request: Option, on_chain: String, }, - #[error("the provided deposit info of transaction {tx_hash} is inconsistent. got '{request:?}' while the value on chain is '{on_chain}'")] + #[error("the provided deposit info of deposit {deposit_id} is inconsistent. got '{request:?}' while the value on chain is '{on_chain}'")] InconsistentDepositInfo { - tx_hash: Hash, + deposit_id: u32, request: Option, on_chain: String, }, diff --git a/nym-validator-rewarder/src/rewarder/credential_issuance/monitor.rs b/nym-validator-rewarder/src/rewarder/credential_issuance/monitor.rs index 4df0395ec4..b4a2befa02 100644 --- a/nym-validator-rewarder/src/rewarder/credential_issuance/monitor.rs +++ b/nym-validator-rewarder/src/rewarder/credential_issuance/monitor.rs @@ -11,14 +11,13 @@ use crate::rewarder::nyxd_client::NyxdClient; use crate::rewarder::storage::RewarderStorage; use bip39::rand::prelude::SliceRandom; use bip39::rand::thread_rng; -use nym_coconut::{ - hash_to_scalar, verify_partial_blind_signature, Base58, G1Projective, VerificationKey, -}; use nym_coconut_dkg_common::types::EpochId; -use nym_credentials::coconut::bandwidth::bandwidth_credential_params; +use nym_compact_ecash::scheme::withdrawal::verify_partial_blind_signature; +use nym_compact_ecash::{date_scalar, type_scalar, Bytable, G1Projective, VerificationKeyAuth}; +use nym_credentials::ecash::utils::EcashTime; use nym_crypto::asymmetric::ed25519; use nym_task::TaskClient; -use nym_validator_client::nym_api::{IssuedCredential, IssuedCredentialBody, NymApiClientExt}; +use nym_validator_client::nym_api::{IssuedTicketbook, IssuedTicketbookBody, NymApiClientExt}; use std::cmp::max; use tokio::time::interval; use tracing::{debug, error, info, instrument, trace}; @@ -47,7 +46,7 @@ impl CredentialIssuanceMonitor { fn validate_credential_signature( &mut self, - issued_credential: &IssuedCredentialBody, + issued_credential: &IssuedTicketbookBody, identity_key: &ed25519::PublicKey, ) -> Result<(), NymRewarderError> { let plaintext = issued_credential.credential.signable_plaintext(); @@ -66,13 +65,13 @@ impl CredentialIssuanceMonitor { async fn check_deposit_reuse( &mut self, issuer_identity: &str, - credential_info: &IssuedCredentialBody, + credential_info: &IssuedTicketbookBody, ) -> Result { let credential_id = credential_info.credential.id; - let deposit_tx = credential_info.credential.tx_hash; + let deposit_id = credential_info.credential.deposit_id; let prior_id = self .storage - .get_deposit_credential_id(issuer_identity.to_string(), deposit_tx.to_string()) + .get_deposit_credential_id(issuer_identity.to_string(), deposit_id) .await?; match prior_id { @@ -82,7 +81,7 @@ impl CredentialIssuanceMonitor { debug!("we have already verified this credential before"); Ok(true) } else { - error!("double signing detected!! used deposit {deposit_tx} for credentials {prior} and {credential_id}!!"); + error!("double signing detected!! used deposit {deposit_id} for credentials {prior} and {credential_id}!!"); self.storage .insert_double_signing_evidence( issuer_identity.to_string(), @@ -90,8 +89,8 @@ impl CredentialIssuanceMonitor { credential_info, ) .await?; - Err(NymRewarderError::DuplicateDepositHash { - tx_hash: deposit_tx, + Err(NymRewarderError::DuplicateDepositId { + deposit_id, first: prior, other: credential_id, }) @@ -102,90 +101,31 @@ impl CredentialIssuanceMonitor { async fn validate_deposit( &mut self, - issued_credential: &IssuedCredentialBody, + issued_credential: &IssuedTicketbookBody, ) -> Result<(), NymRewarderError> { // check if this deposit even exists - let deposit_tx = issued_credential.credential.tx_hash; + let deposit_id = issued_credential.credential.deposit_id; - let (deposit_value, deposit_info) = self - .nyxd_client - .get_deposit_transaction_attributes(deposit_tx) - .await?; + //not using value anymore, but it should still be there + let _ = self.nyxd_client.get_deposit_details(deposit_id).await?; trace!("deposit exists"); - // check if the deposit values match - let credential_value = issued_credential.credential.public_attributes.first(); - let credential_info = issued_credential.credential.public_attributes.get(1); - - if credential_value != Some(&deposit_value) { - return Err(NymRewarderError::InconsistentDepositValue { - tx_hash: deposit_tx, - request: credential_value.cloned(), - on_chain: deposit_value, - }); - } - trace!("credential values matches the deposit"); - - if credential_info != Some(&deposit_info) { - return Err(NymRewarderError::InconsistentDepositInfo { - tx_hash: deposit_tx, - request: credential_info.cloned(), - on_chain: deposit_info, - }); - } - trace!("credential info matches the deposit"); Ok(()) } fn verify_credential( - &mut self, - vk: &VerificationKey, - credential: &IssuedCredential, + &self, + vk: &VerificationKeyAuth, + credential: &IssuedTicketbook, ) -> Result<(), NymRewarderError> { - let public_attributes = credential - .public_attributes - .iter() - .map(hash_to_scalar) - .collect::>(); - - #[allow(clippy::map_identity)] - let attributes_refs = public_attributes.iter().collect::>(); - - let mut public_attribute_commitments = - Vec::with_capacity(credential.bs58_encoded_private_attributes_commitments.len()); - - for raw_cm in &credential.bs58_encoded_private_attributes_commitments { - match G1Projective::try_from_bs58(raw_cm) { - Ok(cm) => public_attribute_commitments.push(cm), - Err(source) => { - return Err(NymRewarderError::MalformedCredentialCommitment { - raw: raw_cm.clone(), - source, - }) - } - } - } - - // actually do verify the credential now - if !verify_partial_blind_signature( - bandwidth_credential_params(), - &public_attribute_commitments, - &attributes_refs, - &credential.blinded_partial_credential, - vk, - ) { - return Err(NymRewarderError::BlindVerificationFailure); - } - trace!("credential correctly verifies"); - - Ok(()) + verify_credential(vk, credential) } - #[instrument(skip_all, fields(credential_id = %issued_credential.credential.id, deposit = %issued_credential.credential.tx_hash))] + #[instrument(skip_all, fields(credential_id = %issued_credential.credential.id, deposit_id = %issued_credential.credential.deposit_id))] async fn validate_issued_credential( &mut self, issuer: &CredentialIssuer, - issued_credential: &IssuedCredentialBody, + issued_credential: &IssuedTicketbookBody, ) -> Result<(), NymRewarderError> { // check if the issuer has actually signed that issued credential information self.validate_credential_signature(issued_credential, &issuer.public_key)?; @@ -349,3 +289,92 @@ impl CredentialIssuanceMonitor { } } } + +fn verify_credential( + vk: &VerificationKeyAuth, + credential: &IssuedTicketbook, +) -> Result<(), NymRewarderError> { + let public_attributes = [ + date_scalar(credential.expiration_date.ecash_unix_timestamp()), + type_scalar(credential.ticketbook_type.encode()), + ]; + + #[allow(clippy::map_identity)] + let attributes_refs = public_attributes.iter().collect::>(); + + let mut public_attribute_commitments = + Vec::with_capacity(credential.encoded_private_attributes_commitments.len()); + + for raw_cm in &credential.encoded_private_attributes_commitments { + match G1Projective::try_from_byte_slice(raw_cm) { + Ok(cm) => public_attribute_commitments.push(cm), + Err(source) => return Err(NymRewarderError::MalformedCredentialCommitment { source }), + } + } + + // actually do verify the credential now + if !verify_partial_blind_signature( + &public_attribute_commitments, + &attributes_refs, + &credential.blinded_partial_credential, + vk, + ) { + return Err(NymRewarderError::BlindVerificationFailure); + } + trace!("credential correctly verifies"); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use nym_compact_ecash::ttp_keygen; + use nym_credentials::IssuanceTicketBook; + use nym_crypto::asymmetric::{ed25519, identity}; + use rand_chacha::rand_core::{RngCore, SeedableRng}; + + #[test] + fn verify_issued_partial_credential() -> anyhow::Result<()> { + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let ecash_keypair = ttp_keygen(1, 1)?.pop().unwrap(); + + let deposit_id = rng.next_u32(); + + // create dummy ticketbook + let identity_keypair = ed25519::KeyPair::new(&mut rng); + let id_priv = + identity::PrivateKey::from_bytes(&identity_keypair.private_key().to_bytes()).unwrap(); + let identifier = [44u8; 32]; + + let issuance = IssuanceTicketBook::new(deposit_id, identifier, id_priv, Default::default()); + let signing_data = issuance.prepare_for_signing(); + let request = issuance.create_blind_sign_request_body(&signing_data); + + let partial = nym_compact_ecash::scheme::withdrawal::issue( + ecash_keypair.secret_key(), + request.ecash_pubkey.clone(), + &request.inner_sign_request, + request.expiration_date.ecash_unix_timestamp(), + request.ticketbook_type.encode(), + )?; + + let commitments = request.encode_commitments(); + + let issued = IssuedTicketbook { + id: 0, + epoch_id: 0, + deposit_id, + blinded_partial_credential: partial, + encoded_private_attributes_commitments: commitments, + expiration_date: issuance.expiration_date(), + ticketbook_type: issuance.ticketbook_type(), + }; + + assert!(verify_credential(ecash_keypair.verification_key_ref(), &issued).is_ok()); + + Ok(()) + } +} diff --git a/nym-validator-rewarder/src/rewarder/credential_issuance/types.rs b/nym-validator-rewarder/src/rewarder/credential_issuance/types.rs index ce8b65b659..dd39df1cd5 100644 --- a/nym-validator-rewarder/src/rewarder/credential_issuance/types.rs +++ b/nym-validator-rewarder/src/rewarder/credential_issuance/types.rs @@ -6,7 +6,7 @@ use crate::rewarder::epoch::Epoch; use crate::rewarder::helpers::api_client; use crate::rewarder::nyxd_client::NyxdClient; use cosmwasm_std::{Addr, Decimal, Uint128}; -use nym_coconut::VerificationKey; +use nym_compact_ecash::VerificationKeyAuth; use nym_crypto::asymmetric::ed25519; use nym_validator_client::nym_api::NymApiClientExt; use nym_validator_client::nyxd::{AccountId, Coin}; @@ -341,7 +341,7 @@ pub struct CredentialIssuer { pub public_key: ed25519::PublicKey, pub operator_account: AccountId, pub api_runner: String, - pub verification_key: VerificationKey, + pub verification_key: VerificationKeyAuth, } // safety: we're converting between different wrappers for bech32 addresses diff --git a/nym-validator-rewarder/src/rewarder/nyxd_client.rs b/nym-validator-rewarder/src/rewarder/nyxd_client.rs index 540245b5bc..a75aaa0929 100644 --- a/nym-validator-rewarder/src/rewarder/nyxd_client.rs +++ b/nym-validator-rewarder/src/rewarder/nyxd_client.rs @@ -1,18 +1,17 @@ -// Copyright 2023 - Nym Technologies SA +// Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only use crate::config::Config; use crate::error::NymRewarderError; use crate::rewarder::credential_issuance::types::{addr_to_account_id, CredentialIssuer}; -use nym_coconut::{Base58, VerificationKey}; -use nym_coconut_bandwidth_contract_common::events::{ - COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_INFO, DEPOSIT_VALUE, -}; use nym_coconut_dkg_common::types::Epoch; +use nym_compact_ecash::{Base58, VerificationKeyAuth}; use nym_crypto::asymmetric::ed25519; use nym_network_defaults::NymNetworkDetails; -use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient}; -use nym_validator_client::nyxd::helpers::find_tx_attribute; +use nym_validator_client::nyxd::contract_traits::ecash_query_client::{Deposit, DepositId}; +use nym_validator_client::nyxd::contract_traits::{ + DkgQueryClient, EcashQueryClient, PagedDkgQueryClient, +}; use nym_validator_client::nyxd::module_traits::staking::{ QueryHistoricalInfoResponse, QueryValidatorsResponse, }; @@ -110,7 +109,7 @@ impl NyxdClient { public_key: ed25519::PublicKey::from_base58_string(&info.ed25519_identity)?, operator_account: addr_to_account_id(share.owner), api_runner: share.announce_address, - verification_key: VerificationKey::try_from_bs58(share.share).map_err( + verification_key: VerificationKeyAuth::try_from_bs58(share.share).map_err( |source| NymRewarderError::MalformedPartialVerificationKey { runner: info.address.to_string(), source, @@ -123,22 +122,12 @@ impl NyxdClient { Ok(issuers) } - pub(crate) async fn get_deposit_transaction_attributes( + pub(crate) async fn get_deposit_details( &self, - tx_hash: Hash, - ) -> Result<(String, String), NymRewarderError> { - let tx = self.inner.read().await.get_tx(tx_hash).await?; - - // todo: we need to make it more concrete that the first attribute is the deposit value - // and the second one is the deposit info - let deposit_value = - find_tx_attribute(&tx, COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_VALUE) - .ok_or(NymRewarderError::DepositValueNotFound { tx_hash })?; - - let deposit_info = - find_tx_attribute(&tx, COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_INFO) - .ok_or(NymRewarderError::DepositInfoNotFound { tx_hash })?; - - Ok((deposit_value, deposit_info)) + deposit_id: DepositId, + ) -> Result { + let res = self.inner.read().await.get_deposit(deposit_id).await?; + res.deposit + .ok_or(NymRewarderError::DepositNotFound { deposit_id }) } } diff --git a/nym-validator-rewarder/src/rewarder/storage/manager.rs b/nym-validator-rewarder/src/rewarder/storage/manager.rs index 453668c3ef..3efa3d9308 100644 --- a/nym-validator-rewarder/src/rewarder/storage/manager.rs +++ b/nym-validator-rewarder/src/rewarder/storage/manager.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::rewarder::epoch::Epoch; +use nym_validator_client::nyxd::contract_traits::ecash_query_client::DepositId; #[derive(Clone)] pub(crate) struct StorageManager { @@ -186,7 +187,7 @@ impl StorageManager { &self, operator_identity_bs58: String, credential_id: i64, - deposit_tx: String, + deposit_id: DepositId, signed_plaintext: Vec, signature_bs58: String, ) -> Result<(), sqlx::Error> { @@ -195,14 +196,14 @@ impl StorageManager { INSERT INTO validated_deposit ( operator_identity_bs58, credential_id, - deposit_tx, + deposit_id, signed_plaintext, signature_bs58 ) VALUES (?, ?, ?, ?, ?) "#, operator_identity_bs58, credential_id, - deposit_tx, + deposit_id, signed_plaintext, signature_bs58 ) @@ -214,16 +215,16 @@ impl StorageManager { pub(crate) async fn get_deposit_credential_id( &self, operator_identity_bs58: String, - deposit_tx: String, + deposit_id: DepositId, ) -> Result, sqlx::Error> { Ok(sqlx::query!( r#" SELECT credential_id FROM validated_deposit - WHERE operator_identity_bs58 = ? AND deposit_tx = ? + WHERE operator_identity_bs58 = ? AND deposit_id = ? "#, operator_identity_bs58, - deposit_tx + deposit_id ) .fetch_optional(&self.connection_pool) .await? @@ -235,7 +236,7 @@ impl StorageManager { operator_identity_bs58: String, credential_id: i64, original_credential_id: i64, - deposit_tx: String, + deposit_id: DepositId, signed_plaintext: Vec, signature_bs58: String, ) -> Result<(), sqlx::Error> { @@ -245,7 +246,7 @@ impl StorageManager { operator_identity_bs58, credential_id, original_credential_id, - deposit_tx, + deposit_id, signed_plaintext, signature_bs58 ) VALUES (?, ?, ?, ?, ?, ?) @@ -253,7 +254,7 @@ impl StorageManager { operator_identity_bs58, credential_id, original_credential_id, - deposit_tx, + deposit_id, signed_plaintext, signature_bs58 ) diff --git a/nym-validator-rewarder/src/rewarder/storage/mod.rs b/nym-validator-rewarder/src/rewarder/storage/mod.rs index c0463700d0..b002ab46e5 100644 --- a/nym-validator-rewarder/src/rewarder/storage/mod.rs +++ b/nym-validator-rewarder/src/rewarder/storage/mod.rs @@ -6,7 +6,8 @@ use crate::rewarder::credential_issuance::types::CredentialIssuer; use crate::rewarder::epoch::Epoch; use crate::rewarder::storage::manager::StorageManager; use crate::rewarder::{EpochRewards, RewardingResult}; -use nym_validator_client::nym_api::IssuedCredentialBody; +use nym_validator_client::nym_api::IssuedTicketbookBody; +use nym_validator_client::nyxd::contract_traits::ecash_query_client::DepositId; use nym_validator_client::nyxd::Coin; use sqlx::ConnectOptions; use std::fmt::Debug; @@ -83,24 +84,24 @@ impl RewarderStorage { pub(crate) async fn get_deposit_credential_id( &self, operator_identity_bs58: String, - deposit_tx: String, + deposit_id: DepositId, ) -> Result, NymRewarderError> { Ok(self .manager - .get_deposit_credential_id(operator_identity_bs58, deposit_tx) + .get_deposit_credential_id(operator_identity_bs58, deposit_id) .await?) } pub(crate) async fn insert_validated_deposit( &self, operator_identity_bs58: String, - credential_info: &IssuedCredentialBody, + credential_info: &IssuedTicketbookBody, ) -> Result<(), NymRewarderError> { self.manager .insert_validated_deposit( operator_identity_bs58, credential_info.credential.id, - credential_info.credential.tx_hash.to_string(), + credential_info.credential.deposit_id, credential_info.credential.signable_plaintext(), credential_info.signature.to_base58_string(), ) @@ -112,14 +113,14 @@ impl RewarderStorage { &self, operator_identity_bs58: String, original_credential_id: i64, - credential_info: &IssuedCredentialBody, + credential_info: &IssuedTicketbookBody, ) -> Result<(), NymRewarderError> { self.manager .insert_double_signing_evidence( operator_identity_bs58, credential_info.credential.id, original_credential_id, - credential_info.credential.tx_hash.to_string(), + credential_info.credential.deposit_id, credential_info.credential.signable_plaintext(), credential_info.signature.to_base58_string(), ) @@ -130,7 +131,7 @@ impl RewarderStorage { pub(crate) async fn insert_issuance_foul_play_evidence( &self, issuer: &CredentialIssuer, - credential_info: &IssuedCredentialBody, + credential_info: &IssuedTicketbookBody, error_message: String, ) -> Result<(), NymRewarderError> { self.manager diff --git a/nym-wallet/.storybook/mocks/tauri/index.js b/nym-wallet/.storybook/mocks/tauri/index.js index e2bf50b00d..3a40c8158f 100644 --- a/nym-wallet/.storybook/mocks/tauri/index.js +++ b/nym-wallet/.storybook/mocks/tauri/index.js @@ -1,3 +1,55 @@ +const delegations = [ + { + mix_id: 1234, + node_identity: 'FiojKW7oY9WQmLCiYAsCA21tpowZHS6zcUoyYm319p6Z', + delegated_on_iso_datetime: new Date(2021, 1, 1).toDateString(), + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, + amount: { amount: '10', denom: 'nym' }, + owner: '', + block_height: BigInt(100), + cost_params: { + profit_margin_percent: '0.04', + interval_operating_cost: { + amount: '20', + denom: 'nym', + }, + }, + stake_saturation: '0.2', + avg_uptime_percent: 0.5, + accumulated_by_delegates: { amount: '0', denom: 'nym' }, + accumulated_by_operator: { amount: '0', denom: 'nym' }, + uses_vesting_contract_tokens: false, + pending_events: [], + mixnode_is_unbonding: false, + errors: null, + }, + { + mix_id: 5678, + node_identity: 'DT8S942S8AQs2zKHS9SVo1GyHmuca3pfL2uLhLksJ3D8', + unclaimed_rewards: { amount: '0.1', denom: 'nym' }, + amount: { amount: '100', denom: 'nym' }, + delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), + owner: '', + block_height: BigInt(4000), + stake_saturation: '0.5', + avg_uptime_percent: 0.1, + cost_params: { + profit_margin_percent: '0.04', + interval_operating_cost: { + amount: '60', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '0', denom: 'nym' }, + accumulated_by_operator: { amount: '0', denom: 'nym' }, + uses_vesting_contract_tokens: true, + pending_events: [], + mixnode_is_unbonding: false, + errors: null, + }, +]; + + /** * This is a mock for Tauri's API package (@tauri-apps/api), to prevent stories from being excluded, because they either use * or import dependencies that use Tauri. @@ -29,6 +81,26 @@ module.exports = { }, }; } + case 'get_delegation_summary': { + return { + delegations, + total_delegations: { + amount: '1000', + denom: 'nymt', + }, + total_rewards: { + amount: '42', + denom: 'nymt', + }, + }; + } + case 'get_pending_delegation_events' : { + return []; + } + case 'migrate_vested_delegations': { + delegations[1].uses_vesting_contract_tokens = false; + return {}; + } } console.error( diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 061b49cc26..382b526b9f 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -284,6 +284,15 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bip32" version = "0.5.1" @@ -332,18 +341,6 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" -[[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" @@ -392,13 +389,15 @@ dependencies = [ [[package]] name = "bls12_381" version = "0.8.0" -source = "git+https://github.com/jstuczyn/bls12_381?branch=feature/gt-serialization-0.8.0#c4543fde7d02efea6ecfcf22e14476ddb516b483" +source = "git+https://github.com/jstuczyn/bls12_381?branch=temp/experimental-serdect#22cd0a16b674af1629110a2dc8b6cf6c73ea4cd9" dependencies = [ "digest 0.9.0", "ff", "group", "pairing", "rand_core 0.6.4", + "serde", + "serdect 0.3.0-pre.0", "subtle 2.5.0", "zeroize", ] @@ -656,25 +655,6 @@ dependencies = [ "strsim", ] -[[package]] -name = "clap_complete" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc443334c81a804575546c5a8a79b4913b50e28d69232903604cada1de817ce" -dependencies = [ - "clap", -] - -[[package]] -name = "clap_complete_fig" -version = "4.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99fee1d30a51305a6c2ed3fc5709be3c8af626c9c958e04dd9ae94e27bcbce9f" -dependencies = [ - "clap", - "clap_complete", -] - [[package]] name = "clap_derive" version = "4.4.7" @@ -834,17 +814,17 @@ checksum = "32560304ab4c365791fd307282f76637213d8083c1a98490c35159cd67852237" dependencies = [ "prost", "prost-types", - "tendermint-proto", + "tendermint-proto 0.34.0", ] [[package]] name = "cosmos-sdk-proto" -version = "0.20.0" -source = "git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features#67718aa27a96efb9881e927a8f998c94b885093c" +version = "0.22.0-pre" +source = "git+https://github.com/cosmos/cosmos-rust?rev=4b1332e6d8258ac845cef71589c8d362a669675a#4b1332e6d8258ac845cef71589c8d362a669675a" dependencies = [ "prost", "prost-types", - "tendermint-proto", + "tendermint-proto 0.37.0", ] [[package]] @@ -854,7 +834,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47126f5364df9387b9d8559dcef62e99010e1d4098f39eb3f7ee4b5c254e40ea" dependencies = [ "bip32", - "cosmos-sdk-proto 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cosmos-sdk-proto 0.20.0", "ecdsa", "eyre", "k256", @@ -863,17 +843,17 @@ dependencies = [ "serde_json", "signature", "subtle-encoding", - "tendermint", + "tendermint 0.34.0", "thiserror", ] [[package]] name = "cosmrs" -version = "0.15.0" -source = "git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features#67718aa27a96efb9881e927a8f998c94b885093c" +version = "0.17.0-pre" +source = "git+https://github.com/cosmos/cosmos-rust?rev=4b1332e6d8258ac845cef71589c8d362a669675a#4b1332e6d8258ac845cef71589c8d362a669675a" dependencies = [ "bip32", - "cosmos-sdk-proto 0.20.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmos-sdk-proto 0.22.0-pre", "ecdsa", "eyre", "k256", @@ -882,7 +862,7 @@ dependencies = [ "serde_json", "signature", "subtle-encoding", - "tendermint", + "tendermint 0.37.0", "tendermint-rpc", "thiserror", ] @@ -1455,7 +1435,7 @@ dependencies = [ "digest 0.10.7", "elliptic-curve", "rfc6979", - "serdect", + "serdect 0.2.0", "signature", "spki", ] @@ -1535,7 +1515,7 @@ dependencies = [ "pkcs8", "rand_core 0.6.4", "sec1", - "serdect", + "serdect 0.2.0", "subtle 2.5.0", "zeroize", ] @@ -1724,12 +1704,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" -[[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" @@ -2259,9 +2233,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -2639,7 +2613,7 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ - "hermit-abi 0.3.2", + "hermit-abi 0.3.9", "rustix", "windows-sys 0.48.0", ] @@ -2671,6 +2645,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.8" @@ -2940,13 +2923,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.10" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" dependencies = [ + "hermit-abi 0.3.9", "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -3065,16 +3049,6 @@ dependencies = [ "libm", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi 0.3.2", - "libc", -] - [[package]] name = "num_enum" version = "0.5.11" @@ -3110,17 +3084,22 @@ name = "nym-api-requests" version = "0.1.0" dependencies = [ "bs58 0.5.1", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "cosmwasm-std", "ecdsa", "getset", + "nym-compact-ecash", "nym-credentials-interface", "nym-crypto", + "nym-ecash-time", "nym-mixnet-contract-common", "nym-node-requests", + "nym-serde-helpers", "schemars", "serde", - "tendermint", + "sha2 0.10.8", + "tendermint 0.37.0", + "thiserror", "time", ] @@ -3128,9 +3107,6 @@ dependencies = [ name = "nym-bin-common" version = "0.6.0" dependencies = [ - "clap", - "clap_complete", - "clap_complete_fig", "const-str", "log", "pretty_env_logger", @@ -3140,26 +3116,6 @@ dependencies = [ "vergen", ] -[[package]] -name = "nym-coconut" -version = "0.5.0" -dependencies = [ - "bls12_381", - "bs58 0.5.1", - "digest 0.9.0", - "ff", - "getrandom 0.2.10", - "group", - "itertools 0.12.1", - "nym-dkg", - "nym-pemstore", - "rand 0.8.5", - "serde", - "serde_derive", - "sha2 0.9.9", - "thiserror", -] - [[package]] name = "nym-coconut-bandwidth-contract-common" version = "0.1.0" @@ -3182,6 +3138,27 @@ dependencies = [ "nym-multisig-contract-common", ] +[[package]] +name = "nym-compact-ecash" +version = "0.1.0" +dependencies = [ + "bincode", + "bls12_381", + "bs58 0.5.1", + "cfg-if", + "digest 0.9.0", + "ff", + "group", + "itertools 0.12.1", + "nym-network-defaults", + "nym-pemstore", + "rand 0.8.5", + "serde", + "sha2 0.9.9", + "thiserror", + "zeroize", +] + [[package]] name = "nym-config" version = "0.1.0" @@ -3214,9 +3191,14 @@ name = "nym-credentials-interface" version = "0.1.0" dependencies = [ "bls12_381", - "nym-coconut", + "nym-compact-ecash", + "nym-ecash-time", + "nym-network-defaults", + "rand 0.8.5", "serde", + "strum 0.25.0", "thiserror", + "time", ] [[package]] @@ -3237,24 +3219,23 @@ dependencies = [ ] [[package]] -name = "nym-dkg" +name = "nym-ecash-contract-common" version = "0.1.0" dependencies = [ - "bitvec", - "bls12_381", "bs58 0.5.1", - "ff", - "group", - "lazy_static", - "nym-pemstore", - "rand 0.8.5", - "rand_chacha 0.3.1", - "rand_core 0.6.4", - "serde", - "serde_derive", - "sha2 0.9.9", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-utils", + "nym-multisig-contract-common", "thiserror", - "zeroize", +] + +[[package]] +name = "nym-ecash-time" +version = "0.1.0" +dependencies = [ + "time", ] [[package]] @@ -3283,6 +3264,7 @@ version = "0.1.0" dependencies = [ "async-trait", "http 1.1.0", + "nym-bin-common", "reqwest 0.12.4", "serde", "serde_json", @@ -3299,6 +3281,7 @@ dependencies = [ "bs58 0.4.0", "cosmwasm-schema", "cosmwasm-std", + "cw-controllers", "humantime-serde", "log", "nym-contracts-common", @@ -3330,14 +3313,10 @@ dependencies = [ name = "nym-network-defaults" version = "0.1.0" dependencies = [ - "cfg-if", "dotenvy", - "hex-literal", "log", - "once_cell", "schemars", "serde", - "thiserror", "url", ] @@ -3367,6 +3346,15 @@ dependencies = [ "pem", ] +[[package]] +name = "nym-serde-helpers" +version = "0.1.0" +dependencies = [ + "base64 0.21.4", + "bs58 0.5.1", + "serde", +] + [[package]] name = "nym-sphinx-types" version = "0.2.0" @@ -3395,11 +3383,11 @@ name = "nym-types" version = "1.0.0" dependencies = [ "base64 0.21.4", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "cosmwasm-std", "eyre", "hmac", - "itertools 0.12.1", + "itertools 0.13.0", "log", "nym-config", "nym-crypto", @@ -3427,7 +3415,7 @@ dependencies = [ "bip32", "bip39", "colored 2.0.4", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "cosmwasm-std", "cw-controllers", "cw-utils", @@ -3437,14 +3425,15 @@ dependencies = [ "eyre", "flate2", "futures", - "itertools 0.12.1", + "itertools 0.13.0", "log", "nym-api-requests", - "nym-coconut", "nym-coconut-bandwidth-contract-common", "nym-coconut-dkg-common", + "nym-compact-ecash", "nym-config", "nym-contracts-common", + "nym-ecash-contract-common", "nym-group-contract-common", "nym-http-api-client", "nym-mixnet-contract-common", @@ -3458,6 +3447,7 @@ dependencies = [ "sha2 0.9.9", "tendermint-rpc", "thiserror", + "time", "tokio", "url", "wasmtimer", @@ -3495,7 +3485,7 @@ dependencies = [ name = "nym-wallet-types" version = "1.0.0" dependencies = [ - "cosmrs 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cosmrs 0.15.0", "cosmwasm-std", "hex-literal", "nym-config", @@ -3534,7 +3524,7 @@ dependencies = [ "bip39", "cfg-if", "colored 2.0.4", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "cosmwasm-std", "dirs 4.0.0", "dotenvy", @@ -3762,7 +3752,7 @@ dependencies = [ "libc", "redox_syscall 0.3.5", "smallvec", - "windows-targets", + "windows-targets 0.48.1", ] [[package]] @@ -3790,9 +3780,9 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" [[package]] name = "peg" -version = "0.7.0" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c0b841ea54f523f7aa556956fbd293bcbe06f2e67d2eb732b7278aaf1d166a" +checksum = "295283b02df346d1ef66052a757869b2876ac29a6bb0ac3f5f7cd44aebe40e8f" dependencies = [ "peg-macros", "peg-runtime", @@ -3800,9 +3790,9 @@ dependencies = [ [[package]] name = "peg-macros" -version = "0.7.0" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aa52829b8decbef693af90202711348ab001456803ba2a98eb4ec8fb70844c" +checksum = "bdad6a1d9cf116a059582ce415d5f5566aabcd4008646779dab7fdc2a9a9d426" dependencies = [ "peg-runtime", "proc-macro2", @@ -3811,9 +3801,9 @@ dependencies = [ [[package]] name = "peg-runtime" -version = "0.7.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c719dcf55f09a3a7e764c6649ab594c18a177e3599c467983cdf644bfc0a4088" +checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a" [[package]] name = "pem" @@ -4102,7 +4092,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit", + "toml_edit 0.19.14", ] [[package]] @@ -4206,12 +4196,6 @@ 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.7.3" @@ -4731,7 +4715,7 @@ dependencies = [ "der", "generic-array 0.14.7", "pkcs8", - "serdect", + "serdect 0.2.0", "subtle 2.5.0", "zeroize", ] @@ -4930,6 +4914,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serdect" +version = "0.3.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791ef964bfaba6be28a5c3f0c56836e17cb711ac009ca1074b9c735a3ebf240a" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "serialize-to-javascript" version = "0.1.1" @@ -5363,7 +5357,7 @@ dependencies = [ "scopeguard", "serde", "unicode-segmentation", - "uuid 1.4.1", + "uuid 1.10.0", "windows 0.39.0", "windows-implement", "x11-dl", @@ -5438,7 +5432,7 @@ dependencies = [ "time", "tokio", "url", - "uuid 1.4.1", + "uuid 1.10.0", "webkit2gtk", "webview2-com", "windows 0.39.0", @@ -5483,7 +5477,7 @@ dependencies = [ "tauri-utils", "thiserror", "time", - "uuid 1.4.1", + "uuid 1.10.0", "walkdir", ] @@ -5516,7 +5510,7 @@ dependencies = [ "serde_json", "tauri-utils", "thiserror", - "uuid 1.4.1", + "uuid 1.10.0", "webview2-com", "windows 0.39.0", ] @@ -5534,7 +5528,7 @@ dependencies = [ "raw-window-handle", "tauri-runtime", "tauri-utils", - "uuid 1.4.1", + "uuid 1.10.0", "webkit2gtk", "webview2-com", "windows 0.39.0", @@ -5608,22 +5602,53 @@ dependencies = [ "signature", "subtle 2.5.0", "subtle-encoding", - "tendermint-proto", + "tendermint-proto 0.34.0", + "time", + "zeroize", +] + +[[package]] +name = "tendermint" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "954496fbc9716eb4446cdd6d00c071a3e2f22578d62aa03b40c7e5b4fda3ed42" +dependencies = [ + "bytes", + "digest 0.10.7", + "ed25519", + "ed25519-consensus", + "flex-error", + "futures", + "k256", + "num-traits", + "once_cell", + "prost", + "prost-types", + "ripemd", + "serde", + "serde_bytes", + "serde_json", + "serde_repr", + "sha2 0.10.8", + "signature", + "subtle 2.5.0", + "subtle-encoding", + "tendermint-proto 0.37.0", "time", "zeroize", ] [[package]] name = "tendermint-config" -version = "0.34.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a25dbe8b953e80f3d61789fbdb83bf9ad6c0ef16df5ca6546f49912542cc137" +checksum = "f84b11b57d20ee4492a1452faff85f5c520adc36ca9fe5e701066935255bb89f" dependencies = [ "flex-error", "serde", "serde_json", - "tendermint", - "toml 0.5.11", + "tendermint 0.37.0", + "toml 0.8.2", "url", ] @@ -5646,10 +5671,26 @@ dependencies = [ ] [[package]] -name = "tendermint-rpc" -version = "0.34.0" +name = "tendermint-proto" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbf0a4753b46a190f367337e0163d0b552a2674a6bac54e74f9f2cdcde2969b" +checksum = "dc87024548c7f3da479885201e3da20ef29e85a3b13d04606b380ac4c7120d87" +dependencies = [ + "bytes", + "flex-error", + "prost", + "prost-types", + "serde", + "serde_bytes", + "subtle-encoding", + "time", +] + +[[package]] +name = "tendermint-rpc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfdc2281e271277fda184d96d874a6fe59f569b130b634289257baacfc95aa85" dependencies = [ "async-trait", "bytes", @@ -5658,6 +5699,7 @@ dependencies = [ "getrandom 0.2.10", "peg", "pin-project", + "rand 0.8.5", "reqwest 0.11.22", "semver 1.0.22", "serde", @@ -5665,15 +5707,15 @@ dependencies = [ "serde_json", "subtle 2.5.0", "subtle-encoding", - "tendermint", + "tendermint 0.37.0", "tendermint-config", - "tendermint-proto", + "tendermint-proto 0.37.0", "thiserror", "time", "tokio", "tracing", "url", - "uuid 0.8.2", + "uuid 1.10.0", "walkdir", ] @@ -5783,28 +5825,27 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.1" +version = "1.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2 0.5.5", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", @@ -5874,7 +5915,19 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.19.14", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.20.2", ] [[package]] @@ -5899,6 +5952,19 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.0.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -6115,9 +6181,9 @@ checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" [[package]] name = "uuid" -version = "1.4.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom 0.2.10", ] @@ -6446,7 +6512,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets", + "windows-targets 0.48.1", ] [[package]] @@ -6496,7 +6562,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.1", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -6514,6 +6589,22 @@ dependencies = [ "windows_x86_64_msvc 0.48.0", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows-tokens" version = "0.39.0" @@ -6532,6 +6623,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_msvc" version = "0.37.0" @@ -6556,6 +6653,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_i686_gnu" version = "0.37.0" @@ -6580,6 +6683,18 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_msvc" version = "0.37.0" @@ -6604,6 +6719,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_x86_64_gnu" version = "0.37.0" @@ -6628,6 +6749,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -6640,6 +6767,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_msvc" version = "0.37.0" @@ -6664,6 +6797,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "winnow" version = "0.5.10" @@ -6740,15 +6879,6 @@ 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.21.0" diff --git a/nym-wallet/nym-wallet-types/Cargo.toml b/nym-wallet/nym-wallet-types/Cargo.toml index f9cb2d4552..9bfd3717b9 100644 --- a/nym-wallet/nym-wallet-types/Cargo.toml +++ b/nym-wallet/nym-wallet-types/Cargo.toml @@ -12,7 +12,7 @@ serde_json = "1.0" strum = { version = "0.23", features = ["derive"] } ts-rs = "7.0.0" -cosmwasm-std = "1.3.0" +cosmwasm-std = "1.4.3" cosmrs = "=0.15.0" nym-config = { path = "../../common/config" } diff --git a/nym-wallet/nym-wallet-types/src/admin.rs b/nym-wallet/nym-wallet-types/src/admin.rs index 23fc67cecb..0aac089ee2 100644 --- a/nym-wallet/nym-wallet-types/src/admin.rs +++ b/nym-wallet/nym-wallet-types/src/admin.rs @@ -1,7 +1,11 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_mixnet_contract_common::ContractStateParams; +use cosmwasm_std::Coin; +use nym_mixnet_contract_common::{ + ContractStateParams, OperatingCostRange as ContractOperatingCostRange, + ProfitMarginRange as ContractProfitMarginRange, +}; use nym_types::currency::{DecCoin, RegisteredCoins}; use nym_types::error::TypesError; use serde::{Deserialize, Serialize}; @@ -16,6 +20,31 @@ pub struct TauriContractStateParams { minimum_mixnode_pledge: DecCoin, minimum_gateway_pledge: DecCoin, minimum_mixnode_delegation: Option, + + operating_cost: TauriOperatingCostRange, + profit_margin: TauriProfitMarginRange, +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "nym-wallet/src/types/rust/OperatingCostRange.ts") +)] +#[derive(Serialize, Deserialize, Debug)] +pub struct TauriOperatingCostRange { + minimum: DecCoin, + maximum: DecCoin, +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "nym-wallet/src/types/rust/ProfitMarginRange.ts") +)] +#[derive(Serialize, Deserialize, Debug)] +pub struct TauriProfitMarginRange { + minimum: String, + maximum: String, } impl TauriContractStateParams { @@ -23,6 +52,16 @@ impl TauriContractStateParams { state_params: ContractStateParams, reg: &RegisteredCoins, ) -> Result { + let rewarding_denom = &state_params.minimum_mixnode_pledge.denom; + let min_operating_cost_c = Coin { + denom: rewarding_denom.into(), + amount: state_params.interval_operating_cost.minimum, + }; + let max_operating_cost_c = Coin { + denom: rewarding_denom.into(), + amount: state_params.interval_operating_cost.maximum, + }; + Ok(TauriContractStateParams { minimum_mixnode_pledge: reg .attempt_convert_to_display_dec_coin(state_params.minimum_mixnode_pledge.into())?, @@ -32,6 +71,15 @@ impl TauriContractStateParams { .minimum_mixnode_delegation .map(|min_del| reg.attempt_convert_to_display_dec_coin(min_del.into())) .transpose()?, + + operating_cost: TauriOperatingCostRange { + minimum: reg.attempt_convert_to_display_dec_coin(min_operating_cost_c.into())?, + maximum: reg.attempt_convert_to_display_dec_coin(max_operating_cost_c.into())?, + }, + profit_margin: TauriProfitMarginRange { + minimum: state_params.profit_margin.minimum.to_string(), + maximum: state_params.profit_margin.maximum.to_string(), + }, }) } @@ -39,6 +87,14 @@ impl TauriContractStateParams { self, reg: &RegisteredCoins, ) -> Result { + assert_eq!( + self.operating_cost.maximum.denom, + self.operating_cost.minimum.denom + ); + + let min_operating_cost_c = reg.attempt_convert_to_base_coin(self.operating_cost.minimum)?; + let max_operating_cost_c = reg.attempt_convert_to_base_coin(self.operating_cost.maximum)?; + Ok(ContractStateParams { minimum_mixnode_delegation: self .minimum_mixnode_delegation @@ -51,6 +107,15 @@ impl TauriContractStateParams { minimum_gateway_pledge: reg .attempt_convert_to_base_coin(self.minimum_gateway_pledge)? .into(), + + profit_margin: ContractProfitMarginRange { + minimum: self.profit_margin.minimum.parse()?, + maximum: self.profit_margin.maximum.parse()?, + }, + interval_operating_cost: ContractOperatingCostRange { + minimum: min_operating_cost_c.amount.into(), + maximum: max_operating_cost_c.amount.into(), + }, }) } } diff --git a/nym-wallet/nym-wallet-types/src/network/qa.rs b/nym-wallet/nym-wallet-types/src/network/qa.rs index 46f33218e2..761d08f614 100644 --- a/nym-wallet/nym-wallet-types/src/network/qa.rs +++ b/nym-wallet/nym-wallet-types/src/network/qa.rs @@ -15,14 +15,14 @@ pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = "n1hm4y6fzgxgu688jgf7ek66px6xkrtmn3gyk8fax3eawhp68c2d5qujz296"; pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "n1jlzdxnyces4hrhqz68dqk28mrw5jgwtcfq0c2funcwrmw0dx9l9s8nnnvj"; -pub(crate) const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = - "n1w798gp0zqv3s9hjl3jlnwxtwhykga6rn93p46q2crsdqhaj3y4gs68f74j"; +pub(crate) const ECASH_CONTRACT_ADDRESS: &str = + "n13xspq62y9gq6nueqmywxcdv2yep4p6nzv98w2889k25v3nhdy2dq2rkrk7"; pub(crate) const GROUP_CONTRACT_ADDRESS: &str = - "n1sthrn5ep8ls5vzz8f9gp89khhmedahhdqd244dh9uqzk3hx2pzrsvf7zgk"; + "n13l7rwuwktklrwskc7m6lv70zws07en85uma28j7dxwsz9y5hvvhspl7a2t"; pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str = - "n1sr06m8yqg0wzqqyqvzvp5t07dj4nevx9u8qc7j4qa72qu8e3ct8qledthy"; + "n138c9pyf7f3hyx0j3t6vmsz7ultnw2wj0lu6hzndep9z5grgq9haqlc25k0"; pub(crate) const COCONUT_DKG_CONTRACT_ADDRESS: &str = - "n1udfs22xpxle475m2nz7u47jfa3vngncdegmczwwdx00cmetypa3s7uyuqn"; + "n1pk8jgr6y4c5k93gz7qf3xc0hvygmp7csk88c2tf8l39tkq6834wq2a6dtr"; // -- Constructor functions -- @@ -48,13 +48,12 @@ pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails { contracts: NymContracts { mixnet_contract_address: parse_optional_str(MIXNET_CONTRACT_ADDRESS), vesting_contract_address: parse_optional_str(VESTING_CONTRACT_ADDRESS), - coconut_bandwidth_contract_address: parse_optional_str( - COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - ), + ecash_contract_address: parse_optional_str(ECASH_CONTRACT_ADDRESS), group_contract_address: parse_optional_str(GROUP_CONTRACT_ADDRESS), multisig_contract_address: parse_optional_str(MULTISIG_CONTRACT_ADDRESS), coconut_dkg_contract_address: parse_optional_str(COCONUT_DKG_CONTRACT_ADDRESS), }, explorer_api: parse_optional_str(EXPLORER_API), + nym_vpn_api_url: None, } } diff --git a/nym-wallet/nym-wallet-types/src/network/sandbox.rs b/nym-wallet/nym-wallet-types/src/network/sandbox.rs index 41a21bdad4..c436c9f09e 100644 --- a/nym-wallet/nym-wallet-types/src/network/sandbox.rs +++ b/nym-wallet/nym-wallet-types/src/network/sandbox.rs @@ -15,14 +15,14 @@ pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = "n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav"; pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "n1unyuj8qnmygvzuex3dwmg9yzt9alhvyeat0uu0jedg2wj33efl5qackslz"; -pub(crate) const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = - "n16a32stm6kknhq5cc8rx77elr66pygf2hfszw7wvpq746x3uffylqkjar4l"; +pub(crate) const ECASH_CONTRACT_ADDRESS: &str = + "n1v3vydvs2ued84yv3khqwtgldmgwn0elljsdh08dr5s2j9x4rc5fs9jlwz9"; pub(crate) const GROUP_CONTRACT_ADDRESS: &str = - "n1pd7kfgvr5tpcv0xnlv46c4jsq9jg2r799xxrcwqdm4l2jhq2pjwqrmz5ju"; + "n1ewmwz97xm0h8rdk8sw7h9mwn866qkx9hl9zlmagqfkhuzvwk5hhq844ue9"; pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str = - "n14ph4e660eyqz0j36zlkaey4zgzexm5twkmjlqaequxr2cjm9eprqsmad6k"; + "n1tz0setr8vkh9udp8xyxgpqc89ns27k4d0jx2h942hr0ax63yjhmqz6xct8"; pub(crate) const COCONUT_DKG_CONTRACT_ADDRESS: &str = - "n1ahg0erc2fs6xx3j5m8sfx3ryuzdjh6kf6qm9plsf865fltekyrfsesac6a"; + "n1v3n2ly2dp3a9ng3ff6rh26yfkn0pc5hed7w2shc5u9ca5c865utqj5elvh"; // -- Constructor functions -- @@ -48,13 +48,12 @@ pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails { contracts: NymContracts { mixnet_contract_address: parse_optional_str(MIXNET_CONTRACT_ADDRESS), vesting_contract_address: parse_optional_str(VESTING_CONTRACT_ADDRESS), - coconut_bandwidth_contract_address: parse_optional_str( - COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - ), + ecash_contract_address: parse_optional_str(ECASH_CONTRACT_ADDRESS), group_contract_address: parse_optional_str(GROUP_CONTRACT_ADDRESS), multisig_contract_address: parse_optional_str(MULTISIG_CONTRACT_ADDRESS), coconut_dkg_contract_address: parse_optional_str(COCONUT_DKG_CONTRACT_ADDRESS), }, explorer_api: parse_optional_str(EXPLORER_API), + nym_vpn_api_url: None, } } diff --git a/nym-wallet/package.json b/nym-wallet/package.json index e9098e8c15..3d97eefd26 100644 --- a/nym-wallet/package.json +++ b/nym-wallet/package.json @@ -1,6 +1,6 @@ { "name": "@nymproject/nym-wallet-app", - "version": "1.2.13", + "version": "1.2.14", "license": "MIT", "main": "index.js", "scripts": { diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index 75a4046264..8e995a784f 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym_wallet" -version = "1.2.13" +version = "1.2.14" description = "Nym Native Wallet" authors = ["Nym Technologies SA"] license = "" @@ -8,7 +8,7 @@ repository = "" default-run = "nym_wallet" edition = "2021" build = "src/build.rs" -rust-version = "1.58" +rust-version = "1.76" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -50,7 +50,7 @@ base64 = "0.13" zeroize = { version = "1.5", features = ["zeroize_derive", "serde"] } cosmwasm-std = "1.3.0" -cosmrs = { git = "https://github.com/jstuczyn/cosmos-rust", branch = "nym-temp/all-validator-features" } +cosmrs = { git = "https://github.com/cosmos/cosmos-rust", rev = "4b1332e6d8258ac845cef71589c8d362a669675a" } nym-validator-client = { path = "../../common/client-libs/validator-client" } nym-crypto = { path = "../../common/crypto", features = ["asymmetric"] } diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index 1ed617dc4b..9d879c706e 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -134,6 +134,10 @@ pub enum BackendError { WalletValidatorConnectionFailed, #[error("No defined default validator URL")] WalletNoDefaultValidator, + #[error( + "this vesting operation has been disabled. please use the non-vesting variant instead." + )] + UnsupportedVestingOperation, #[error(transparent)] WalletError { @@ -151,6 +155,9 @@ pub enum BackendError { #[error("This command ({name}) has been removed. Please try to use {alternative} instead.")] RemovedCommand { name: String, alternative: String }, + + #[error("there aren't any vesting delegations to migrate")] + NoVestingDelegations, } impl Serialize for BackendError { diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index 148dcded2b..623ab63e45 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -129,6 +129,8 @@ fn main() { vesting::bond::withdraw_vested_coins, vesting::delegate::vesting_delegate_to_mixnode, vesting::delegate::vesting_undelegate_from_mixnode, + vesting::migrate::migrate_vested_mixnode, + vesting::migrate::migrate_vested_delegations, vesting::queries::get_account_info, vesting::queries::get_current_vesting_period, vesting::queries::locked_coins, diff --git a/nym-wallet/src-tauri/src/operations/helpers.rs b/nym-wallet/src-tauri/src/operations/helpers.rs index 9f66f29289..da0093e293 100644 --- a/nym-wallet/src-tauri/src/operations/helpers.rs +++ b/nym-wallet/src-tauri/src/operations/helpers.rs @@ -9,10 +9,10 @@ use nym_contracts_common::signing::{ }; use nym_crypto::asymmetric::identity; use nym_mixnet_contract_common::{ - construct_mixnode_bonding_sign_payload, Gateway, GatewayBondingPayload, MixNode, - MixNodeCostParams, SignableGatewayBondingMsg, SignableMixNodeBondingMsg, + construct_legacy_mixnode_bonding_sign_payload, Gateway, GatewayBondingPayload, MixNode, + MixNodeCostParams, SignableGatewayBondingMsg, SignableLegacyMixNodeBondingMsg, }; -use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, NymContractsProvider}; +use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::Coin; use nym_validator_client::DirectSigningHttpRpcValidatorClient; @@ -21,7 +21,6 @@ use nym_validator_client::DirectSigningHttpRpcValidatorClient; #[async_trait] pub(crate) trait AddressAndNonceProvider { async fn get_signing_nonce(&self) -> Result; - fn vesting_contract_address(&self) -> Addr; fn cw_address(&self) -> Addr; } @@ -31,30 +30,11 @@ impl AddressAndNonceProvider for DirectSigningHttpRpcValidatorClient { self.nyxd.get_signing_nonce(&self.nyxd.address()).await } - fn vesting_contract_address(&self) -> Addr { - // the call to unchecked is fine here as we're converting directly from `AccountId` - // which must have been a valid bech32 address - Addr::unchecked( - self.nyxd - .vesting_contract_address() - .expect("unknown vesting contract address") - .as_ref(), - ) - } - fn cw_address(&self) -> Addr { self.nyxd.cw_address() } } -fn proxy(client: &P, vesting: bool) -> Option { - if vesting { - Some(client.vesting_contract_address()) - } else { - None - } -} - // since the message has to go back to the user due to the increasing nonce, we might as well sign the entire payload pub(crate) async fn create_mixnode_bonding_sign_payload( client: &P, @@ -62,15 +42,16 @@ pub(crate) async fn create_mixnode_bonding_sign_payload Result { +) -> Result { + if vesting { + return Err(BackendError::UnsupportedVestingOperation); + } let sender = client.cw_address(); - let proxy = proxy(client, vesting); let nonce = client.get_signing_nonce().await?; - Ok(construct_mixnode_bonding_sign_payload( + Ok(construct_legacy_mixnode_bonding_sign_payload( nonce, sender, - proxy, pledge.into(), mix_node, cost_params, @@ -85,6 +66,9 @@ pub(crate) async fn verify_mixnode_bonding_sign_payload Result<(), BackendError> { + if vesting { + return Err(BackendError::UnsupportedVestingOperation); + } let identity_key = identity::PublicKey::from_base58_string(&mix_node.identity_key)?; let signature = identity::Signature::from_bytes(msg_signature.as_ref())?; @@ -118,10 +102,12 @@ pub(crate) async fn create_gateway_bonding_sign_payload Result { + if vesting { + return Err(BackendError::UnsupportedVestingOperation); + } let payload = GatewayBondingPayload::new(gateway); let sender = client.cw_address(); - let proxy = proxy(client, vesting); - let content = ContractMessageContent::new(sender, proxy, vec![pledge.into()], payload); + let content = ContractMessageContent::new(sender, vec![pledge.into()], payload); let nonce = client.get_signing_nonce().await?; Ok(SignableMessage::new(nonce, content)) @@ -134,6 +120,9 @@ pub(crate) async fn verify_gateway_bonding_sign_payload Result<(), BackendError> { + if vesting { + return Err(BackendError::UnsupportedVestingOperation); + } let identity_key = identity::PublicKey::from_base58_string(&gateway.identity_key)?; let signature = identity::Signature::from_bytes(msg_signature.as_ref())?; @@ -170,7 +159,6 @@ mod tests { struct MockClient { address: Addr, - vesting_contract: Addr, signing_nonce: Nonce, } @@ -180,10 +168,6 @@ mod tests { Ok(self.signing_nonce) } - fn vesting_contract_address(&self) -> Addr { - self.vesting_contract.clone() - } - fn cw_address(&self) -> Addr { self.address.clone() } @@ -211,7 +195,6 @@ mod tests { let dummy_account = Addr::unchecked("n16t2umcd83zjpl5puyuuq6lgmy4p3qedjd8ynn6"); let dummy_client = MockClient { address: dummy_account, - vesting_contract: Addr::unchecked("n17tj0a0w6v7r2dc54rnkzfza6s8hxs87rj273a5"), signing_nonce: 42, }; @@ -240,16 +223,8 @@ mod tests { dummy_pledge.clone(), true, ) - .await - .unwrap(); - - let plaintext_vesting = signing_msg_vesting.to_plaintext().unwrap(); - let sig_vesting: MessageSignature = identity_keypair - .private_key() - .sign(&plaintext_vesting) - .to_bytes() - .as_ref() - .into(); + .await; + assert!(signing_msg_vesting.is_err()); let res = verify_mixnode_bonding_sign_payload( &dummy_client, @@ -262,28 +237,6 @@ mod tests { .await; assert!(res.is_ok()); - let res = verify_mixnode_bonding_sign_payload( - &dummy_client, - &dummy_mixnode, - &dummy_cost_params, - &dummy_pledge, - true, - &sig_vesting, - ) - .await; - assert!(res.is_ok()); - - let res = verify_mixnode_bonding_sign_payload( - &dummy_client, - &dummy_mixnode, - &dummy_cost_params, - &dummy_pledge, - false, - &sig_vesting, - ) - .await; - assert!(res.is_err()); - let res = verify_mixnode_bonding_sign_payload( &dummy_client, &dummy_mixnode, @@ -315,7 +268,6 @@ mod tests { let dummy_account = Addr::unchecked("n16t2umcd83zjpl5puyuuq6lgmy4p3qedjd8ynn6"); let dummy_client = MockClient { address: dummy_account, - vesting_contract: Addr::unchecked("n17tj0a0w6v7r2dc54rnkzfza6s8hxs87rj273a5"), signing_nonce: 42, }; @@ -342,16 +294,8 @@ mod tests { dummy_pledge.clone(), true, ) - .await - .unwrap(); - - let plaintext_vesting = signing_msg_vesting.to_plaintext().unwrap(); - let sig_vesting: MessageSignature = identity_keypair - .private_key() - .sign(&plaintext_vesting) - .to_bytes() - .as_ref() - .into(); + .await; + assert!(signing_msg_vesting.is_err()); let res = verify_gateway_bonding_sign_payload( &dummy_client, @@ -363,26 +307,6 @@ mod tests { .await; assert!(res.is_ok()); - let res = verify_gateway_bonding_sign_payload( - &dummy_client, - &dummy_gateway, - &dummy_pledge, - true, - &sig_vesting, - ) - .await; - assert!(res.is_ok()); - - let res = verify_gateway_bonding_sign_payload( - &dummy_client, - &dummy_gateway, - &dummy_pledge, - false, - &sig_vesting, - ) - .await; - assert!(res.is_err()); - let res = verify_gateway_bonding_sign_payload( &dummy_client, &dummy_gateway, diff --git a/nym-wallet/src-tauri/src/operations/signatures/sign.rs b/nym-wallet/src-tauri/src/operations/signatures/sign.rs index c494bc5b4f..59da5cae36 100644 --- a/nym-wallet/src-tauri/src/operations/signatures/sign.rs +++ b/nym-wallet/src-tauri/src/operations/signatures/sign.rs @@ -131,8 +131,5 @@ pub async fn verify( let message_as_bytes = message.into_bytes(); Ok(verifying_key .verify(&message_as_bytes, &signature) - .map_err(|e| { - log::error!(">>> Verification failed, wrong signature"); - e - })?) + .inspect_err(|_| log::error!(">>> Verification failed, wrong signature"))?) } diff --git a/nym-wallet/src-tauri/src/operations/vesting/migrate.rs b/nym-wallet/src-tauri/src/operations/vesting/migrate.rs new file mode 100644 index 0000000000..e5624cfbf4 --- /dev/null +++ b/nym-wallet/src-tauri/src/operations/vesting/migrate.rs @@ -0,0 +1,93 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::BackendError; +use crate::nyxd_client; +use crate::state::WalletState; +use nym_mixnet_contract_common::ExecuteMsg; +use nym_types::transaction::TransactionExecuteResult; +use nym_validator_client::nyxd::contract_traits::{ + MixnetSigningClient, NymContractsProvider, PagedMixnetQueryClient, +}; +use nym_validator_client::nyxd::Fee; + +#[tauri::command] +pub async fn migrate_vested_mixnode( + fee: Option, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.read().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + log::info!(">>> migrate vested mixnode, fee = {fee:?}"); + + let res = nyxd_client!(state).migrate_vested_mixnode(fee).await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + +#[tauri::command] +pub async fn migrate_vested_delegations( + state: tauri::State<'_, WalletState>, +) -> Result { + log::info!(">>> migrate vested delegations"); + + let guard = state.read().await; + let client = guard.current_client()?; + + let address = client.nyxd.address(); + let mixnet_contract = client + .nyxd + .mixnet_contract_address() + .expect("unavailable mixnet contract address"); + + log::info!(" >>> Get delegations"); + let delegations = client + .nyxd + .get_all_delegator_delegations(&address) + .await + .inspect_err(|err| { + log::error!(" <<< Failed to get delegations. Error: {}", err); + })?; + log::info!(" <<< {} delegations", delegations.len()); + + let vesting_delegations = delegations + .into_iter() + .filter(|d| d.proxy.is_some()) + .collect::>(); + + log::info!(" <<< {} vesting delegations", vesting_delegations.len()); + + if vesting_delegations.is_empty() { + return Err(BackendError::NoVestingDelegations); + } + + let mut migrate_msgs = Vec::new(); + for delegation in &vesting_delegations { + migrate_msgs.push(( + ExecuteMsg::MigrateVestedDelegation { + mix_id: delegation.mix_id, + }, + Vec::new(), + )); + } + + let res = client + .nyxd + .execute_multiple( + mixnet_contract, + migrate_msgs, + None, + format!( + "migrating {} vesting delegations", + vesting_delegations.len() + ), + ) + .await?; + + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result(res, None)?) +} diff --git a/nym-wallet/src-tauri/src/operations/vesting/mod.rs b/nym-wallet/src-tauri/src/operations/vesting/mod.rs index fb80d4ed43..7ed3d0f0dc 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/mod.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/mod.rs @@ -1,4 +1,5 @@ pub mod bond; pub mod delegate; +pub mod migrate; pub mod queries; pub mod rewards; diff --git a/nym-wallet/src-tauri/tauri.conf.json b/nym-wallet/src-tauri/tauri.conf.json index 5236817066..0067868c37 100644 --- a/nym-wallet/src-tauri/tauri.conf.json +++ b/nym-wallet/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "package": { "productName": "nym-wallet", - "version": "1.2.13" + "version": "1.2.14" }, "build": { "distDir": "../dist", diff --git a/nym-wallet/src/components/Bonding/BondedMixnode.tsx b/nym-wallet/src/components/Bonding/BondedMixnode.tsx index 199c8f5653..e569cd7aec 100644 --- a/nym-wallet/src/components/Bonding/BondedMixnode.tsx +++ b/nym-wallet/src/components/Bonding/BondedMixnode.tsx @@ -127,6 +127,7 @@ export const BondedMixnode = ({ ), id: 'actions-cell', diff --git a/nym-wallet/src/components/Bonding/forms/MixnodeAmountForm.tsx b/nym-wallet/src/components/Bonding/forms/MixnodeAmountForm.tsx index 42a8d88061..f24f9138b7 100644 --- a/nym-wallet/src/components/Bonding/forms/MixnodeAmountForm.tsx +++ b/nym-wallet/src/components/Bonding/forms/MixnodeAmountForm.tsx @@ -22,6 +22,8 @@ const MixnodeAmountForm = ({ denom: CurrencyDenom; onNext: (data: MixnodeAmount) => void; }) => { + const { mixnetContractParams } = useContext(AppContext); + const { register, formState: { errors }, @@ -29,7 +31,7 @@ const MixnodeAmountForm = ({ setValue, getValues, setError, - } = useForm({ resolver: yupResolver(amountSchema), defaultValues: amountData }); + } = useForm({ resolver: yupResolver(amountSchema(mixnetContractParams)), defaultValues: amountData }); const { userBalance } = useContext(AppContext); diff --git a/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts b/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts index 4ba393db38..a72364377c 100644 --- a/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts +++ b/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts @@ -8,6 +8,7 @@ import { validateRawPort, validateVersion, } from 'src/utils'; +import { TauriContractStateParams } from '../../../types'; export const mixnodeValidationSchema = Yup.object().shape({ identityKey: Yup.string() @@ -41,39 +42,60 @@ export const mixnodeValidationSchema = Yup.object().shape({ .test('valid-http', 'A valid http-api port is required', (value) => (value ? validateRawPort(value) : false)), }); -const operatingCostAndPmValidation = { - profitMargin: Yup.number().required('Profit Percentage is required').min(4).max(80), - operatorCost: Yup.object().shape({ - amount: Yup.string() - .required('An operating cost is required') - // eslint-disable-next-line prefer-arrow-callback - .test( - 'valid-operating-cost', - 'A valid amount is required (min 40 - max 2000)', - async function isValidAmount(this, value) { - if (value && (!Number(value) || isLessThan(+value, 40) || isGreaterThan(+value, 2000))) { - return this.createError({ message: 'A valid amount is required (min 40 - max 2000)' }); +const operatingCostAndPmValidation = (params?: TauriContractStateParams) => { + const defaultParams = { + profit_margin: { + minimum: parseFloat(params?.profit_margin.minimum || '0%'), + maximum: parseFloat(params?.profit_margin.maximum || '100%'), + }, + + operating_cost: { + minimum: parseFloat(params?.operating_cost.minimum.amount || '0'), + maximum: parseFloat(params?.operating_cost.maximum.amount || '1000000000'), + }, + }; + + return { + profitMargin: Yup.number() + .required('Profit Percentage is required') + .min(defaultParams.profit_margin.minimum) + .max(defaultParams.profit_margin.maximum), + operatorCost: Yup.object().shape({ + amount: Yup.string() + .required('An operating cost is required') + // eslint-disable-next-line prefer-arrow-callback + .test('valid-operating-cost', 'A valid amount is required', async function isValidAmount(this, value) { + if ( + value && + (!Number(value) || + isLessThan(+value, defaultParams.operating_cost.minimum) || + isGreaterThan(+value, Number(defaultParams.operating_cost.maximum))) + ) { + return this.createError({ + message: `A valid amount is required (min ${defaultParams?.operating_cost.minimum} - max ${defaultParams?.operating_cost.maximum})`, + }); } return true; - }, - ), - }), + }), + }), + }; }; -export const amountSchema = Yup.object().shape({ - amount: Yup.object().shape({ - amount: Yup.string() - .required('An amount is required') - .test('valid-amount', 'Pledge error', async function isValidAmount(this, value) { - const isValid = await validateAmount(value || '', '100'); - if (!isValid) { - return this.createError({ message: 'A valid amount is required (min 100)' }); - } - return true; - }), - }), - ...operatingCostAndPmValidation, -}); +export const amountSchema = (params?: TauriContractStateParams) => + Yup.object().shape({ + amount: Yup.object().shape({ + amount: Yup.string() + .required('An amount is required') + .test('valid-amount', 'Pledge error', async function isValidAmount(this, value) { + const isValid = await validateAmount(value || '', '100'); + if (!isValid) { + return this.createError({ message: 'A valid amount is required (min 100)' }); + } + return true; + }), + }), + ...operatingCostAndPmValidation(params), + }); export const bondedInfoParametersValidationSchema = Yup.object().shape({ host: Yup.string() @@ -99,6 +121,7 @@ export const bondedInfoParametersValidationSchema = Yup.object().shape({ .test('valid-http', 'A valid http-api port is required', (value) => (value ? validateRawPort(value) : false)), }); -export const bondedNodeParametersValidationSchema = Yup.object().shape({ - ...operatingCostAndPmValidation, -}); +export const bondedNodeParametersValidationSchema = (params?: TauriContractStateParams) => + Yup.object().shape({ + ...operatingCostAndPmValidation(params), + }); diff --git a/nym-wallet/src/components/VestingWarningModal/index.tsx b/nym-wallet/src/components/VestingWarningModal/index.tsx new file mode 100644 index 0000000000..58fc4d30b4 --- /dev/null +++ b/nym-wallet/src/components/VestingWarningModal/index.tsx @@ -0,0 +1,33 @@ +import React, { FC } from 'react'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; + +export const VestingWarningModal: FC<{ + kind: 'delegations' | 'bond'; + isVisible: boolean; + handleClose: () => void; + handleMigrate: () => Promise; +}> = ({ kind, isVisible, handleClose, handleMigrate }) => ( +

+ Migrate your {kind}? + + + By clicking yes we will migrate your {kind} to the mixnet contract. + + + The operation will be instant, you will keep your rewards and they will continue to accumulate. Once migrated, + you will be able to withdraw your rewards. + + + + + + + +); diff --git a/nym-wallet/src/context/bonding.tsx b/nym-wallet/src/context/bonding.tsx index 687c754ff7..58c212a4e8 100644 --- a/nym-wallet/src/context/bonding.tsx +++ b/nym-wallet/src/context/bonding.tsx @@ -53,6 +53,7 @@ import { generateGatewayMsgPayload as generateGatewayMsgPayloadReq, updateBond as updateBondReq, vestingUpdateBond as vestingUpdateBondReq, + migrateVestedMixnode as tauriMigrateVestedMixnode, } from '../requests'; import { useCheckOwnership } from '../hooks/useCheckOwnership'; import { AppContext } from './main'; @@ -128,6 +129,7 @@ export type TBondingContext = { generateMixnodeMsgPayload: (data: TBondMixnodeSignatureArgs) => Promise; generateGatewayMsgPayload: (data: TBondGatewaySignatureArgs) => Promise; isVestingAccount: boolean; + migrateVestedMixnode: () => Promise; }; export const BondingContext = createContext({ @@ -157,6 +159,9 @@ export const BondingContext = createContext({ generateGatewayMsgPayload: async () => { throw new Error('Not implemented'); }, + migrateVestedMixnode: async () => { + throw new Error('Not implemented'); + }, isVestingAccount: false, }); @@ -599,6 +604,13 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen return message; }; + const migrateVestedMixnode = async () => { + setIsLoading(true); + const tx = await tauriMigrateVestedMixnode(); + setIsLoading(false); + return tx; + }; + const memoizedValue = useMemo( () => ({ isLoading: isLoading || isOwnershipLoading, @@ -613,6 +625,7 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen updateBondAmount, generateMixnodeMsgPayload, generateGatewayMsgPayload, + migrateVestedMixnode, isVestingAccount, }), [isLoading, isOwnershipLoading, error, bondedNode, isVestingAccount], diff --git a/nym-wallet/src/context/main.tsx b/nym-wallet/src/context/main.tsx index af965b79a0..24970ca3b6 100644 --- a/nym-wallet/src/context/main.tsx +++ b/nym-wallet/src/context/main.tsx @@ -4,9 +4,10 @@ import { useNavigate } from 'react-router-dom'; import { useSnackbar } from 'notistack'; import { Account, AccountEntry, MixNodeDetails } from '@nymproject/types'; import { getVersion } from '@tauri-apps/api/app'; -import { AppEnv, Network } from '../types'; +import { AppEnv, Network, TauriContractStateParams } from '../types'; import { TUseuserBalance, useGetBalance } from '../hooks/useGetBalance'; import { + getContractParams, getEnv, getMixnodeBondDetails, listAccounts, @@ -67,6 +68,7 @@ export type TAppContext = { keepState: () => Promise; printBalance: string; printVestedBalance?: string; // spendable vested token + mixnetContractParams?: TauriContractStateParams; }; interface RustState { @@ -94,6 +96,7 @@ export const AppProvider: FCWithChildren = ({ children }) => { const [showReceiveModal, setShowReceiveModal] = useState(false); const [printBalance, setPrintBalance] = useState('-'); const [printVestedBalance, setPrintVestedBalance] = useState(); + const [mixnetContractParams, setMixnetContractParams] = useState(); const userBalance = useGetBalance(clientDetails); const navigate = useNavigate(); @@ -230,6 +233,10 @@ export const AppProvider: FCWithChildren = ({ children }) => { } } setIsAdminAddress(newValue); + + getContractParams().then((params) => { + setMixnetContractParams(params); + }); }, [appEnv, network, clientDetails?.client_address]); const logIn = async ({ type, value }: { type: TLoginType; value: string }) => { @@ -329,6 +336,7 @@ export const AppProvider: FCWithChildren = ({ children }) => { handleSwitchMode, printBalance, printVestedBalance, + mixnetContractParams, }), [ appVersion, @@ -347,6 +355,7 @@ export const AppProvider: FCWithChildren = ({ children }) => { showTerminal, showSendModal, showReceiveModal, + mixnetContractParams, ], ); diff --git a/nym-wallet/src/context/mocks/bonding.tsx b/nym-wallet/src/context/mocks/bonding.tsx index 6218e9aa50..e961272c66 100644 --- a/nym-wallet/src/context/mocks/bonding.tsx +++ b/nym-wallet/src/context/mocks/bonding.tsx @@ -207,6 +207,7 @@ export const MockBondingContextProvider = ({ generateMixnodeMsgPayload, generateGatewayMsgPayload, isVestingAccount: false, + migrateVestedMixnode: async () => undefined, }), [isLoading, error, bondedMixnode, bondedGateway, trigger, fee], ); diff --git a/nym-wallet/src/pages/bonding/Bonding.tsx b/nym-wallet/src/pages/bonding/Bonding.tsx index 24aeb768ee..3ab472d1e4 100644 --- a/nym-wallet/src/pages/bonding/Bonding.tsx +++ b/nym-wallet/src/pages/bonding/Bonding.tsx @@ -1,7 +1,7 @@ import React, { useContext, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { FeeDetails } from '@nymproject/types'; -import { Box } from '@mui/material'; +import { Alert, AlertTitle, Box, Button, Typography } from '@mui/material'; import { TPoolOption } from 'src/components'; import { Bond } from 'src/components/Bonding/Bond'; import { BondedMixnode } from 'src/components/Bonding/BondedMixnode'; @@ -17,15 +17,16 @@ import { AppContext, urls } from 'src/context/main'; import { isGateway, isMixnode, TBondGatewayArgs, TBondMixNodeArgs, TUpdateBondArgs } from 'src/types'; import { BondedGateway } from 'src/components/Bonding/BondedGateway'; import { RedeemRewardsModal } from 'src/components/Bonding/modals/RedeemRewardsModal'; +import { VestingWarningModal } from 'src/components/VestingWarningModal'; import { BondingContextProvider, useBondingContext } from '../../context'; -const Bonding = () => { +export const Bonding = () => { const [showModal, setShowModal] = useState< 'bond-mixnode' | 'bond-gateway' | 'update-bond' | 'update-bond-oversaturated' | 'unbond' | 'redeem' >(); const [confirmationDetails, setConfirmationDetails] = useState(); const [uncappedSaturation, setUncappedSaturation] = useState(); - + const [showMigrationModal, setShowMigrationModal] = useState(false); const { network, clientDetails, @@ -34,8 +35,17 @@ const Bonding = () => { const navigate = useNavigate(); - const { bondedNode, bondMixnode, bondGateway, redeemRewards, isLoading, updateBondAmount, error, refresh } = - useBondingContext(); + const { + bondedNode, + bondMixnode, + bondGateway, + redeemRewards, + isLoading, + updateBondAmount, + error, + refresh, + migrateVestedMixnode, + } = useBondingContext(); useEffect(() => { if (bondedNode && isMixnode(bondedNode) && bondedNode.uncappedStakeSaturation) { @@ -43,6 +53,18 @@ const Bonding = () => { } }, [bondedNode]); + const handleMigrateVestedMixnode = async () => { + setShowMigrationModal(false); + const tx = await migrateVestedMixnode(); + if (tx) { + setConfirmationDetails({ + status: 'success', + title: 'Migration successful', + txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`, + }); + } + }; + const handleCloseModal = async () => { setShowModal(undefined); refresh(); @@ -138,6 +160,33 @@ const Bonding = () => { return ( + {bondedNode?.proxy && ( + + Your bonded node is using tokens from the vesting contract! + + In order to claim your rewards, you will need to migrate it out of the vesting contract.{' '} + + + Never fear, if you do not migrate them, you will continue to get rewards. + However, please migrate your bonded node as soon as possible. + + + + )} + + { + setShowMigrationModal(false); + }} + handleMigrate={async () => { + await handleMigrateVestedMixnode(); + }} + /> + {!bondedNode && setShowModal('bond-mixnode')} />} {bondedNode && isMixnode(bondedNode) && ( 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 0558d0af35..4f2032ac30 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 @@ -51,6 +51,8 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode const { fee, getFee, resetFeeState } = useGetFee(); + const { mixnetContractParams } = useContext(AppContext); + const defaultValues = { operatorCost: bondedNode.operatorCost, profitMargin: bondedNode.profitMargin, @@ -63,7 +65,7 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode setValue, formState: { errors, isSubmitting, isDirty, isValid }, } = useForm({ - resolver: yupResolver(bondedNodeParametersValidationSchema), + resolver: yupResolver(bondedNodeParametersValidationSchema(mixnetContractParams)), mode: 'onChange', defaultValues, }); diff --git a/nym-wallet/src/pages/delegation/index.tsx b/nym-wallet/src/pages/delegation/index.tsx index 3e985a3526..b51ebcf60f 100644 --- a/nym-wallet/src/pages/delegation/index.tsx +++ b/nym-wallet/src/pages/delegation/index.tsx @@ -1,5 +1,5 @@ import React, { FC, useContext, useEffect, useState } from 'react'; -import { Box, Button, Paper, Stack, Typography } from '@mui/material'; +import { Alert, AlertTitle, Box, Button, Paper, Stack, Typography } from '@mui/material'; import { Theme, useTheme } from '@mui/material/styles'; import { DecCoin, decimalToFloatApproximation, DelegationWithEverything, FeeDetails } from '@nymproject/types'; import { Link } from '@nymproject/react/link/Link'; @@ -8,11 +8,11 @@ import { DelegationList } from 'src/components/Delegation/DelegationList'; import { TPoolOption } from 'src/components'; import { Console } from 'src/utils/console'; import { OverSaturatedBlockerModal } from 'src/components/Delegation/DelegateBlocker'; -import { getSpendableCoins, userBalance } from 'src/requests'; +import { getSpendableCoins, migrateVestedDelegations, userBalance } from 'src/requests'; import { LoadingModal } from 'src/components/Modals/LoadingModal'; import { getIntervalAsDate, toPercentIntegerString } from 'src/utils'; import { RewardsSummary } from '../../components/Rewards/RewardsSummary'; -import { DelegationContextProvider, TDelegations, useDelegationContext } from '../../context/delegations'; +import { DelegationContextProvider, isDelegation, TDelegations, useDelegationContext } from '../../context/delegations'; import { RewardsContextProvider, useRewardsContext } from '../../context/rewards'; import { DelegateModal } from '../../components/Delegation/DelegateModal'; import { UndelegateModal } from '../../components/Delegation/UndelegateModal'; @@ -20,6 +20,7 @@ import { DelegationListItemActions } from '../../components/Delegation/Delegatio import { RedeemModal } from '../../components/Rewards/RedeemModal'; import { DelegationModal, DelegationModalProps } from '../../components/Delegation/DelegationModal'; import { backDropStyles, modalStyles } from '../../../.storybook/storiesStyles'; +import { VestingWarningModal } from '../../components/VestingWarningModal'; const storybookStyles = (theme: Theme, isStorybook?: boolean, backdropProps?: object) => isStorybook @@ -38,6 +39,8 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { const [currentDelegationListActionItem, setCurrentDelegationListActionItem] = useState(); const [saturationError, setSaturationError] = useState<{ action: 'compound' | 'delegate'; saturation: string }>(); const [nextEpoch, setNextEpoch] = useState(); + const [showVestingWarningModal, setShowVestingWarningModal] = useState(false); + const [showVestingMigrationProgressModal, setShowVestingMigrationProgressModal] = useState(false); const theme = useTheme(); const { @@ -57,6 +60,11 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { refresh: refreshDelegations, } = useDelegationContext(); + const delegationsUseVestingTokens: boolean = React.useMemo( + () => Boolean(delegations?.filter((d) => isDelegation(d) && d.uses_vesting_contract_tokens).length), + [delegations], + ); + const { refresh: refreshRewards, claimRewards } = useRewardsContext(); const refresh = async () => Promise.all([refreshDelegations(), refreshRewards()]); @@ -105,6 +113,13 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { return () => clearInterval(timer); }, []); + const doMigrateNow = async () => { + setShowVestingMigrationProgressModal(true); + await migrateVestedDelegations(); + await refresh(); + setShowVestingMigrationProgressModal(false); + }; + useEffect(() => { refreshWithIntervalUpdate(); }, [clientDetails, confirmationModalProps]); @@ -119,6 +134,11 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { return; } + if (item.uses_vesting_contract_tokens) { + setShowVestingWarningModal(true); + return; + } + setCurrentDelegationListActionItem(item); // eslint-disable-next-line default-case switch (action) { @@ -305,12 +325,46 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { const delegationsComponent = (delegationItems: TDelegations | undefined) => { if (delegationItems && Boolean(delegationItems?.length)) { return ( - + <> + {delegationsUseVestingTokens && ( + <> + + + Some of your delegations are using tokens from the vesting contract! + + + In order to claim your rewards, you will need to migrate them out of the vesting contract.{' '} + + + Never fear, if you do not migrate them,{' '} + you will continue to get rewards. However, please migrate your delegations as soon as + possible. + + + + setShowVestingWarningModal(false)} + /> + {showVestingMigrationProgressModal && } + + )} + + ); } diff --git a/nym-wallet/src/requests/actions.ts b/nym-wallet/src/requests/actions.ts index cf09085d5f..7361841f3f 100644 --- a/nym-wallet/src/requests/actions.ts +++ b/nym-wallet/src/requests/actions.ts @@ -52,3 +52,5 @@ export const unbond = async (type: EnumNodeType) => { export const updateBond = async (args: TUpdateBondArgs) => invokeWrapper('update_pledge', args); + +export const migrateVestedMixnode = async () => invokeWrapper('migrate_vested_mixnode'); diff --git a/nym-wallet/src/requests/delegation.ts b/nym-wallet/src/requests/delegation.ts index d68d8a9084..1ab73ba655 100644 --- a/nym-wallet/src/requests/delegation.ts +++ b/nym-wallet/src/requests/delegation.ts @@ -31,3 +31,6 @@ export const undelegateAllFromMixnode = async ( export const delegateToMixnode = async (mixId: number, amount: DecCoin, fee?: Fee) => invokeWrapper('delegate_to_mixnode', { mixId, amount, fee }); + +export const migrateVestedDelegations = async () => + invokeWrapper('migrate_vested_delegations'); diff --git a/nym-wallet/src/types/rust/OperatingCostRange.ts b/nym-wallet/src/types/rust/OperatingCostRange.ts new file mode 100644 index 0000000000..b3877c8347 --- /dev/null +++ b/nym-wallet/src/types/rust/OperatingCostRange.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DecCoin } from '@nymproject/types/src/types/rust/DecCoin'; + +export interface TauriOperatingCostRange { + minimum: DecCoin; + maximum: DecCoin; +} diff --git a/nym-wallet/src/types/rust/ProfitMarginRange.ts b/nym-wallet/src/types/rust/ProfitMarginRange.ts new file mode 100644 index 0000000000..c12d49d5d9 --- /dev/null +++ b/nym-wallet/src/types/rust/ProfitMarginRange.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface TauriProfitMarginRange { + minimum: string; + maximum: string; +} diff --git a/nym-wallet/src/types/rust/StateParams.ts b/nym-wallet/src/types/rust/StateParams.ts index 2119c59470..b909e4c734 100644 --- a/nym-wallet/src/types/rust/StateParams.ts +++ b/nym-wallet/src/types/rust/StateParams.ts @@ -1,8 +1,12 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { DecCoin } from '@nymproject/types/src/types/rust/DecCoin'; +import type { TauriOperatingCostRange } from './OperatingCostRange'; +import type { TauriProfitMarginRange } from './ProfitMarginRange'; export interface TauriContractStateParams { minimum_mixnode_pledge: DecCoin; minimum_gateway_pledge: DecCoin; minimum_mixnode_delegation: DecCoin | null; + operating_cost: TauriOperatingCostRange; + profit_margin: TauriProfitMarginRange; } diff --git a/sdk/rust/nym-sdk/Cargo.toml b/sdk/rust/nym-sdk/Cargo.toml index b657db8570..d1170d3973 100644 --- a/sdk/rust/nym-sdk/Cargo.toml +++ b/sdk/rust/nym-sdk/Cargo.toml @@ -9,11 +9,16 @@ license.workspace = true [dependencies] async-trait = { workspace = true } bip39 = { workspace = true } -nym-client-core = { path = "../../../common/client-core", features = ["fs-surb-storage", "fs-gateways-storage"] } +nym-client-core = { path = "../../../common/client-core", features = [ + "fs-credentials-storage", + "fs-surb-storage", + "fs-gateways-storage", +] } nym-crypto = { path = "../../../common/crypto" } -nym-gateway-requests = { path = "../../../gateway/gateway-requests" } +nym-gateway-requests = { path = "../../../common/gateway-requests" } nym-bandwidth-controller = { path = "../../../common/bandwidth-controller" } nym-credentials = { path = "../../../common/credentials" } +nym-credentials-interface = { path = "../../../common/credentials-interface" } nym-credential-storage = { path = "../../../common/credential-storage" } nym-credential-utils = { path = "../../../common/credential-utils" } nym-network-defaults = { path = "../../../common/network-defaults" } @@ -21,7 +26,9 @@ nym-sphinx = { path = "../../../common/nymsphinx" } nym-task = { path = "../../../common/task" } nym-topology = { path = "../../../common/topology" } nym-socks5-client-core = { path = "../../../common/socks5-client-core" } -nym-validator-client = { path = "../../../common/client-libs/validator-client", features = ["http-client"] } +nym-validator-client = { path = "../../../common/client-libs/validator-client", features = [ + "http-client", +] } nym-socks5-requests = { path = "../../../common/socks5/requests" } nym-ordered-buffer = { path = "../../../common/socks5/ordered-buffer" } nym-service-providers-common = { path = "../../../service-providers/common" } @@ -29,6 +36,7 @@ bytecodec = { workspace = true } httpcodec = { workspace = true } bytes = { workspace = true } http = { workspace = true } +zeroize = { workspace = true } futures = { workspace = true } log = { workspace = true } diff --git a/sdk/rust/nym-sdk/examples/bandwidth.rs b/sdk/rust/nym-sdk/examples/bandwidth.rs index 96a064cf1d..b104ee51ea 100644 --- a/sdk/rust/nym-sdk/examples/bandwidth.rs +++ b/sdk/rust/nym-sdk/examples/bandwidth.rs @@ -18,10 +18,12 @@ async fn main() -> anyhow::Result<()> { .enable_credentials_mode() .build()?; - let bandwidth_client = mixnet_client.create_bandwidth_client(mnemonic)?; + let bandwidth_client = mixnet_client + .create_bandwidth_client(mnemonic, Default::default()) + .await?; - // Get a bandwidth credential worth 1000000 unym for the mixnet_client - bandwidth_client.acquire(1000000).await?; + // Get a bandwidth credential for the mixnet_client + bandwidth_client.acquire().await?; // Connect using paid bandwidth credential let mut client = mixnet_client.connect_to_mixnet().await?; diff --git a/sdk/rust/nym-sdk/examples/simple.rs b/sdk/rust/nym-sdk/examples/simple.rs index 72b6568b6d..00ad24dc9a 100644 --- a/sdk/rust/nym-sdk/examples/simple.rs +++ b/sdk/rust/nym-sdk/examples/simple.rs @@ -6,6 +6,7 @@ async fn main() { nym_bin_common::logging::setup_logging(); // Passing no config makes the client fire up an ephemeral session and figure shit out on its own + // let mut client = mixnet::MixnetClient::connect_new().await.unwrap(); let mut client = mixnet::MixnetClient::connect_new().await.unwrap(); // Be able to get our client address diff --git a/sdk/rust/nym-sdk/src/bandwidth.rs b/sdk/rust/nym-sdk/src/bandwidth.rs index b1548dcb96..2152319f97 100644 --- a/sdk/rust/nym-sdk/src/bandwidth.rs +++ b/sdk/rust/nym-sdk/src/bandwidth.rs @@ -15,10 +15,10 @@ //! .build() //! .unwrap(); //! -//! let bandwidth_client = mixnet_client.create_bandwidth_client(String::from("my super secret mnemonic")).unwrap(); +//! let bandwidth_client = mixnet_client.create_bandwidth_client(String::from("my super secret mnemonic"), Default::default()).await.unwrap(); //! -//! // Get a bandwidth credential worth 1000000 unym for the mixnet_client -//! bandwidth_client.acquire(1000000).await.unwrap(); +//! // Get a bandwidth credential for the mixnet_client +//! bandwidth_client.acquire().await.unwrap(); //! //! // Connect using paid bandwidth credential //! let mut client = mixnet_client.connect_to_mixnet().await.unwrap(); @@ -41,4 +41,4 @@ mod client; -pub use client::{BandwidthAcquireClient, VoucherBlob}; +pub use client::BandwidthAcquireClient; diff --git a/sdk/rust/nym-sdk/src/bandwidth/client.rs b/sdk/rust/nym-sdk/src/bandwidth/client.rs index 5051e5a1d4..3f5acbd5e9 100644 --- a/sdk/rust/nym-sdk/src/bandwidth/client.rs +++ b/sdk/rust/nym-sdk/src/bandwidth/client.rs @@ -1,18 +1,14 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::error::{Error, Result}; -use nym_bandwidth_controller::acquire::state::State; +use crate::error::Result; use nym_credential_storage::storage::Storage; -use nym_credentials::coconut::bandwidth::IssuanceBandwidthCredential; +use nym_credential_utils::utils::issue_credential; +use nym_credentials_interface::TicketType; use nym_network_defaults::NymNetworkDetails; -use nym_validator_client::nyxd::Coin; use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; - -/// The serialized version of the yet untransformed bandwidth voucher. It can be used to complete -/// the acquirement process of a bandwidth credential. -/// Its serialized nature makes it easy to store and load it to e.g. disk. -pub type VoucherBlob = Vec; +use std::ops::Deref; +use zeroize::Zeroizing; /// Represents a client that can be used to acquire bandwidth. You typically create one when you /// want to connect to the mixnet using paid coconut bandwidth credentials. @@ -20,9 +16,10 @@ pub type VoucherBlob = Vec; /// [`crate::mixnet::DisconnectedMixnetClient::create_bandwidth_client`] on the associated mixnet /// client. pub struct BandwidthAcquireClient<'a, St: Storage> { - network_details: NymNetworkDetails, client: DirectSigningHttpRpcNyxdClient, storage: &'a St, + client_id: Zeroizing>, + ticketbook_type: TicketType, } impl<'a, St> BandwidthAcquireClient<'a, St> @@ -34,6 +31,8 @@ where network_details: NymNetworkDetails, mnemonic: String, storage: &'a St, + client_id: Vec, + ticketbook_type: TicketType, ) -> Result { let nyxd_url = network_details.endpoints[0].nyxd_url.as_str(); let config = nyxd::Config::try_from_nym_network_details(&network_details)?; @@ -44,40 +43,21 @@ where mnemonic.parse()?, )?; Ok(Self { - network_details, client, storage, + client_id: client_id.into(), + ticketbook_type, }) } - /// Buy a credential worth amount utokens. If [`Error::UnconvertedDeposit`] is returned, it - /// means the tokens have been deposited, but the proper bandwidth credential hasn't yet been - /// created. A [`VoucherBlob`] is returned that can be used for a later recovery of the - /// associated bandwidth credential, using [`Self::recover`]. - pub async fn acquire(&self, amount: u128) -> Result<()> { - let amount = Coin::new(amount, &self.network_details.chain_details.mix_denom.base); - let state = nym_bandwidth_controller::acquire::deposit(&self.client, amount).await?; - nym_bandwidth_controller::acquire::get_bandwidth_voucher(&state, &self.client, self.storage) - .await - .map_err(|reason| Error::UnconvertedDeposit { - reason, - voucher_blob: state.voucher.to_recovery_bytes(), - }) - } - - /// In case of an error in the mid of the acquire process, this function should be used for - /// later retries to recover the bandwidth credential, either immediately or after some time. - pub async fn recover(&self, voucher_blob: &VoucherBlob) -> Result<()> { - let voucher = IssuanceBandwidthCredential::try_from_recovered_bytes(voucher_blob) - .map_err(|_| Error::InvalidVoucherBlob)?; - let state = State::new(voucher); - nym_bandwidth_controller::acquire::get_bandwidth_voucher( - &state, + pub async fn acquire(&self) -> Result<()> { + issue_credential( &self.client, self.storage, + self.client_id.deref(), + self.ticketbook_type, ) .await?; - Ok(()) } } diff --git a/sdk/rust/nym-sdk/src/error.rs b/sdk/rust/nym-sdk/src/error.rs index e47b8b784f..7727583529 100644 --- a/sdk/rust/nym-sdk/src/error.rs +++ b/sdk/rust/nym-sdk/src/error.rs @@ -55,15 +55,6 @@ pub enum Error { #[error("socks5 channel could not be started")] Socks5NotStarted, - #[error( - "deposited funds were not converted to a deposit - {reason}; the voucher blob can be used for \ - later retry" - )] - UnconvertedDeposit { - reason: nym_bandwidth_controller::error::BandwidthControllerError, - voucher_blob: crate::bandwidth::VoucherBlob, - }, - #[error("bandwidth controller error: {0}")] BandwidthControllerError(#[from] nym_bandwidth_controller::error::BandwidthControllerError), @@ -88,6 +79,12 @@ pub enum Error { source: Box, }, + #[error(transparent)] + CredentialIssuanceError { + #[from] + source: nym_credential_utils::Error, + }, + #[error("loaded shared gateway key without providing information about what gateway it corresponds to")] GatewayWithUnknownEndpoint, diff --git a/sdk/rust/nym-sdk/src/mixnet.rs b/sdk/rust/nym-sdk/src/mixnet.rs index 1d2fd0fcc1..57a17c1569 100644 --- a/sdk/rust/nym-sdk/src/mixnet.rs +++ b/sdk/rust/nym-sdk/src/mixnet.rs @@ -65,14 +65,14 @@ pub use nym_client_core::{ }; pub use nym_credential_storage::{ ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage, - models::StoredIssuedCredential, storage::Storage as CredentialStorage, + models::StoredIssuedTicketbook, storage::Storage as CredentialStorage, }; pub use nym_crypto::asymmetric::ed25519; pub use nym_network_defaults::NymNetworkDetails; pub use nym_socks5_client_core::config::Socks5; pub use nym_sphinx::{ addressing::{ - clients::{ClientIdentity, Recipient}, + clients::{ClientIdentity, Recipient, RecipientFormattingError}, nodes::NodeIdentity, }, anonymous_replies::requests::AnonymousSenderTag, diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index f22c20e072..19048f5005 100644 --- a/sdk/rust/nym-sdk/src/mixnet/client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -28,6 +28,7 @@ use nym_client_core::error::ClientCoreError; use nym_client_core::init::helpers::current_gateways; use nym_client_core::init::setup_gateway; use nym_client_core::init::types::{GatewaySelectionSpecification, GatewaySetup}; +use nym_credentials_interface::TicketType; use nym_network_defaults::WG_TUN_DEVICE_IP_ADDRESS; use nym_socks5_client_core::config::Socks5; use nym_task::manager::TaskStatus; @@ -39,6 +40,7 @@ use std::net::IpAddr; use std::path::Path; use std::path::PathBuf; use url::Url; +use zeroize::Zeroizing; // The number of surbs to include in a message by default const DEFAULT_NUMBER_OF_SURBS: u32 = 10; @@ -550,18 +552,35 @@ where } /// Creates an associated [`BandwidthAcquireClient`] that can be used to acquire bandwidth - /// credentials for this client to consume. - pub fn create_bandwidth_client( + /// credentials of particular type for this client to consume. + pub async fn create_bandwidth_client( &self, mnemonic: String, + ticketbook_type: TicketType, ) -> Result> { if !self.config.enabled_credentials_mode { return Err(Error::DisabledCredentialsMode); } + let client_id_array = Zeroizing::new( + self.storage + .key_store() + .load_keys() + .await + .map_err(|e| Error::KeyStorageError { + source: Box::new(e), + })? + .identity_keypair() + .private_key() + .to_bytes(), + ); + let client_id = client_id_array.to_vec(); + BandwidthAcquireClient::new( self.config.network_details.clone(), mnemonic, self.storage.credential_store(), + client_id, + ticketbook_type, ) } diff --git a/sdk/typescript/packages/react-components/package.json b/sdk/typescript/packages/react-components/package.json index 49c288d4a2..6d7a21747f 100644 --- a/sdk/typescript/packages/react-components/package.json +++ b/sdk/typescript/packages/react-components/package.json @@ -26,9 +26,9 @@ "@cosmjs/math": "^0.27.1", "@mui/icons-material": ">= 5", "@mui/lab": "^5.0.0-alpha.72", - "@mui/material": ">= 5", - "@mui/styles": ">= 5", - "@mui/system": ">= 5", + "@mui/material": "5", + "@mui/styles": "5", + "@mui/system": "5", "@nymproject/mui-theme": "1", "@nymproject/nym-validator-client": "^0.18.0", "@nymproject/types": "1", @@ -39,7 +39,8 @@ "zxcvbn": "^4.4.2" }, "dependencies": { - "@mui/x-tree-view": "^7.10.0", + "@mui/lab": "5.0.0-alpha.170", + "@mui/x-tree-view": "^7.11.1", "flat": "^5.0.2", "use-clipboard-copy": "^0.2.0" }, diff --git a/service-providers/authenticator/Cargo.toml b/service-providers/authenticator/Cargo.toml index 31509db477..c25a5b2d47 100644 --- a/service-providers/authenticator/Cargo.toml +++ b/service-providers/authenticator/Cargo.toml @@ -13,7 +13,8 @@ anyhow = { workspace = true } bincode = { workspace = true } bs58 = { workspace = true } bytes = { workspace = true } -clap = { workspace = true } +clap = { workspace = true, features = ["cargo", "derive"] } +defguard_wireguard_rs = { workspace = true } fastrand = { workspace = true } futures = { workspace = true } ipnetwork = { workspace = true } @@ -28,8 +29,11 @@ tokio-util = { workspace = true, features = ["codec"] } url = { workspace = true } nym-authenticator-requests = { path = "../../common/authenticator-requests" } -nym-bin-common = { path = "../../common/bin-common" } -nym-client-core = { path = "../../common/client-core" } +nym-bin-common = { path = "../../common/bin-common", features = [ + "clap", + "output_format", +] } +nym-client-core = { path = "../../common/client-core", features = ["cli"] } nym-config = { path = "../../common/config" } nym-crypto = { path = "../../common/crypto" } nym-id = { path = "../../common/nym-id" } diff --git a/service-providers/authenticator/src/cli/peer_handler.rs b/service-providers/authenticator/src/cli/peer_handler.rs index b4d6e1faea..8c644f7ef7 100644 --- a/service-providers/authenticator/src/cli/peer_handler.rs +++ b/service-providers/authenticator/src/cli/peer_handler.rs @@ -38,6 +38,10 @@ impl DummyHandler { log::info!("[DUMMY] Removing peer {:?}", key); self.response_tx.send(PeerControlResponse::RemovePeer { success: true }).ok(); } + PeerControlRequest::QueryPeer(key) => { + log::info!("[DUMMY] Querying peer {:?}", key); + self.response_tx.send(PeerControlResponse::QueryPeer { success: false, peer: None }).ok(); + } PeerControlRequest::QueryBandwidth(key) => { log::info!("[DUMMY] Querying bandwidth for peer {:?}", key); self.response_tx.send(PeerControlResponse::QueryBandwidth { bandwidth_data: None }).ok(); diff --git a/service-providers/authenticator/src/error.rs b/service-providers/authenticator/src/error.rs index addb1cf56c..add82fd778 100644 --- a/service-providers/authenticator/src/error.rs +++ b/service-providers/authenticator/src/error.rs @@ -67,6 +67,9 @@ pub enum AuthenticatorError { #[error("internal data corruption: {0}")] InternalDataCorruption(String), + + #[error("peers can't be interacted with anymore")] + PeerInteractionStopped, } pub type Result = std::result::Result; diff --git a/service-providers/authenticator/src/lib.rs b/service-providers/authenticator/src/lib.rs index bcdfd3c341..ec7ca579f9 100644 --- a/service-providers/authenticator/src/lib.rs +++ b/service-providers/authenticator/src/lib.rs @@ -9,3 +9,4 @@ pub mod config; pub mod error; pub mod mixnet_client; pub mod mixnet_listener; +mod peer_manager; diff --git a/service-providers/authenticator/src/mixnet_listener.rs b/service-providers/authenticator/src/mixnet_listener.rs index a93b2185ef..b08b5054a5 100644 --- a/service-providers/authenticator/src/mixnet_listener.rs +++ b/service-providers/authenticator/src/mixnet_listener.rs @@ -6,7 +6,7 @@ use std::{ time::{Duration, SystemTime}, }; -use crate::error::AuthenticatorError; +use crate::{error::AuthenticatorError, peer_manager::PeerManager}; use futures::StreamExt; use ipnetwork::IpNetwork; use nym_authenticator_requests::v1::{ @@ -14,6 +14,7 @@ use nym_authenticator_requests::v1::{ request::{AuthenticatorRequest, AuthenticatorRequestData}, response::AuthenticatorResponse, }; +use nym_crypto::asymmetric::x25519::KeyPair; use nym_sdk::mixnet::{InputMessage, MixnetMessageSender, Recipient, TransmissionLane}; use nym_sphinx::receiver::ReconstructedMessage; use nym_task::TaskHandle; @@ -44,9 +45,7 @@ pub(crate) struct MixnetListener { // Registrations awaiting confirmation pub(crate) registration_in_progres: Arc, - pub(crate) wireguard_gateway_data: WireguardGatewayData, - - pub(crate) response_rx: UnboundedReceiver, + pub(crate) peer_manager: PeerManager, pub(crate) free_private_network_ips: Arc, @@ -69,8 +68,7 @@ impl MixnetListener { mixnet_client, task_handle, registration_in_progres: Default::default(), - wireguard_gateway_data, - response_rx, + peer_manager: PeerManager::new(wireguard_gateway_data, response_rx), free_private_network_ips: Arc::new( private_ip_network.iter().map(|ip| (ip, None)).collect(), ), @@ -78,6 +76,10 @@ impl MixnetListener { } } + fn keypair(&self) -> &Arc { + self.peer_manager.wireguard_gateway_data.keypair() + } + fn remove_stale_registrations(&self) -> Result<()> { for reg in self.registration_in_progres.iter().map(|reg| reg.clone()) { let mut ip = self @@ -125,21 +127,18 @@ impl MixnetListener { reply_to, )); } - if let Some(gateway_client) = self - .wireguard_gateway_data - .client_registry() - .get(&remote_public) - { + + let peer = self.peer_manager.query_peer(remote_public).await?; + if let Some(peer) = peer { + let Some(allowed_ip) = peer.allowed_ips.first() else { + return Err(AuthenticatorError::InternalError( + "private ip list should not be empty".to_string(), + )); + }; return Ok(AuthenticatorResponse::new_registered( RegistredData { - pub_key: PeerPublicKey::new( - self.wireguard_gateway_data - .keypair() - .public_key() - .to_bytes() - .into(), - ), - private_ip: gateway_client.private_ip, + pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + private_ip: allowed_ip.ip, wg_port: self.config.authenticator.announced_port, }, reply_to, @@ -155,7 +154,7 @@ impl MixnetListener { // mark it as used, even though it's not final *private_ip_ref = Some(SystemTime::now()); let gateway_data = GatewayClient::new( - self.wireguard_gateway_data.keypair().private_key(), + self.keypair().private_key(), remote_public.inner(), *private_ip_ref.key(), nonce, @@ -189,40 +188,12 @@ impl MixnetListener { .clone(); if gateway_client - .verify( - self.wireguard_gateway_data.keypair().private_key(), - registration_data.nonce, - ) + .verify(self.keypair().private_key(), registration_data.nonce) .is_ok() { - self.wireguard_gateway_data - .add_peer(&gateway_client) - .map_err(|err| { - AuthenticatorError::InternalError(format!("could not add peer: {:?}", err)) - })?; - - let PeerControlResponse::AddPeer { success } = - self.response_rx - .recv() - .await - .ok_or(AuthenticatorError::InternalError( - "no response for add peer".to_string(), - ))? - else { - return Err(AuthenticatorError::InternalError( - "unexpected response type".to_string(), - )); - }; - if !success { - return Err(AuthenticatorError::InternalError( - "adding peer could not be performed".to_string(), - )); - } + self.peer_manager.add_peer(&gateway_client).await?; self.registration_in_progres .remove(&gateway_client.pub_key()); - self.wireguard_gateway_data - .client_registry() - .insert(gateway_client.pub_key(), gateway_client); Ok(AuthenticatorResponse::new_registered( RegistredData { @@ -244,26 +215,7 @@ impl MixnetListener { request_id: u64, reply_to: Recipient, ) -> AuthenticatorHandleResult { - self.wireguard_gateway_data - .query_bandwidth(peer_public_key) - .map_err(|err| { - AuthenticatorError::InternalError(format!( - "could not query peer bandwidth: {:?}", - err - )) - })?; - let PeerControlResponse::QueryBandwidth { bandwidth_data } = self - .response_rx - .recv() - .await - .ok_or(AuthenticatorError::InternalError( - "no response for query".to_string(), - ))? - else { - return Err(AuthenticatorError::InternalError( - "unexpected response type".to_string(), - )); - }; + let bandwidth_data = self.peer_manager.query_bandwidth(peer_public_key).await?; Ok(AuthenticatorResponse::new_remaining_bandwidth( bandwidth_data, reply_to, diff --git a/service-providers/authenticator/src/peer_manager.rs b/service-providers/authenticator/src/peer_manager.rs new file mode 100644 index 0000000000..cdc847a2ca --- /dev/null +++ b/service-providers/authenticator/src/peer_manager.rs @@ -0,0 +1,140 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::*; +use defguard_wireguard_rs::{host::Peer, key::Key, net::IpAddrMask}; +use nym_wireguard::{ + peer_controller::{PeerControlRequest, PeerControlResponse}, + WireguardGatewayData, +}; +use nym_wireguard_types::{registration::RemainingBandwidthData, GatewayClient, PeerPublicKey}; +use tokio::sync::mpsc::UnboundedReceiver; + +pub struct PeerManager { + pub(crate) wireguard_gateway_data: WireguardGatewayData, + + pub(crate) response_rx: UnboundedReceiver, +} + +impl PeerManager { + pub fn new( + wireguard_gateway_data: WireguardGatewayData, + response_rx: UnboundedReceiver, + ) -> Self { + PeerManager { + wireguard_gateway_data, + response_rx, + } + } + pub async fn add_peer(&mut self, client: &GatewayClient) -> Result<()> { + let mut peer = Peer::new(Key::new(client.pub_key.to_bytes())); + peer.allowed_ips + .push(IpAddrMask::new(client.private_ip, 32)); + let msg = PeerControlRequest::AddPeer(peer); + self.wireguard_gateway_data + .peer_tx() + .send(msg) + .map_err(|_| AuthenticatorError::PeerInteractionStopped)?; + + let PeerControlResponse::AddPeer { success } = + self.response_rx + .recv() + .await + .ok_or(AuthenticatorError::InternalError( + "no response for add peer".to_string(), + ))? + else { + return Err(AuthenticatorError::InternalError( + "unexpected response type".to_string(), + )); + }; + if !success { + return Err(AuthenticatorError::InternalError( + "adding peer could not be performed".to_string(), + )); + } + Ok(()) + } + + pub async fn _remove_peer(&mut self, client: &GatewayClient) -> Result<()> { + let key = Key::new(client.pub_key().to_bytes()); + let msg = PeerControlRequest::RemovePeer(key); + self.wireguard_gateway_data + .peer_tx() + .send(msg) + .map_err(|_| AuthenticatorError::PeerInteractionStopped)?; + + let PeerControlResponse::RemovePeer { success } = + self.response_rx + .recv() + .await + .ok_or(AuthenticatorError::InternalError( + "no response for add peer".to_string(), + ))? + else { + return Err(AuthenticatorError::InternalError( + "unexpected response type".to_string(), + )); + }; + if !success { + return Err(AuthenticatorError::InternalError( + "adding peer could not be performed".to_string(), + )); + } + Ok(()) + } + + pub async fn query_peer(&mut self, public_key: PeerPublicKey) -> Result> { + let key = Key::new(public_key.to_bytes()); + let msg = PeerControlRequest::QueryPeer(key); + self.wireguard_gateway_data + .peer_tx() + .send(msg) + .map_err(|_| AuthenticatorError::PeerInteractionStopped)?; + + let PeerControlResponse::QueryPeer { success, peer } = self + .response_rx + .recv() + .await + .ok_or(AuthenticatorError::InternalError( + "no response for query peer".to_string(), + ))? + else { + return Err(AuthenticatorError::InternalError( + "unexpected response type".to_string(), + )); + }; + if !success { + return Err(AuthenticatorError::InternalError( + "querying peer could not be performed".to_string(), + )); + } + Ok(peer) + } + + pub async fn query_bandwidth( + &mut self, + peer_public_key: PeerPublicKey, + ) -> Result> { + let key = Key::new(peer_public_key.to_bytes()); + let msg = PeerControlRequest::QueryBandwidth(key); + self.wireguard_gateway_data + .peer_tx() + .send(msg) + .map_err(|_| AuthenticatorError::PeerInteractionStopped)?; + + let PeerControlResponse::QueryBandwidth { bandwidth_data } = self + .response_rx + .recv() + .await + .ok_or(AuthenticatorError::InternalError( + "no response for query".to_string(), + ))? + else { + return Err(AuthenticatorError::InternalError( + "unexpected response type".to_string(), + )); + }; + Ok(bandwidth_data) + } +} diff --git a/service-providers/ip-packet-router/src/cli/mod.rs b/service-providers/ip-packet-router/src/cli/mod.rs index 3de91de6be..d269d5045d 100644 --- a/service-providers/ip-packet-router/src/cli/mod.rs +++ b/service-providers/ip-packet-router/src/cli/mod.rs @@ -15,6 +15,7 @@ mod import_credential; mod init; mod list_gateways; mod run; +mod show_ticketbooks; mod sign; mod switch_gateway; @@ -76,6 +77,9 @@ pub(crate) enum Commands { /// Change the currently active gateway. Note that you must have already registered with the new gateway! SwitchGateway(switch_gateway::Args), + /// Display information associated with the imported ticketbooks, + ShowTicketbooks(show_ticketbooks::Args), + /// Sign to prove ownership of this network requester Sign(sign::Sign), @@ -132,6 +136,7 @@ pub(crate) async fn execute(args: Cli) -> Result<(), IpPacketRouterError> { Commands::ListGateways(args) => list_gateways::execute(args).await?, Commands::AddGateway(args) => add_gateway::execute(args).await?, Commands::SwitchGateway(args) => switch_gateway::execute(args).await?, + Commands::ShowTicketbooks(args) => show_ticketbooks::execute(args).await?, Commands::Sign(m) => sign::execute(&m).await?, Commands::BuildInfo(m) => build_info::execute(m), Commands::Completions(s) => s.generate(&mut Cli::command(), bin_name), diff --git a/service-providers/ip-packet-router/src/cli/show_ticketbooks.rs b/service-providers/ip-packet-router/src/cli/show_ticketbooks.rs new file mode 100644 index 0000000000..20c58a412c --- /dev/null +++ b/service-providers/ip-packet-router/src/cli/show_ticketbooks.rs @@ -0,0 +1,32 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CliIpPacketRouterClient; +use nym_bin_common::output_format::OutputFormat; +use nym_client_core::cli_helpers::client_show_ticketbooks::{ + show_ticketbooks, CommonShowTicketbooksArgs, +}; +use nym_ip_packet_router::error::IpPacketRouterError; + +#[derive(clap::Args)] +pub(crate) struct Args { + #[command(flatten)] + common_args: CommonShowTicketbooksArgs, + + #[arg(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +impl AsRef for Args { + fn as_ref(&self) -> &CommonShowTicketbooksArgs { + &self.common_args + } +} + +pub(crate) async fn execute(args: Args) -> Result<(), IpPacketRouterError> { + let output = args.output; + let res = show_ticketbooks::(args).await?; + + println!("{}", output.format(&res)); + Ok(()) +} diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index 7a9ffb06ff..adfdde70c7 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-network-requester" license = "GPL-3.0" -version = "1.1.40" +version = "1.1.41" authors.workspace = true edition.workspace = true rust-version = "1.70" @@ -44,7 +44,7 @@ zeroize = { workspace = true } # internal nym-async-file-watcher = { path = "../../common/async-file-watcher" } -nym-bin-common = { path = "../../common/bin-common", features = ["output_format"] } +nym-bin-common = { path = "../../common/bin-common", features = ["output_format", "clap"] } nym-client-core = { path = "../../common/client-core", features = ["cli", "fs-gateways-storage", "fs-surb-storage"] } nym-client-websocket-requests = { path = "../../clients/native/websocket-requests" } nym-config = { path = "../../common/config" } diff --git a/service-providers/network-requester/src/cli/mod.rs b/service-providers/network-requester/src/cli/mod.rs index d15d0e1621..11ef00cfd8 100644 --- a/service-providers/network-requester/src/cli/mod.rs +++ b/service-providers/network-requester/src/cli/mod.rs @@ -22,6 +22,7 @@ mod import_credential; mod init; mod list_gateways; mod run; +mod show_ticketbooks; mod sign; mod switch_gateway; @@ -86,6 +87,9 @@ pub(crate) enum Commands { /// Change the currently active gateway. Note that you must have already registered with the new gateway! SwitchGateway(switch_gateway::Args), + /// Display information associated with the imported ticketbooks, + ShowTicketbooks(show_ticketbooks::Args), + /// Show build information of this binary BuildInfo(build_info::BuildInfo), @@ -152,6 +156,7 @@ pub(crate) async fn execute(args: Cli) -> Result<(), NetworkRequesterError> { Commands::ListGateways(args) => list_gateways::execute(args).await?, Commands::AddGateway(args) => add_gateway::execute(args).await?, Commands::SwitchGateway(args) => switch_gateway::execute(args).await?, + Commands::ShowTicketbooks(args) => show_ticketbooks::execute(args).await?, Commands::BuildInfo(m) => build_info::execute(m), Commands::Completions(s) => s.generate(&mut Cli::command(), bin_name), Commands::GenerateFigSpec => fig_generate(&mut Cli::command(), bin_name), diff --git a/service-providers/network-requester/src/cli/show_ticketbooks.rs b/service-providers/network-requester/src/cli/show_ticketbooks.rs new file mode 100644 index 0000000000..b0e7f2febd --- /dev/null +++ b/service-providers/network-requester/src/cli/show_ticketbooks.rs @@ -0,0 +1,32 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CliNetworkRequesterClient; +use crate::error::NetworkRequesterError; +use nym_bin_common::output_format::OutputFormat; +use nym_client_core::cli_helpers::client_show_ticketbooks::{ + show_ticketbooks, CommonShowTicketbooksArgs, +}; + +#[derive(clap::Args)] +pub(crate) struct Args { + #[command(flatten)] + common_args: CommonShowTicketbooksArgs, + + #[arg(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +impl AsRef for Args { + fn as_ref(&self) -> &CommonShowTicketbooksArgs { + &self.common_args + } +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkRequesterError> { + let output = args.output; + let res = show_ticketbooks::(args).await?; + + println!("{}", output.format(&res)); + Ok(()) +} diff --git a/tools/internal/testnet-manager/Cargo.toml b/tools/internal/testnet-manager/Cargo.toml new file mode 100644 index 0000000000..929ac555a6 --- /dev/null +++ b/tools/internal/testnet-manager/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "testnet-manager" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow.workspace = true +bip39.workspace = true +bs58.workspace = true +console = "0.15.8" +cw-utils.workspace = true +clap = { workspace = true, features = ["cargo", "derive"] } +indicatif = "0.17.8" +rand.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"] } +tempfile = { workspace = true } +thiserror.workspace = true +time = { workspace = true, features = ["parsing", "formatting"] } +tokio = { workspace = true, features = ["rt-multi-thread", "macros", "process"] } +toml = "0.8.14" +tracing.workspace = true +url.workspace = true +zeroize = { workspace = true, features = ["zeroize_derive"] } + + +nym-bin-common = { path = "../../../common/bin-common", features = ["output_format", "basic_tracing"] } +nym-crypto = { path = "../../../common/crypto", features = ["asymmetric", "rand", "serde"] } +nym-config = { path = "../../../common/config" } +nym-validator-client = { path = "../../../common/client-libs/validator-client" } +nym-compact-ecash = { path = "../../../common/nym_offline_compact_ecash" } +dkg-bypass-contract = { path = "dkg-bypass-contract", default-features = false } + +# contracts: +nym-mixnet-contract-common = { path = "../../../common/cosmwasm-smart-contracts/mixnet-contract" } +nym-contracts-common = { path = "../../../common/cosmwasm-smart-contracts/contracts-common" } +nym-vesting-contract-common = { path = "../../../common/cosmwasm-smart-contracts/vesting-contract" } +nym-group-contract-common = { path = "../../../common/cosmwasm-smart-contracts/group-contract" } +nym-ecash-contract-common = { path = "../../../common/cosmwasm-smart-contracts/ecash-contract" } +nym-coconut-dkg-common = { path = "../../../common/cosmwasm-smart-contracts/coconut-dkg" } +nym-multisig-contract-common = { path = "../../../common/cosmwasm-smart-contracts/multisig-contract" } +nym-pemstore = { path = "../../../common/pemstore" } + + +[build-dependencies] +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } diff --git a/tools/internal/testnet-manager/Makefile b/tools/internal/testnet-manager/Makefile new file mode 100644 index 0000000000..74399c646a --- /dev/null +++ b/tools/internal/testnet-manager/Makefile @@ -0,0 +1,2 @@ +build-bypass-contract: + $(MAKE) -C dkg-bypass-contract build \ No newline at end of file diff --git a/tools/internal/testnet-manager/README.md b/tools/internal/testnet-manager/README.md new file mode 100644 index 0000000000..32a5b1dae1 --- /dev/null +++ b/tools/internal/testnet-manager/README.md @@ -0,0 +1,150 @@ +# Testnet manager + +This is extremely experimental tool. Only to be used internally. Expect a lot of breaking changes. + +Currently (as of 11.07.24), it exposes the following commands: + +## `build-info` + +Show build information of this binary. Does it need any more than that? + +## `initialise-new-network` + +pre-requisites: + +1. you must have built all nym-contracts and put them in the same directory (just run `make contracts` from the root + directory) + +Initialises new testnet network: + +1. attempts to retrieve paths to all .wasm files of the nym-contracts based on provided arguments +2. uploads all the contracts to the specified nyxd +3. creates mnemonics for all contract admins +4. transfers some tokens to each created account +5. instantiates all the contracts +6. performs post-instantiation migration (like sets vesting contract address inside the mixnet contract) +7. queries each contract and retrieves its build information to display any warnings if they were built using some + ancient commits +8. persists all the network info (addresses, mnemonics, etc.) in the database for future use + +**note: if you intend to `bond-local-mixnet` afterward, you want to set `--custom-epoch-duration-secs` to a rather low +value (like 60s)** + +## `load-network-details` + +Attempt to load testnet network details using either the provided name, or if nothing was specified, the latest one +created. + +It outputs contents of an `.env` file you'd use with that network. + +## `bypass-dkg` + +pre-requisites: + +1. you must have built the `dkg-bypass contract` (just run `make build-bypass-contract` from **this** directory) + +Attempts to bypass the DKG by overwriting the contract state with pre-generated keys: + +1. generates data for each specified ecash signer: + - ecash keys via a ttp + - ed25519 identity keys + - cosmos mnemonic +2. validates the existing DKG contract to make sure the DKG hasn't actually already been run and checks the group + contract to make sure its empty +3. persists the signer data generated at the beginning +4. uploads the bypass contract +5. overwrites the contract state (endpoints, keys, etc.) using the uploaded contract +6. restores the original DKG contract code +7. adds the ecash signers to the CW4 group +8. transfers some tokens to each ecash signer so they could actually execute txs + +## `initialise-post-dkg-network` + +pre-requisites: + +1. you must have built all nym-contracts and put them in the same directory (just run `make contracts` from the root + directory) +2. you must have built the `dkg-bypass contract` (just run `make build-bypass-contract` from **this** directory) + +Initialises new network and bypasses the DKG. It's just the equivalent of running `initialise-new-network` +and `bypass-dkg` separately: + +1. runs equivalent of `initialise-new-network` +2. runs equivalent of `bypass-dkg` + +## `create-local-ecash-apis` + +pre-requisites: + +1. you must have built all nym-contracts and put them in the same directory (just run `make contracts` from the root + directory) +2. you must have built the `dkg-bypass contract` (just run `make build-bypass-contract` from **this** directory) +3. you must have built `nym-api` binary + +Attempt to create brand new network, in post DKG-state, using locally running nym-apis. + +1. runs equivalent of `initialise-post-dkg-network`, with one difference: rather than requiring you to provide api + endpoints to all signers, it defaults to `http:://127.0.0.1:X`, where `X = 10000 + i`, based on the number of apis + specified in the args +2. runs `nym-api init` for all required api +3. copies over keys generated during `bypass-dkg` into the correct path for each API, +4. generates an `.env` file to use in all subsequent `run` commands +5. generates and outputs (either as raw string or `json` if used with `--output=json`) run commands for each nym-api + using full canonical and absolute paths (so you could paste them regardless of local directory) + +## `bond-local-mixnet` + +pre-requisites: + +1. you must have a running network **including nym-api** (just run `create-local-ecash-apis` and start the binaries) +2. the mixnet epoch must be waiting for transition (thus `--custom-epoch-duration-secs` recommendation) +3. you must have built `nym-node` binary + +Attempt to bond minimal local mixnet (3 mixnodes + 1 gateways) and output the run commands. + +1. runs `nym-node init` 4 times, including once in `mode==entry` (with credentials) +2. generates mnemonics for each node +3. generates bonding signatures for each node +4. transfers some tokens to each bond owner +5. performs bonding of mixnode/gateway +6. assigns all nodes to the active set by: + - starting epoch transition + - reconciling epoch events + - advancing current epoch and assigning the nodes to the set +7. generates and outputs (either as raw string or `json` if used with `--output=json`) run commands for each nym-node + using full canonical and absolute paths (so you could paste them regardless of local directory) + +## `create-local-client` + +pre-requisites: + +1. you must have a running MIXNET **including nym-api AND nym-nodes** (just run `create-local-ecash-apis` followed + by `bond-local-mixnet` and start the binaries) +2. you must have built `nym-client` binary + +Initialise a locally run nym-client, adjust its config and output the run command: + +1. runs `nym-client init` in credentials mode +2. updates its config to add `minimum_mixnode_performance = 0` and `minimum_gateway_performance = 0` thus ignoring the + lack of a network monitor +3. generates and outputs run command for the client using full canonical and absolute paths (so you could paste it + regardless of local directory) + +### Extra + +For reference, my workflow was as follows: + +note: for the very first run you'll have to explicitly provide mnemonics and nyxd + +1. rebuild whichever binary/contract was needed +2. `cargo run -- create-local-ecash-apis --bypass-dkg-contract ../../../target/wasm32-unknown-unknown/release/dkg_bypass_contract.wasm --number-of-apis=2 --nym-api-bin ../../../target/release/nym-api --built-contracts ../../../contracts/target/wasm32-unknown-unknown/release --custom-epoch-duration-secs=60` +3. run the apis in separate terminal window +4. `cargo run -- bond-local-mixnet --nym-node-bin ../../../target/release/nym-node` +5. start all the nym-nodes +6. `cargo run -- create-local-client --nym-client-bin ../../../target/debug/nym-client` +7. usually at this point I was using `nym-cli` to get some ticketbooks into my client before running it with the command + that was output in the previous step + + + + diff --git a/tools/internal/testnet-manager/build.rs b/tools/internal/testnet-manager/build.rs new file mode 100644 index 0000000000..cdd97f9505 --- /dev/null +++ b/tools/internal/testnet-manager/build.rs @@ -0,0 +1,25 @@ +use sqlx::{Connection, SqliteConnection}; +use std::env; + +#[tokio::main] +async fn main() { + let out_dir = env::var("OUT_DIR").unwrap(); + let database_path = format!("{}/nym-api-example.sqlite", out_dir); + + let mut conn = SqliteConnection::connect(&format!("sqlite://{}?mode=rwc", database_path)) + .await + .expect("Failed to create SQLx database connection"); + + sqlx::migrate!("./migrations") + .run(&mut conn) + .await + .expect("Failed to perform SQLx migrations"); + + #[cfg(target_family = "unix")] + println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); + + #[cfg(target_family = "windows")] + // for some strange reason we need to add a leading `/` to the windows path even though it's + // not a valid windows path... but hey, it works... + println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); +} diff --git a/tools/internal/testnet-manager/dkg-bypass-contract/Cargo.toml b/tools/internal/testnet-manager/dkg-bypass-contract/Cargo.toml new file mode 100644 index 0000000000..06ec3f196e --- /dev/null +++ b/tools/internal/testnet-manager/dkg-bypass-contract/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "dkg-bypass-contract" +version = "0.1.0" +edition = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +repository = { workspace = true } + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-storage = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } + +nym-coconut-dkg-common = { path = "../../../../common/cosmwasm-smart-contracts/coconut-dkg" } +nym-contracts-common = { path = "../../../../common/cosmwasm-smart-contracts/contracts-common" } + +[features] +default = ["library"] +library = [] \ No newline at end of file diff --git a/tools/internal/testnet-manager/dkg-bypass-contract/Makefile b/tools/internal/testnet-manager/dkg-bypass-contract/Makefile new file mode 100644 index 0000000000..2aa57e5ef9 --- /dev/null +++ b/tools/internal/testnet-manager/dkg-bypass-contract/Makefile @@ -0,0 +1,5 @@ +all: build + +build: + RUSTFLAGS='-C link-arg=-s' cargo build --release --lib --target wasm32-unknown-unknown + wasm-opt --signext-lowering -O ../../../../target/wasm32-unknown-unknown/release/dkg_bypass_contract.wasm -o ../../../../target/wasm32-unknown-unknown/release/dkg_bypass_contract.wasm \ No newline at end of file diff --git a/tools/internal/testnet-manager/dkg-bypass-contract/src/contract.rs b/tools/internal/testnet-manager/dkg-bypass-contract/src/contract.rs new file mode 100644 index 0000000000..c04fae8ef2 --- /dev/null +++ b/tools/internal/testnet-manager/dkg-bypass-contract/src/contract.rs @@ -0,0 +1,136 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::msg::MigrateMsg; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{ + Addr, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, StdError, StdResult, Storage, +}; +use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex}; +use nym_coconut_dkg_common::dealer::DealerRegistrationDetails; +use nym_coconut_dkg_common::types::{Epoch, EpochId, EpochState, NodeIndex}; +use nym_coconut_dkg_common::verification_key::ContractVKShare; + +pub(crate) type Dealer<'a> = &'a Addr; + +pub(crate) const CURRENT_EPOCH: Item<'_, Epoch> = Item::new("current_epoch"); + +pub const THRESHOLD: Item = Item::new("threshold"); + +pub const EPOCH_THRESHOLDS: Map = Map::new("epoch_thresholds"); + +pub(crate) const NODE_INDEX_COUNTER: Item = Item::new("node_index_counter"); + +// use the same storage types as the actual DKG contract +pub(crate) const DEALERS_INDICES: Map = Map::new("dealer_index"); + +pub(crate) const EPOCH_DEALERS_MAP: Map<(EpochId, Dealer), DealerRegistrationDetails> = + Map::new("epoch_dealers"); + +type VKShareKey<'a> = (&'a Addr, EpochId); + +pub(crate) struct VkShareIndex<'a> { + pub(crate) epoch_id: MultiIndex<'a, EpochId, ContractVKShare, VKShareKey<'a>>, +} + +impl<'a> IndexList for VkShareIndex<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.epoch_id]; + Box::new(v.into_iter()) + } +} + +pub(crate) fn vk_shares<'a>() -> IndexedMap<'a, VKShareKey<'a>, ContractVKShare, VkShareIndex<'a>> { + let indexes = VkShareIndex { + epoch_id: MultiIndex::new(|_pk, d| d.epoch_id, "vksp", "vkse"), + }; + IndexedMap::new("vksp", 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) +} + +#[cw_serde] +pub enum EmptyMessage {} + +#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] +pub fn instantiate( + _: DepsMut<'_>, + _: Env, + _: MessageInfo, + _: EmptyMessage, +) -> Result { + Ok(Response::new()) +} + +/// Handle an incoming message +#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] +pub fn execute( + _: DepsMut<'_>, + _: Env, + _: MessageInfo, + _: EmptyMessage, +) -> Result { + Ok(Response::new()) +} + +#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] +pub fn query(_: Deps<'_>, _: Env, _: EmptyMessage) -> Result { + Ok(Default::default()) +} + +// LIMITATION: we're not storing dealings themselves +#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] +pub fn migrate(deps: DepsMut<'_>, env: Env, msg: MigrateMsg) -> Result { + // on migration immediately attempt to rewrite the storage + let threshold = (2 * msg.dealers.len() as u64 + 3 - 1) / 3; + let epoch = CURRENT_EPOCH.load(deps.storage)?; + assert_eq!(0, epoch.epoch_id); + + // set epoch data + THRESHOLD.save(deps.storage, &threshold)?; + EPOCH_THRESHOLDS.save(deps.storage, 0, &threshold)?; + let duration = epoch + .time_configuration + .state_duration(EpochState::InProgress); + + CURRENT_EPOCH.save( + deps.storage, + &Epoch { + state: EpochState::InProgress, + epoch_id: 0, + state_progress: Default::default(), + time_configuration: epoch.time_configuration, + deadline: duration.map(|d| env.block.time.plus_seconds(d)), + }, + )?; + + // set dealer data + for dealer in msg.dealers { + let node_index = next_node_index(deps.storage)?; + DEALERS_INDICES.save(deps.storage, &dealer.owner, &node_index)?; + + let registration_details = DealerRegistrationDetails { + bte_public_key_with_proof: "fakekey".to_string(), + ed25519_identity: dealer.ed25519_identity, + announce_address: dealer.announce.clone(), + }; + let vk_share = ContractVKShare { + share: dealer.vk, + announce_address: dealer.announce, + node_index, + owner: dealer.owner.clone(), + epoch_id: 0, + verified: true, + }; + + EPOCH_DEALERS_MAP.save(deps.storage, (0, &dealer.owner), ®istration_details)?; + vk_shares().save(deps.storage, (&dealer.owner, 0), &vk_share)?; + } + + Ok(Response::new()) +} diff --git a/tools/internal/testnet-manager/dkg-bypass-contract/src/lib.rs b/tools/internal/testnet-manager/dkg-bypass-contract/src/lib.rs new file mode 100644 index 0000000000..69475aa119 --- /dev/null +++ b/tools/internal/testnet-manager/dkg-bypass-contract/src/lib.rs @@ -0,0 +1,4 @@ +pub mod contract; +pub mod msg; + +pub use msg::MigrateMsg; diff --git a/tools/internal/testnet-manager/dkg-bypass-contract/src/msg.rs b/tools/internal/testnet-manager/dkg-bypass-contract/src/msg.rs new file mode 100644 index 0000000000..fbdc165d69 --- /dev/null +++ b/tools/internal/testnet-manager/dkg-bypass-contract/src/msg.rs @@ -0,0 +1,19 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Addr; +use nym_coconut_dkg_common::verification_key::VerificationKeyShare; + +#[cw_serde] +pub struct FakeDealerData { + pub vk: VerificationKeyShare, + pub ed25519_identity: String, + pub announce: String, + pub owner: Addr, +} + +#[cw_serde] +pub struct MigrateMsg { + pub dealers: Vec, +} diff --git a/tools/internal/testnet-manager/migrations/01_initial_tables.sql b/tools/internal/testnet-manager/migrations/01_initial_tables.sql new file mode 100644 index 0000000000..791a23e07a --- /dev/null +++ b/tools/internal/testnet-manager/migrations/01_initial_tables.sql @@ -0,0 +1,51 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +CREATE TABLE metadata ( + id INTEGER PRIMARY KEY CHECK (id = 0), + latest_network_id INTEGER REFERENCES network(id), + + master_mnemonic TEXT NOT NULL, + rpc_endpoint TEXT NOT NULL +); + +CREATE TABLE network ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + + mixnet_contract_id INTEGER NOT NULL REFERENCES contract(id), + vesting_contract_id INTEGER NOT NULL REFERENCES contract(id), + ecash_contract_id INTEGER NOT NULL REFERENCES contract(id), + cw3_multisig_contract_id INTEGER NOT NULL REFERENCES contract(id), + cw4_group_contract_id INTEGER NOT NULL REFERENCES contract(id), + dkg_contract_id INTEGER NOT NULL REFERENCES contract(id), + + rewarder_address TEXT NOT NULL REFERENCES account(address), + ecash_holding_account_address TEXT NOT NULL REFERENCES account(address) +); + +CREATE TABLE contract ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + address TEXT NOT NULL, + admin_address TEXT NOT NULL REFERENCES account(address) +); + +CREATE TABLE account ( + address TEXT NOT NULL UNIQUE, + -- for the future 'import' feature this will have to be nullable + mnemonic TEXT NOT NULL +); + +CREATE TABLE node ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + identity_key TEXT NOT NULL, + network_id INTEGER NOT NULL REFERENCES network(id), + + -- i.e. mixnode or gateway + bonded_type TEXT NOT NULL, + owner_address TEXT NOT NULL REFERENCES account(address) +); \ No newline at end of file diff --git a/tools/internal/testnet-manager/src/cli/build_info.rs b/tools/internal/testnet-manager/src/cli/build_info.rs new file mode 100644 index 0000000000..ed3d7ecadb --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/build_info.rs @@ -0,0 +1,17 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NetworkManagerError; +use nym_bin_common::bin_info_owned; +use nym_bin_common::output_format::OutputFormat; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) fn execute(args: Args) -> Result<(), NetworkManagerError> { + println!("{}", args.output.format(&bin_info_owned!())); + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/cli/bypass_dkg.rs b/tools/internal/testnet-manager/src/cli/bypass_dkg.rs new file mode 100644 index 0000000000..a938e73d47 --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/bypass_dkg.rs @@ -0,0 +1,50 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::CommonArgs; +use crate::error::NetworkManagerError; +use crate::helpers::default_storage_dir; +use std::path::PathBuf; +use url::Url; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + #[clap(long)] + signer_data_output_directory: Option, + + #[clap(long)] + network_name: Option, + + /// The URLs of that the DKG parties would have put in the contract + #[clap(long, value_delimiter = ',')] + api_endpoints: Vec, + + /// Path to the contract built from the `dkg-bypass-contract` directory + #[clap(long)] + bypass_dkg_contract: PathBuf, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { + let manager = args.common.network_manager().await?; + let network = manager.load_existing_network(args.network_name).await?; + + let signer_data_output_directory = if let Some(explicit) = args.signer_data_output_directory { + explicit + } else { + default_storage_dir().join(&network.name) + }; + + manager + .attempt_bypass_dkg( + args.api_endpoints, + &network, + args.bypass_dkg_contract, + signer_data_output_directory, + ) + .await?; + + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/cli/initialise_new_network.rs b/tools/internal/testnet-manager/src/cli/initialise_new_network.rs new file mode 100644 index 0000000000..574559bf82 --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/initialise_new_network.rs @@ -0,0 +1,49 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::CommonArgs; +use crate::error::NetworkManagerError; +use crate::manager::env::Env; +use nym_bin_common::output_format::OutputFormat; +use std::path::PathBuf; +use std::time::Duration; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + /// Path containing .wasm files of all contracts + #[clap(long)] + built_contracts: PathBuf, + + #[clap(long)] + network_name: Option, + + /// Specifies custom duration of mixnet epochs + /// It's recommended to set it to rather low value (like 60s) if you intend to bond the mixnet afterward. + #[clap(long)] + custom_epoch_duration_secs: Option, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { + let network = args + .common + .network_manager() + .await? + .initialise_new_network( + args.built_contracts, + args.network_name, + args.custom_epoch_duration_secs.map(Duration::from_secs), + ) + .await? + .into_loaded(); + + let env = Env::from(&network); + println!("add the following to your .env file: \n{env}",); + + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/cli/initialise_post_dkg_network.rs b/tools/internal/testnet-manager/src/cli/initialise_post_dkg_network.rs new file mode 100644 index 0000000000..d302177fad --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/initialise_post_dkg_network.rs @@ -0,0 +1,78 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::CommonArgs; +use crate::error::NetworkManagerError; +use crate::helpers::default_storage_dir; +use crate::manager::env::Env; +use crate::manager::network::LoadedNetwork; +use nym_bin_common::output_format::OutputFormat; +use std::path::PathBuf; +use std::time::Duration; +use url::Url; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + /// Path containing .wasm files of all contracts + #[clap(long)] + built_contracts: PathBuf, + + #[clap(long)] + network_name: Option, + + #[clap(long)] + signer_data_output_directory: Option, + + /// The URLs of that the DKG parties would have put in the contract + #[clap(long, value_delimiter = ',')] + api_endpoints: Vec, + + /// Path to the contract built from the `dkg-bypass-contract` directory + #[clap(long)] + bypass_dkg_contract: PathBuf, + + /// Specifies custom duration of mixnet epochs + /// It's recommended to set it to rather low value (like 60s) if you intend to bond the mixnet afterward. + #[clap(long)] + custom_epoch_duration_secs: Option, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { + let manager = args.common.network_manager().await?; + + let network: LoadedNetwork = manager + .initialise_new_network( + args.built_contracts, + args.network_name, + args.custom_epoch_duration_secs.map(Duration::from_secs), + ) + .await? + .into(); + + let signer_data_output_directory = if let Some(explicit) = args.signer_data_output_directory { + explicit + } else { + default_storage_dir().join(&network.name) + }; + + let env = Env::from(&network); + + manager + .attempt_bypass_dkg( + args.api_endpoints, + &network, + args.bypass_dkg_contract, + signer_data_output_directory, + ) + .await?; + + println!("add the following to your .env file: \n{env}",); + + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/cli/load_network_details.rs b/tools/internal/testnet-manager/src/cli/load_network_details.rs new file mode 100644 index 0000000000..657b68ce1a --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/load_network_details.rs @@ -0,0 +1,35 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NetworkManagerError; +use crate::helpers::default_db_file; +use crate::manager::env::Env; +use crate::manager::NetworkManager; +use nym_bin_common::output_format::OutputFormat; +use std::path::PathBuf; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(long)] + network_name: Option, + + #[clap(long)] + storage_path: Option, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { + let storage = args.storage_path.unwrap_or_else(default_db_file); + + let network = NetworkManager::new(storage, None, None) + .await? + .load_existing_network(args.network_name) + .await?; + + let env = Env::from(&network); + println!("add the following to your .env file: \n{env}",); + + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/cli/local_client.rs b/tools/internal/testnet-manager/src/cli/local_client.rs new file mode 100644 index 0000000000..25a55f1253 --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/local_client.rs @@ -0,0 +1,41 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CommonArgs; +use crate::error::NetworkManagerError; +use nym_bin_common::output_format::OutputFormat; +use std::path::PathBuf; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + /// Path to the `nym-client` binary + #[clap(long)] + nym_client_bin: PathBuf, + + #[clap(long)] + gateway: Option, + + #[clap(long)] + network_name: Option, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { + let manager = args.common.network_manager().await?; + let network = manager.load_existing_network(args.network_name).await?; + + let run_cmd = manager + .init_local_nym_client(args.nym_client_bin, &network, args.gateway) + .await?; + + if !args.output.is_text() { + args.output.to_stderr(&run_cmd) + } + + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/cli/local_ecash_apis.rs b/tools/internal/testnet-manager/src/cli/local_ecash_apis.rs new file mode 100644 index 0000000000..f0f9a4475a --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/local_ecash_apis.rs @@ -0,0 +1,80 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CommonArgs; +use crate::error::NetworkManagerError; +use crate::manager::network::LoadedNetwork; +use nym_bin_common::output_format::OutputFormat; +use std::path::PathBuf; +use std::time::Duration; +use tempfile::tempdir; +use url::Url; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + /// Path to the `nym-api` binary + #[clap(long)] + nym_api_bin: PathBuf, + + /// Path containing .wasm files of all contracts + #[clap(long)] + built_contracts: PathBuf, + + #[clap(long)] + number_of_apis: usize, + + #[clap(long)] + network_name: Option, + + /// Path to the contract built from the `dkg-bypass-contract` directory + #[clap(long)] + bypass_dkg_contract: PathBuf, + + /// Specifies custom duration of mixnet epochs + #[clap(long)] + custom_epoch_duration_secs: Option, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { + let endpoints = (0..args.number_of_apis) + .map(|i| format!("http://127.0.0.1:{}", 10000 + i).parse().unwrap()) + .collect::>(); + + let manager = args.common.network_manager().await?; + + let network: LoadedNetwork = manager + .initialise_new_network( + args.built_contracts, + args.network_name, + args.custom_epoch_duration_secs.map(Duration::from_secs), + ) + .await? + .into(); + + let temp_output = tempdir()?; + + let signer_details = manager + .attempt_bypass_dkg( + endpoints, + &network, + args.bypass_dkg_contract, + temp_output.path(), + ) + .await?; + + let run_cmds = manager + .setup_local_apis(args.nym_api_bin, &network, signer_details) + .await?; + + if !args.output.is_text() { + args.output.to_stderr(&run_cmds) + } + + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/cli/local_nodes.rs b/tools/internal/testnet-manager/src/cli/local_nodes.rs new file mode 100644 index 0000000000..d84b9a83e5 --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/local_nodes.rs @@ -0,0 +1,44 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CommonArgs; +use crate::error::NetworkManagerError; +use nym_bin_common::output_format::OutputFormat; +use std::path::PathBuf; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + /// Path to the `nym-node` binary + #[clap(long)] + nym_node_bin: PathBuf, + + #[clap(long, default_value_t = 3)] + mixnodes: u16, + + #[clap(long, default_value_t = 1)] + gateways: u16, + + #[clap(long)] + network_name: Option, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { + let manager = args.common.network_manager().await?; + let network = manager.load_existing_network(args.network_name).await?; + + let run_cmds = manager + .init_local_nym_nodes(args.nym_node_bin, &network, args.mixnodes, args.gateways) + .await?; + + if !args.output.is_text() { + args.output.to_stderr(&run_cmds) + } + + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/cli/migrate.rs b/tools/internal/testnet-manager/src/cli/migrate.rs new file mode 100644 index 0000000000..13075aca1a --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/migrate.rs @@ -0,0 +1,20 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NetworkManagerError; +use clap::Parser; +use nym_validator_client::nyxd::cosmwasm_client::types::{ContractCodeId, EmptyMsg}; + +// nyxd-style command so, for example `migrate ecash 123 '{}'` +#[derive(Debug, Parser)] +pub(crate) struct Args { + pub contract_name: String, + + pub code_id: ContractCodeId, + + pub message: serde_json::Value, +} + +pub(crate) fn execute(args: Args) -> Result<(), NetworkManagerError> { + todo!() +} diff --git a/tools/internal/testnet-manager/src/cli/mod.rs b/tools/internal/testnet-manager/src/cli/mod.rs new file mode 100644 index 0000000000..b8d143ae95 --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/mod.rs @@ -0,0 +1,108 @@ +use std::path::PathBuf; +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only +use crate::error::NetworkManagerError; +use crate::helpers::default_db_file; +use crate::manager::NetworkManager; +use clap::{Parser, Subcommand}; +use nym_bin_common::bin_info; +use std::sync::OnceLock; +use url::Url; + +mod build_info; +mod bypass_dkg; +mod initialise_new_network; +mod initialise_post_dkg_network; +mod load_network_details; +mod local_client; +mod local_ecash_apis; +mod local_nodes; +// mod migrate; + +#[derive(clap::Args, Debug)] +pub(crate) struct CommonArgs { + #[clap(long)] + master_mnemonic: Option, + + #[clap(long)] + rpc_endpoint: Option, + + #[clap(long)] + storage_path: Option, +} + +impl CommonArgs { + pub(crate) async fn network_manager(self) -> Result { + let storage = self.storage_path.unwrap_or_else(default_db_file); + NetworkManager::new(storage, self.master_mnemonic, self.rpc_endpoint).await + } +} + +// Helper for passing LONG_VERSION to clap +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Parser, Debug)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub(crate) struct Cli { + #[clap(subcommand)] + command: Commands, +} + +impl Cli { + pub(crate) async fn execute(self) -> Result<(), NetworkManagerError> { + match self.command { + Commands::BuildInfo(args) => build_info::execute(args), + Commands::InitialiseNewNetwork(args) => initialise_new_network::execute(args).await, + Commands::LoadNetworkDetails(args) => load_network_details::execute(args).await, + Commands::BypassDkg(args) => bypass_dkg::execute(args).await, + Commands::InitialisePostDkgNetwork(args) => { + initialise_post_dkg_network::execute(args).await + } + Commands::CreateLocalEcashApis(args) => local_ecash_apis::execute(args).await, + Commands::BondLocalMixnet(args) => local_nodes::execute(args).await, + Commands::CreateLocalClient(args) => local_client::execute(args).await, + } + } +} + +#[derive(Subcommand, Debug)] +pub(crate) enum Commands { + /// Show build information of this binary + BuildInfo(build_info::Args), + + /// Initialise new testnet network + InitialiseNewNetwork(initialise_new_network::Args), + + /// Attempt to load testnet network details + LoadNetworkDetails(load_network_details::Args), + + /// Attempt to bypass the DKG by overwriting the contract state with pre-generated keys + BypassDkg(bypass_dkg::Args), + + /// Initialise new network and bypass the DKG. + /// Equivalent of running `initialise-new-network` and `bypass-dkg` separately. + InitialisePostDkgNetwork(initialise_post_dkg_network::Args), + + /// Attempt to create brand new network, in post DKG-state, using locally running nym-apis + CreateLocalEcashApis(local_ecash_apis::Args), + + /// Attempt to bond minimal local mixnet (3 mixnodes + 1 gateways) and output the run commands + BondLocalMixnet(local_nodes::Args), + + /// Initialise a locally run nym-client, adjust its config and output the run command + CreateLocalClient(local_client::Args), +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::CommandFactory; + + #[test] + fn verify_cli() { + Cli::command().debug_assert(); + } +} diff --git a/tools/internal/testnet-manager/src/error.rs b/tools/internal/testnet-manager/src/error.rs new file mode 100644 index 0000000000..d85d8c7dcb --- /dev/null +++ b/tools/internal/testnet-manager/src/error.rs @@ -0,0 +1,106 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only +use nym_compact_ecash::CompactEcashError; +use nym_validator_client::nyxd::error::NyxdError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum NetworkManagerError { + #[error("io error: {0}")] + IoError(#[from] std::io::Error), + + #[error("failed to parse mnemonic: {0}")] + Bip39Error(#[from] bip39::Error), + + #[error("failed to parse the url: {0}")] + MalformedUrl(#[from] url::ParseError), + + #[error("one of the account addresses was malformed - the developer was too lazy to propagate the actual error message with the address")] + MalformedAccountAddress, + + #[error(transparent)] + Nyxd(#[from] NyxdError), + + #[error("you need to set the master mnemonic on initial run")] + MnemonicNotSet, + + #[error("you need to set the rpc endpoint on initial run")] + RpcEndpointNotSet, + + #[error("experienced internal database error: {0}")] + InternalDatabaseError(#[from] sqlx::Error), + + #[error("failed to perform startup SQL migration - {0}")] + StartupMigrationFailure(#[from] sqlx::migrate::MigrateError), + + #[error("could not find .wasm file for {name} contract under the provided directory")] + ContractWasmNotFound { name: String }, + + #[error("could not find code_id for {name} contract")] + ContractNotUploaded { name: String }, + + #[error("could not find contract admin for {name} contract")] + ContractAdminNotSet { name: String }, + + #[error("could not find address for {name} contract")] + ContractNotInitialised { name: String }, + + #[error("could not find build information for {name} contract")] + ContractNotQueried { name: String }, + + #[error("contract {name} has been build before build information got standarised. this is not supported")] + MissingBuildInfo { name: String }, + + #[error("there aren't any initialised networks in the storage")] + NoNetworksInitialised, + + #[error("you must specify at least a single api endpoint for the DKG")] + NoApiEndpoints, + + #[error("the DKG process has already been started on the target network")] + DkgAlreadyStarted, + + #[error("the target network is already in non-zero DKG epoch")] + NonZeroEpoch, + + #[error("the target already has registered cw4 members")] + ExistingCW4Members, + + #[error("failed to compute ecash keys: {source}")] + EcashCryptoFailure { + #[from] + source: CompactEcashError, + }, + + #[error("the provided contract path does not point to a valid .wasm file")] + MalformedDkgBypassContractPath, + + #[error("nym api initialisation returned non-zero return code")] + NymApiExecutionFailure, + + #[error("nym node initialisation returned non-zero return code")] + NymNodeExecutionFailure, + + #[error("nym client initialisation returned non-zero return code")] + NymClientExecutionFailure, + + #[error("failed to deserialise nym-api config: {0}")] + TomlDeserialisationFailure(#[from] toml::de::Error), + + #[error("failed to deserialise nym-node output: {0}")] + JsonDeserialisationFailure(#[from] serde_json::Error), + + #[error( + "the corresponding env file hasn't been generated. you need to setup local apis first." + )] + EnvFileNotGenerated, + + #[error("the default, pre-generated, .env file does not have the nym-api endpoint set!")] + NymApiEndpointMissing, + + #[error("timed out while waiting for some gateway to appear in the directory (you don't need to run it)")] + ApiGatewayWaitTimeout, + + #[error("timed out while waiting for the gateway to start receiving traffic (you need to actually run it!)")] + GatewayWaitTimeout, +} diff --git a/tools/internal/testnet-manager/src/helpers.rs b/tools/internal/testnet-manager/src/helpers.rs new file mode 100644 index 0000000000..0c38c8238d --- /dev/null +++ b/tools/internal/testnet-manager/src/helpers.rs @@ -0,0 +1,153 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NetworkManagerError; +use indicatif::{HumanDuration, ProgressBar}; +use nym_config::{must_get_home, NYM_DIR}; +use serde::{Deserialize, Serialize}; +use std::borrow::Cow; +use std::fmt::{Display, Formatter}; +use std::future::Future; +use std::io::Read; +use std::path::{Path, PathBuf}; +use std::time::{Duration, Instant}; +use tokio::pin; +use tokio::time::interval; + +// struct Ctx<'a, T> { +// progress: ProgressTracker, +// network: LoadedNetwork<'a>, +// inner: T, +// } + +pub(crate) trait ProgressCtx { + fn progress_tracker(&self) -> &ProgressTracker; + + fn println>(&self, msg: I) { + self.progress_tracker().println(msg) + } + + fn set_pb_prefix(&self, prefix: impl Into>) { + self.progress_tracker().set_pb_prefix(prefix) + } + + fn set_pb_message(&self, msg: impl Into>) { + self.progress_tracker().set_pb_message(msg) + } + + async fn async_with_progress(&self, fut: F) -> T + where + F: Future, + { + async_with_progress(fut, &self.progress_tracker().progress_bar).await + } +} + +// pub(crate) trait NetworkCtx { +// fn loaded_network(&self) -> &LoadedNetwork; +// } + +#[derive(Serialize, Deserialize)] +pub struct RunCommands(pub(crate) Vec); + +impl Display for RunCommands { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + for cmd in &self.0 { + writeln!(f, "{cmd}")? + } + Ok(()) + } +} + +pub(crate) struct ProgressTracker { + start: Instant, + pub(crate) progress_bar: ProgressBar, +} + +impl ProgressTracker { + pub(crate) fn new>(msg: I) -> Self { + let progress_bar = ProgressBar::new_spinner(); + progress_bar.println(msg); + + ProgressTracker { + start: Instant::now(), + progress_bar, + } + } + + pub(crate) fn println>(&self, msg: I) { + self.progress_bar.println(msg) + } + + pub(crate) fn set_pb_prefix(&self, prefix: impl Into>) { + self.progress_bar.set_prefix(prefix) + } + + pub(crate) fn set_pb_message(&self, msg: impl Into>) { + self.progress_bar.set_message(msg) + } + + pub(crate) fn output_run_commands(&self, cmds: &RunCommands) { + self.println("🏇 run the binaries with the following commands:"); + for cmd in &cmds.0 { + self.println(cmd) + } + } +} + +impl Default for ProgressTracker { + fn default() -> Self { + ProgressTracker { + start: Instant::now(), + progress_bar: ProgressBar::new_spinner(), + } + } +} + +impl Drop for ProgressTracker { + fn drop(&mut self) { + self.progress_bar.println(format!( + "✨ Done in {}", + HumanDuration(self.start.elapsed()) + )); + self.progress_bar.finish_and_clear(); + } +} + +pub(crate) fn default_storage_dir() -> PathBuf { + must_get_home().join(NYM_DIR).join("testnet-manager") +} + +pub(crate) fn default_db_file() -> PathBuf { + default_storage_dir().join("network-data.sqlite") +} + +pub(crate) async fn async_with_progress(fut: F, pb: &ProgressBar) -> T +where + F: Future, +{ + pb.tick(); + pin!(fut); + let mut update_interval = interval(Duration::from_millis(50)); + + loop { + tokio::select! { + _ = update_interval.tick() => { + pb.tick() + } + res = &mut fut => { + return res + } + } + } +} + +pub(crate) fn wasm_code>(path: P) -> Result, NetworkManagerError> { + let path = path.as_ref(); + assert!(path.exists()); + let mut file = std::fs::File::open(path)?; + let mut data = Vec::new(); + + file.read_to_end(&mut data)?; + Ok(data) +} diff --git a/tools/internal/testnet-manager/src/main.rs b/tools/internal/testnet-manager/src/main.rs new file mode 100644 index 0000000000..49d93a46dc --- /dev/null +++ b/tools/internal/testnet-manager/src/main.rs @@ -0,0 +1,39 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +// Allow dead code for not(unix) +#![cfg_attr(not(unix), allow(dead_code))] + +#[cfg(unix)] +pub(crate) mod cli; +#[cfg(unix)] +pub(crate) mod error; +#[cfg(unix)] +mod helpers; +#[cfg(unix)] +mod manager; + +#[cfg(unix)] +#[tokio::main] +async fn main() -> anyhow::Result<()> { + use crate::cli::Cli; + use clap::Parser; + use nym_bin_common::logging::setup_tracing_logger; + + // std::env::set_var( + // "RUST_LOG", + // "trace,handlebars=warn,tendermint_rpc=warn,h2=warn,hyper=warn,rustls=warn,reqwest=warn,tungstenite=warn,async_tungstenite=warn,tokio_util=warn,tokio_tungstenite=warn,tokio-util=warn", + // ); + + let cli = Cli::parse(); + setup_tracing_logger(); + + cli.execute().await?; + + Ok(()) +} + +#[cfg(not(unix))] +fn main() { + println!("This binary is only supported on Unix systems"); +} diff --git a/tools/internal/testnet-manager/src/manager/contract.rs b/tools/internal/testnet-manager/src/manager/contract.rs new file mode 100644 index 0000000000..8295550dfc --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/contract.rs @@ -0,0 +1,283 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only +use crate::error::NetworkManagerError; +use nym_mixnet_contract_common::ContractBuildInformation; +use nym_validator_client::nyxd::cosmwasm_client::types::{ + ContractCodeId, InstantiateResult, MigrateResult, UploadResult, +}; +use nym_validator_client::nyxd::{AccountId, Hash}; +use nym_validator_client::DirectSecp256k1HdWallet; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct LoadedNymContracts { + pub(crate) mixnet: LoadedContract, + pub(crate) vesting: LoadedContract, + pub(crate) ecash: LoadedContract, + pub(crate) cw3_multisig: LoadedContract, + pub(crate) cw4_group: LoadedContract, + pub(crate) dkg: LoadedContract, +} + +impl From for LoadedNymContracts { + fn from(value: NymContracts) -> Self { + LoadedNymContracts { + mixnet: value.mixnet.into(), + vesting: value.vesting.into(), + ecash: value.ecash.into(), + cw3_multisig: value.cw3_multisig.into(), + cw4_group: value.cw4_group.into(), + dkg: value.dkg.into(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct NymContracts { + pub(crate) mixnet: Contract, + pub(crate) vesting: Contract, + pub(crate) ecash: Contract, + pub(crate) cw3_multisig: Contract, + pub(crate) cw4_group: Contract, + pub(crate) dkg: Contract, +} + +impl NymContracts { + pub(crate) fn fake_iter(&self) -> Vec<&Contract> { + vec![ + &self.mixnet, + &self.vesting, + &self.ecash, + &self.cw3_multisig, + &self.cw4_group, + &self.dkg, + ] + } + + pub(crate) fn fake_iter_mut(&mut self) -> Vec<&mut Contract> { + vec![ + &mut self.mixnet, + &mut self.vesting, + &mut self.ecash, + &mut self.cw3_multisig, + &mut self.cw4_group, + &mut self.dkg, + ] + } + + pub(crate) fn count(&self) -> usize { + 6 + } + + pub(crate) fn discover_paths>( + &mut self, + base_path: P, + ) -> Result<(), NetworkManagerError> { + // just look in the base path, don't traverse + for entry_res in base_path.as_ref().read_dir()? { + let entry = entry_res?; + let Ok(name) = entry.file_name().into_string() else { + continue; + }; + + if name.ends_with(".wasm") { + if name.contains("mixnet") { + self.mixnet.wasm_path = Some(entry.path()) + } + if name.contains("vesting") { + self.vesting.wasm_path = Some(entry.path()) + } + if name.contains("ecash") { + self.ecash.wasm_path = Some(entry.path()) + } + if name.contains("cw4") { + self.cw4_group.wasm_path = Some(entry.path()) + } + if name.contains("cw3") { + self.cw3_multisig.wasm_path = Some(entry.path()) + } + if name.contains("dkg") { + self.dkg.wasm_path = Some(entry.path()) + } + } + } + + if let Some(no_path) = self.fake_iter().iter().find(|c| c.wasm_path.is_none()) { + return Err(NetworkManagerError::ContractWasmNotFound { + name: no_path.name.clone(), + }); + } + + Ok(()) + } +} + +impl Default for NymContracts { + fn default() -> Self { + NymContracts { + mixnet: Contract::new("mixnet"), + vesting: Contract::new("vesting"), + ecash: Contract::new("ecash"), + cw4_group: Contract::new("cw4_group"), + cw3_multisig: Contract::new("cw3_multisig"), + dkg: Contract::new("dkg"), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct Account { + pub(crate) address: AccountId, + pub(crate) mnemonic: bip39::Mnemonic, +} + +impl Account { + pub(crate) fn new() -> Account { + let mnemonic = bip39::Mnemonic::generate(24).unwrap(); + // sure, we're using hardcoded prefix, but realistically this will never change + let wallet = DirectSecp256k1HdWallet::from_mnemonic("n", mnemonic.clone()); + let acc = wallet.try_derive_accounts().unwrap().pop().unwrap(); + Account { + address: acc.address, + mnemonic, + } + } + + pub(crate) fn address(&self) -> AccountId { + self.address.clone() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct MinimalUploadInfo { + pub transaction_hash: Hash, + pub code_id: ContractCodeId, +} + +impl From for MinimalUploadInfo { + fn from(value: UploadResult) -> Self { + MinimalUploadInfo { + transaction_hash: value.transaction_hash, + code_id: value.code_id, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct MinimalInitInfo { + pub transaction_hash: Hash, + pub contract_address: AccountId, +} + +impl From for MinimalInitInfo { + fn from(value: InstantiateResult) -> Self { + MinimalInitInfo { + transaction_hash: value.transaction_hash, + contract_address: value.contract_address, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct MinimalMigrateInfo { + pub transaction_hash: Hash, +} + +impl From for MinimalMigrateInfo { + fn from(value: MigrateResult) -> Self { + MinimalMigrateInfo { + transaction_hash: value.transaction_hash, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct LoadedContract { + pub(crate) name: String, + pub(crate) address: AccountId, + pub(crate) admin_address: AccountId, + pub(crate) admin_mnemonic: bip39::Mnemonic, +} + +impl From for LoadedContract { + fn from(value: Contract) -> Self { + let admin = value.admin.expect("no admin set"); + LoadedContract { + name: value.name, + address: value.init_info.expect("uninitialised").contract_address, + admin_address: admin.address, + admin_mnemonic: admin.mnemonic, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct Contract { + pub(crate) name: String, + pub(crate) wasm_path: Option, + pub(crate) upload_info: Option, + pub(crate) admin: Option, + pub(crate) init_info: Option, + pub(crate) migrate_info: Option, + pub(crate) build_info: Option, +} + +impl Contract { + pub(crate) fn new>(name: S) -> Self { + Contract { + name: name.into(), + wasm_path: None, + upload_info: None, + admin: None, + init_info: None, + migrate_info: None, + build_info: None, + } + } + + pub(crate) fn wasm_path(&self) -> Result<&PathBuf, NetworkManagerError> { + self.wasm_path + .as_ref() + .ok_or_else(|| NetworkManagerError::ContractWasmNotFound { + name: self.name.clone(), + }) + } + + pub(crate) fn upload_info(&self) -> Result<&MinimalUploadInfo, NetworkManagerError> { + self.upload_info + .as_ref() + .ok_or_else(|| NetworkManagerError::ContractNotUploaded { + name: self.name.clone(), + }) + } + + pub(crate) fn admin(&self) -> Result<&Account, NetworkManagerError> { + self.admin + .as_ref() + .ok_or_else(|| NetworkManagerError::ContractAdminNotSet { + name: self.name.clone(), + }) + } + + pub(crate) fn init_info(&self) -> Result<&MinimalInitInfo, NetworkManagerError> { + self.init_info + .as_ref() + .ok_or_else(|| NetworkManagerError::ContractNotInitialised { + name: self.name.clone(), + }) + } + + #[allow(dead_code)] + pub(crate) fn build_info(&self) -> Result<&ContractBuildInformation, NetworkManagerError> { + self.build_info + .as_ref() + .ok_or_else(|| NetworkManagerError::ContractNotQueried { + name: self.name.clone(), + }) + } + + pub(crate) fn address(&self) -> Result<&AccountId, NetworkManagerError> { + self.init_info().map(|info| &info.contract_address) + } +} diff --git a/tools/internal/testnet-manager/src/manager/dkg_skip.rs b/tools/internal/testnet-manager/src/manager/dkg_skip.rs new file mode 100644 index 0000000000..d09e219228 --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/dkg_skip.rs @@ -0,0 +1,473 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NetworkManagerError; +use crate::helpers::{ProgressCtx, ProgressTracker}; +use crate::manager::contract::Account; +use crate::manager::network::LoadedNetwork; +use crate::manager::NetworkManager; +use console::style; +use dkg_bypass_contract::msg::FakeDealerData; +use nym_compact_ecash::{ttp_keygen, Base58, KeyPairAuth}; +use nym_crypto::asymmetric::ed25519; +use nym_mixnet_contract_common::Addr; +use nym_pemstore::traits::PemStorableKey; +use nym_pemstore::{store_key, store_keypair, KeyPairPath}; +use nym_validator_client::nyxd::contract_traits::{ + DkgQueryClient, GroupSigningClient, PagedGroupQueryClient, +}; +use nym_validator_client::nyxd::cosmwasm::ContractCodeId; +use nym_validator_client::nyxd::cw4::Member; +use nym_validator_client::nyxd::{AccountId, CosmWasmClient}; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use rand::rngs::OsRng; +use std::fs; +use std::ops::Deref; +use std::path::{Path, PathBuf}; +use url::Url; +use zeroize::Zeroizing; + +pub(crate) struct EcashSigner { + pub(crate) ed25519_keypair: ed25519::KeyPair, + pub(crate) ecash_keypair: nym_compact_ecash::KeyPairAuth, + pub(crate) cosmos_account: Account, + pub(crate) endpoint: Url, +} + +#[derive(Default)] +pub(crate) struct EcashSignerPaths { + pub(crate) ecash_key: PathBuf, + pub(crate) ed25519_keys: KeyPairPath, + pub(crate) mnemonic_path: PathBuf, + pub(crate) endpoint_path: PathBuf, +} + +pub(crate) struct EcashSignerWithPaths { + pub(crate) data: EcashSigner, + pub(crate) paths: EcashSignerPaths, +} + +// perform the same serialisation as the nym-api keys +struct FakeDkgKey<'a> { + inner: &'a KeyPairAuth, +} + +impl<'a> FakeDkgKey<'a> { + fn new(inner: &'a KeyPairAuth) -> Self { + FakeDkgKey { inner } + } +} + +impl<'a> PemStorableKey for FakeDkgKey<'a> { + type Error = NetworkManagerError; + + fn pem_type() -> &'static str { + "ECASH KEY WITH EPOCH" + } + + fn to_bytes(&self) -> Vec { + // our fake key is ALWAYS issued for epoch 0 + let mut bytes = vec![0u8; 8]; + bytes.append(&mut self.inner.secret_key().to_bytes()); + bytes + } + + fn from_bytes(_: &[u8]) -> Result { + unimplemented!("this is not meant to be ever called") + } +} + +impl EcashSignerWithPaths { + pub(crate) fn api_port(&self) -> u16 { + self.data.endpoint.port().unwrap() + } +} + +struct DkgSkipCtx<'a> { + progress: ProgressTracker, + network: &'a LoadedNetwork, + dkg_admin: DirectSigningHttpRpcNyxdClient, + ecash_signers: Vec, +} + +impl<'a> ProgressCtx for DkgSkipCtx<'a> { + fn progress_tracker(&self) -> &ProgressTracker { + &self.progress + } +} + +impl<'a> DkgSkipCtx<'a> { + fn dkg_contract(&self) -> &AccountId { + &self.network.contracts.dkg.address + } + + fn new(network: &'a LoadedNetwork) -> Result { + let progress = ProgressTracker::new(format!( + "\n🥷 attempting to skip DKG on network '{}'", + network.name + )); + + Ok(DkgSkipCtx { + progress, + dkg_admin: network.dkg_signing_client()?, + network, + ecash_signers: vec![], + }) + } + + fn group_signing_client(&self) -> Result { + self.network.cw4_group_signing_client() + } + + fn admin_signing_client( + &self, + mnemonic: bip39::Mnemonic, + ) -> Result { + Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + self.network.client_config()?, + self.network.rpc_endpoint.as_str(), + mnemonic, + )?) + } +} + +impl NetworkManager { + fn generate_ecash_signer_data( + &self, + ctx: &mut DkgSkipCtx, + api_endpoints: Vec, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "📝 {}Generating ecash keys for all signers...", + style("[1/8]").bold().dim() + )); + + // generate required materials + let n = api_endpoints.len(); + let threshold = (2 * n + 3 - 1) / 3; + + let ecash_keys = ttp_keygen(threshold as u64, n as u64)?; + + let mut ecash_signers = Vec::new(); + let mut rng = OsRng; + for (endpoint, ecash_keypair) in api_endpoints.into_iter().zip(ecash_keys.into_iter()) { + let ed25519_keypair = ed25519::KeyPair::new(&mut rng); + let data = EcashSigner { + ed25519_keypair, + ecash_keypair, + cosmos_account: Account::new(), + endpoint, + }; + ctx.println(format!( + "\t{} will be managed by {}", + data.endpoint, data.cosmos_account.address + )); + let full = EcashSignerWithPaths { + data, + paths: EcashSignerPaths::default(), + }; + ecash_signers.push(full) + } + ctx.ecash_signers = ecash_signers; + + ctx.println("\t✅ generated ecash keys for all signers"); + Ok(()) + } + + async fn validate_existing_contracts<'a>( + &self, + ctx: &DkgSkipCtx<'a>, + ) -> Result { + ctx.println(format!( + "🔬 {}Validating the current DKG and group contracts...", + style("[2/8]").bold().dim() + )); + + ctx.set_pb_prefix("[1/3]"); + ctx.set_pb_message("checking DKG epoch data..."); + let epoch_fut = ctx.dkg_admin.get_current_epoch(); + let dkg_epoch = ctx.async_with_progress(epoch_fut).await?; + if dkg_epoch.epoch_id != 0 { + return Err(NetworkManagerError::NonZeroEpoch); + } + + if !dkg_epoch.state.is_waiting_initialisation() { + return Err(NetworkManagerError::DkgAlreadyStarted); + } + + ctx.set_pb_prefix("[2/3]"); + ctx.set_pb_message("retrieving DKG contract code_id..."); + let code_fut = ctx + .dkg_admin + .get_contract_code_history(&ctx.network.contracts.dkg.address); + let code_history = ctx.async_with_progress(code_fut).await?; + + // SAFETY: + // if this is empty our abci query is invalid since we have just queried the contract so it must exist + let current_code = code_history.last().unwrap().code_id; + ctx.println("\tthe DKG contract is all good!"); + + ctx.set_pb_prefix("[3/3]"); + ctx.set_pb_message("checking cw4 group members data..."); + let members_fut = ctx.dkg_admin.get_all_members(); + let members = ctx.async_with_progress(members_fut).await?; + if !members.is_empty() { + return Err(NetworkManagerError::ExistingCW4Members); + } + + ctx.println("\tthe group contract is all good!"); + ctx.println("\t✅ the existing contracts are all good!"); + + Ok(current_code) + } + + async fn persist_dkg_keys<'a, P: AsRef>( + &self, + ctx: &mut DkgSkipCtx<'a>, + output_dir: P, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "📦 {}Persisting the signer keys...", + style("[3/8]").bold().dim() + )); + + ctx.set_pb_message("storing the signer data on disk..."); + + let output_dir = output_dir.as_ref(); + let pb = &ctx.progress.progress_bar; + + for signer in &mut ctx.ecash_signers { + let address = &signer.data.cosmos_account.address; + let url = &signer.data.endpoint; + let signer_dir = output_dir.join(address.to_string()); + fs::create_dir_all(&signer_dir)?; + + let fake_ecash_key = FakeDkgKey::new(&signer.data.ecash_keypair); + + let ecash_path = signer_dir.join("ecash"); + + let ed25519_paths = KeyPairPath { + private_key_path: signer_dir.join("ed25519"), + public_key_path: signer_dir.join("ed25519.pub"), + }; + + let mnemonic_path = signer_dir.join("mnemonic"); + let endpoint_path = signer_dir.join("announce_address"); + + store_key(&fake_ecash_key, &ecash_path)?; + store_keypair(&signer.data.ed25519_keypair, &ed25519_paths)?; + + fs::write( + &mnemonic_path, + Zeroizing::new(signer.data.cosmos_account.mnemonic.to_string()), + )?; + fs::write(&endpoint_path, url.as_str())?; + + signer.paths.ecash_key = ecash_path; + signer.paths.ed25519_keys = ed25519_paths; + signer.paths.mnemonic_path = mnemonic_path; + signer.paths.endpoint_path = endpoint_path; + + pb.println(format!( + "\tpersisted {address} (endpoint: {url}) data under {}", + signer_dir.display() + )); + } + + ctx.println("\t✅ persisted all the signer keys!"); + Ok(()) + } + + async fn upload_bypass_contract<'a, P: AsRef>( + &self, + ctx: &DkgSkipCtx<'a>, + dkg_bypass_contract: P, + ) -> Result { + ctx.println(format!( + "🚚 {}Uploading the bypass contract...", + style("[4/8]").bold().dim() + )); + + ctx.set_pb_message("uploading the bypass contract..."); + + let res = self + .upload_contract( + &ctx.dkg_admin, + &ctx.progress.progress_bar, + dkg_bypass_contract, + ) + .await?; + + ctx.println("\t✅ uploaded the bypass contract!"); + + Ok(res.code_id) + } + + async fn migrate_to_bypass_contract<'a>( + &self, + ctx: &DkgSkipCtx<'a>, + code_id: ContractCodeId, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "🔀 {}Attempting to migrate into the bypass contract...", + style("[5/8]").bold().dim() + )); + + ctx.set_pb_message("migrating the DKG contract..."); + + let migrate_msg = dkg_bypass_contract::MigrateMsg { + dealers: ctx + .ecash_signers + .iter() + .map(|signer| FakeDealerData { + vk: signer.data.ecash_keypair.verification_key().to_bs58(), + ed25519_identity: signer.data.ed25519_keypair.public_key().to_base58_string(), + announce: signer.data.endpoint.to_string(), + owner: Addr::unchecked(signer.data.cosmos_account.address.as_ref()), + }) + .collect(), + }; + + let migrate_fut = ctx.dkg_admin.migrate( + ctx.dkg_contract(), + code_id, + &migrate_msg, + "migrating bypass DKG contract from testnet-manager", + None, + ); + ctx.async_with_progress(migrate_fut).await?; + + ctx.println("\t✅ migrated the DKG into the bypass contract!"); + + Ok(()) + } + + async fn restore_dkg_contract<'a>( + &self, + ctx: &DkgSkipCtx<'a>, + code_id: ContractCodeId, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "↩️ {}Attempting to migrate back into the original DKG contract...", + style("[6/8]").bold().dim() + )); + + ctx.set_pb_message("migrating the DKG contract..."); + + let migrate_msg = nym_coconut_dkg_common::msg::MigrateMsg {}; + let migrate_fut = ctx.dkg_admin.migrate( + ctx.dkg_contract(), + code_id, + &migrate_msg, + "migrating initial DKG contract from testnet-manager", + None, + ); + ctx.async_with_progress(migrate_fut).await?; + + ctx.println("\t✅ restored the original DKG contract!"); + + Ok(()) + } + + async fn add_group_members<'a>(&self, ctx: &DkgSkipCtx<'a>) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "👪 {}Adding all the cw4 group members...", + style("[7/8]").bold().dim() + )); + + ctx.set_pb_message("⛽creating a new big cw4 family..."); + let admin = ctx.group_signing_client()?; + let new_members = ctx + .ecash_signers + .iter() + .map(|s| Member { + addr: s.data.cosmos_account.address.to_string(), + weight: 1, + }) + .collect(); + + let update_fut = admin.update_members(new_members, Vec::new(), None); + + ctx.async_with_progress(update_fut).await?; + ctx.println("\t✅ new cw4 group members got added"); + Ok(()) + } + + async fn transfer_signer_tokens<'a>( + &self, + ctx: &DkgSkipCtx<'a>, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "💸 {}Transferring tokens to the new signers...", + style("[8/8]").bold().dim() + )); + + let admin = ctx.admin_signing_client(self.admin.deref().clone())?; + + let mut receivers = Vec::new(); + for signer in &ctx.ecash_signers { + // send 101nym to the admin + receivers.push(( + signer.data.cosmos_account.address.clone(), + admin.mix_coins(101_000000), + )) + } + + ctx.set_pb_message("attempting to send signer tokens..."); + + let send_future = admin.send_multiple( + receivers, + "signers token transfer from testnet-manager", + None, + ); + let res = ctx.async_with_progress(send_future).await?; + + ctx.println(format!( + "\t✅ sent tokens in transaction: {} (height {})", + res.hash, res.height + )); + Ok(()) + } + + pub(crate) async fn attempt_bypass_dkg( + &self, + api_endpoints: Vec, + network: &LoadedNetwork, + dkg_bypass_contract: P1, + data_output_dir: P2, + ) -> Result, NetworkManagerError> + where + P1: AsRef, + P2: AsRef, + { + if api_endpoints.is_empty() { + return Err(NetworkManagerError::NoApiEndpoints); + } + + let dkg_bypass_contract = dkg_bypass_contract.as_ref(); + if !dkg_bypass_contract.is_file() { + return Err(NetworkManagerError::MalformedDkgBypassContractPath); + } + let Some(ext) = dkg_bypass_contract.extension() else { + return Err(NetworkManagerError::MalformedDkgBypassContractPath); + }; + if ext != "wasm" { + return Err(NetworkManagerError::MalformedDkgBypassContractPath); + } + + let mut ctx = DkgSkipCtx::new(network)?; + + self.generate_ecash_signer_data(&mut ctx, api_endpoints)?; + let current_code_id = self.validate_existing_contracts(&ctx).await?; + self.persist_dkg_keys(&mut ctx, data_output_dir).await?; + let new_code_id = self + .upload_bypass_contract(&ctx, dkg_bypass_contract) + .await?; + self.migrate_to_bypass_contract(&ctx, new_code_id).await?; + self.restore_dkg_contract(&ctx, current_code_id).await?; + self.add_group_members(&ctx).await?; + self.transfer_signer_tokens(&ctx).await?; + + Ok(ctx.ecash_signers) + } +} diff --git a/tools/internal/testnet-manager/src/manager/env.rs b/tools/internal/testnet-manager/src/manager/env.rs new file mode 100644 index 0000000000..005e6b48f4 --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/env.rs @@ -0,0 +1,173 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NetworkManagerError; +use crate::manager::network::LoadedNetwork; +use nym_config::defaults::var_names; +use std::fmt::{Display, Formatter}; +use std::fs; +use std::fs::File; +use std::io::Write; +use std::path::Path; +use tracing::{trace, warn}; + +#[derive(Default)] +pub struct Env { + pub(crate) mixnet_contract_address: Option, + pub(crate) vesting_contract_address: Option, + pub(crate) ecash_contract_address: Option, + pub(crate) cw4_group_contract_address: Option, + pub(crate) cw3_multisig_contract_address: Option, + pub(crate) dkg_contract_address: Option, + pub(crate) nyxd_endpoint: Option, + pub(crate) nym_api_endpoint: Option, +} + +impl Env { + pub fn with_nym_api>(mut self, nym_api: S) -> Self { + self.nym_api_endpoint = Some(nym_api.into()); + self + } + + // this will be used eventually + #[allow(dead_code)] + pub fn try_load>(path: P) -> Result { + let mut env = Env::default(); + let content = fs::read_to_string(path)?; + + for entry in content.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) { + let Some((k, v)) = entry.split_once('=') else { + warn!("malformed .env entry: '{entry}'"); + continue; + }; + + match k { + var_names::CONFIGURED + | var_names::BECH32_PREFIX + | var_names::MIX_DENOM + | var_names::MIX_DENOM_DISPLAY + | var_names::STAKE_DENOM + | var_names::STAKE_DENOM_DISPLAY + | var_names::DENOMS_EXPONENT => { + trace!("ignoring values for {k} and using default instead") + } + var_names::MIXNET_CONTRACT_ADDRESS => { + env.mixnet_contract_address = Some(v.to_string()) + } + var_names::VESTING_CONTRACT_ADDRESS => { + env.vesting_contract_address = Some(v.to_string()) + } + var_names::ECASH_CONTRACT_ADDRESS => { + env.ecash_contract_address = Some(v.to_string()) + } + var_names::GROUP_CONTRACT_ADDRESS => { + env.cw4_group_contract_address = Some(v.to_string()) + } + var_names::MULTISIG_CONTRACT_ADDRESS => { + env.cw3_multisig_contract_address = Some(v.to_string()) + } + var_names::COCONUT_DKG_CONTRACT_ADDRESS => { + env.dkg_contract_address = Some(v.to_string()) + } + var_names::NYXD => env.nyxd_endpoint = Some(v.to_string()), + var_names::NYM_API => env.nym_api_endpoint = Some(v.to_string()), + other => warn!("unsupported .env entry: '{other}'"), + } + } + + Ok(env) + } + + pub fn save>(&self, path: P) -> Result<(), NetworkManagerError> { + let path = path.as_ref(); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let mut env_file = File::create(path)?; + let content = self.to_string(); + env_file.write_all(content.as_bytes())?; + Ok(()) + } +} + +impl<'a> From<&'a LoadedNetwork> for Env { + fn from(network: &'a LoadedNetwork) -> Self { + Env { + mixnet_contract_address: Some(network.contracts.mixnet.address.to_string()), + vesting_contract_address: Some(network.contracts.vesting.address.to_string()), + ecash_contract_address: Some(network.contracts.ecash.address.to_string()), + cw4_group_contract_address: Some(network.contracts.cw4_group.address.to_string()), + cw3_multisig_contract_address: Some(network.contracts.cw3_multisig.address.to_string()), + dkg_contract_address: Some(network.contracts.dkg.address.to_string()), + nyxd_endpoint: Some(network.rpc_endpoint.to_string()), + nym_api_endpoint: None, + } + } +} + +impl Display for Env { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "CONFIGURED=true\n\ +\n\ +BECH32_PREFIX=n\n\ +MIX_DENOM=unym\n\ +MIX_DENOM_DISPLAY=nym\n\ +STAKE_DENOM=unyx\n\ +STAKE_DENOM_DISPLAY=nyx\n\ +DENOMS_EXPONENT=6\n\ +\n\ +" + )?; + if let Some(mixnet_contract_address) = &self.mixnet_contract_address { + writeln!( + f, + "{}={mixnet_contract_address}", + var_names::MIXNET_CONTRACT_ADDRESS + )?; + } + if let Some(vesting_contract_address) = &self.vesting_contract_address { + writeln!( + f, + "{}={vesting_contract_address}", + var_names::VESTING_CONTRACT_ADDRESS + )?; + } + if let Some(ecash_contract_address) = &self.ecash_contract_address { + writeln!( + f, + "{}={ecash_contract_address}", + var_names::ECASH_CONTRACT_ADDRESS + )?; + } + if let Some(cw4_group_contract_address) = &self.cw4_group_contract_address { + writeln!( + f, + "{}={cw4_group_contract_address}", + var_names::GROUP_CONTRACT_ADDRESS + )?; + } + if let Some(cw3_multisig_contract_address) = &self.cw3_multisig_contract_address { + writeln!( + f, + "{}={cw3_multisig_contract_address}", + var_names::MULTISIG_CONTRACT_ADDRESS + )?; + } + if let Some(dkg_contract_address) = &self.dkg_contract_address { + writeln!( + f, + "{}={dkg_contract_address}", + var_names::COCONUT_DKG_CONTRACT_ADDRESS + )?; + } + if let Some(nyxd_endpoint) = &self.nyxd_endpoint { + writeln!(f, "{}={nyxd_endpoint}", var_names::NYXD)?; + } + if let Some(nym_api_endpoint) = &self.nym_api_endpoint { + writeln!(f, "{}={nym_api_endpoint}", var_names::NYM_API)?; + } + Ok(()) + } +} diff --git a/tools/internal/testnet-manager/src/manager/local_apis.rs b/tools/internal/testnet-manager/src/manager/local_apis.rs new file mode 100644 index 0000000000..9fc92d17d0 --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/local_apis.rs @@ -0,0 +1,217 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NetworkManagerError; +use crate::helpers::{ProgressCtx, ProgressTracker, RunCommands}; +use crate::manager::dkg_skip::EcashSignerWithPaths; +use crate::manager::env::Env; +use crate::manager::network::LoadedNetwork; +use crate::manager::NetworkManager; +use console::style; +use nym_config::{ + must_get_home, DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, DEFAULT_NYM_APIS_DIR, NYM_DIR, +}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Stdio; +use tokio::process::Command; +use zeroize::Zeroizing; + +struct LocalApisCtx<'a> { + nym_api_binary: PathBuf, + progress: ProgressTracker, + network: &'a LoadedNetwork, + signers: Vec, +} + +impl<'a> ProgressCtx for LocalApisCtx<'a> { + fn progress_tracker(&self) -> &ProgressTracker { + &self.progress + } +} + +impl<'a> LocalApisCtx<'a> { + fn signer_id(&self, signer: &EcashSignerWithPaths) -> String { + format!( + "{}-{}", + signer.data.cosmos_account.address, self.network.name + ) + } + + fn new( + nym_api_binary: PathBuf, + network: &'a LoadedNetwork, + signers: Vec, + ) -> Result { + let progress = ProgressTracker::new(format!( + "\n🚀 setting up new local signing nym-APIs for network '{}' over {}", + network.name, network.rpc_endpoint + )); + + Ok(LocalApisCtx { + nym_api_binary, + network, + progress, + signers, + }) + } +} + +impl NetworkManager { + fn nym_api_config(&self, api_id: &str) -> PathBuf { + must_get_home() + .join(NYM_DIR) + .join(DEFAULT_NYM_APIS_DIR) + .join(api_id) + .join(DEFAULT_CONFIG_DIR) + .join(DEFAULT_CONFIG_FILENAME) + } + + async fn initialise_api<'a>( + &self, + ctx: &LocalApisCtx<'a>, + info: &EcashSignerWithPaths, + ) -> Result<(), NetworkManagerError> { + let address = &info.data.cosmos_account.address; + + ctx.set_pb_message(format!("initialising api {address}...")); + + let id = ctx.signer_id(info); + + // setup the binary itself + let mut child = Command::new(&ctx.nym_api_binary) + .args([ + "init", + "--id", + &id, + "--nyxd-validator", + ctx.network.rpc_endpoint.as_ref(), + "--mnemonic", + &Zeroizing::new(info.data.cosmos_account.mnemonic.to_string()), + "--enable-zk-nym", + "--announce-address", + info.data.endpoint.as_ref(), + ]) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .stdout(Stdio::null()) + .kill_on_drop(true) + .spawn()?; + let child_fut = child.wait(); + let out = ctx.async_with_progress(child_fut).await?; + if !out.success() { + return Err(NetworkManagerError::NymApiExecutionFailure); + } + + // load the config (and do very nasty things to it) + let config_path = self.nym_api_config(&id); + let config_content = fs::read_to_string(config_path)?; + let parsed_config: toml::Table = toml::from_str(&config_content)?; + let storage_paths = &parsed_config["base"] + .as_table() + .expect("nym-api config serialisation has changed")["storage_paths"] + .as_table() + .expect("nym-api config serialisation has changed"); + + let priv_id = &storage_paths["private_identity_key_file"] + .as_str() + .expect("nym-api config serialisation has changed"); + let pub_id = &storage_paths["public_identity_key_file"] + .as_str() + .expect("nym-api config serialisation has changed"); + let ecash = &parsed_config["coconut_signer"] + .as_table() + .expect("nym-api config serialisation has changed")["storage_paths"] + .as_table() + .expect("nym-api config serialisation has changed")["coconut_key_path"] + .as_str() + .expect("nym-api config serialisation has changed"); + + // overwrite pre-generated files + fs::copy(&info.paths.ecash_key, ecash)?; + fs::copy(&info.paths.ed25519_keys.private_key_path, priv_id)?; + fs::copy(&info.paths.ed25519_keys.public_key_path, pub_id)?; + + ctx.println(format!("\t nym-API {address} got initialised")); + + Ok(()) + } + + async fn initialise_apis<'a>(&self, ctx: &LocalApisCtx<'a>) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "🔏 {}Initialising local nym-apis...", + style("[1/1]").bold().dim() + )); + + for signer in &ctx.signers { + self.initialise_api(ctx, signer).await? + } + + ctx.println("\t✅ all APIs got initialised!"); + Ok(()) + } + + fn prepare_api_run_commands>( + &self, + ctx: &LocalApisCtx, + env_file: P, + ) -> Result { + let bin_canon = fs::canonicalize(&ctx.nym_api_binary)?; + let env_canon = fs::canonicalize(env_file)?; + let bin_canon_display = bin_canon.display(); + let env_canon_display = env_canon.display(); + + let mut cmds = Vec::new(); + for signer in &ctx.signers { + let port = signer.api_port(); + let id = ctx.signer_id(signer); + + cmds.push(format!( + "ROCKET_PORT={port} {bin_canon_display} -c {env_canon_display} run --id {id}" + )); + } + Ok(RunCommands(cmds)) + } + + fn output_api_run_commands(&self, ctx: &LocalApisCtx, cmds: &RunCommands) { + ctx.progress.output_run_commands(cmds) + } + + fn prepare_env_file>( + &self, + ctx: &LocalApisCtx, + env_file: P, + ) -> Result<(), NetworkManagerError> { + let env = Env::from(ctx.network).with_nym_api(ctx.signers[0].data.endpoint.as_ref()); + + let latest = self.default_latest_env_file_path(); + if fs::read_link(&latest).is_ok() { + fs::remove_file(&latest)?; + } + + let env_file_path = env_file.as_ref(); + env.save(env_file_path)?; + + // make symlink for usability purposes + std::os::unix::fs::symlink(env_file_path, &latest)?; + + Ok(()) + } + + pub(crate) async fn setup_local_apis>( + &self, + nym_api_binary: P, + network: &LoadedNetwork, + signer_data: Vec, + ) -> Result { + let ctx = LocalApisCtx::new(nym_api_binary.as_ref().to_path_buf(), network, signer_data)?; + let env_file = ctx.network.default_env_file_path(); + + self.initialise_apis(&ctx).await?; + self.prepare_env_file(&ctx, &env_file)?; + let cmds = self.prepare_api_run_commands(&ctx, env_file)?; + self.output_api_run_commands(&ctx, &cmds); + + Ok(cmds) + } +} diff --git a/tools/internal/testnet-manager/src/manager/local_client.rs b/tools/internal/testnet-manager/src/manager/local_client.rs new file mode 100644 index 0000000000..1a199f410f --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/local_client.rs @@ -0,0 +1,294 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NetworkManagerError; +use crate::helpers::{ProgressCtx, ProgressTracker}; +use crate::manager::network::LoadedNetwork; +use crate::manager::NetworkManager; +use console::style; +use nym_config::{must_get_home, DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, NYM_DIR}; +use nym_validator_client::client::NymApiClientExt; +use nym_validator_client::NymApiClient; +use rand::{thread_rng, RngCore}; +use std::fs; +use std::fs::OpenOptions; +use std::io::prelude::*; +use std::net::SocketAddr; +use std::path::{Path, PathBuf}; +use std::process::Stdio; +use std::time::Duration; +use tokio::net::TcpStream; +use tokio::process::Command; +use tokio::time::sleep; +use url::Url; + +struct LocalClientCtx<'a> { + nym_client_binary: PathBuf, + client_id: String, + gateway: Option, + + progress: ProgressTracker, + network: &'a LoadedNetwork, +} + +impl<'a> ProgressCtx for LocalClientCtx<'a> { + fn progress_tracker(&self) -> &ProgressTracker { + &self.progress + } +} + +impl<'a> LocalClientCtx<'a> { + fn new( + nym_client_binary: PathBuf, + gateway: Option, + network: &'a LoadedNetwork, + ) -> Result { + let progress = ProgressTracker::new(format!( + "\n🚀 setting up new local nym-client for network '{}' over {}", + network.name, network.rpc_endpoint + )); + let mut rng = thread_rng(); + let client_id = format!("{}-client-{}", network.name, rng.next_u32()); + + Ok(LocalClientCtx { + nym_client_binary, + network, + progress, + client_id, + gateway, + }) + } + + // hehe, that's disgusting, but it's not meant to be used by users + fn nym_api_url(&self) -> Result { + let env_file = fs::read_to_string(self.network.default_env_file_path())?; + for entry in env_file.lines() { + if let Some(raw_url) = entry.strip_prefix("NYM_API=") { + return Ok(raw_url.parse()?); + } + } + Err(NetworkManagerError::NymApiEndpointMissing) + } +} + +impl NetworkManager { + fn nym_client_config(&self, client_id: &str) -> PathBuf { + must_get_home() + .join(NYM_DIR) + .join("clients") + .join(client_id) + .join(DEFAULT_CONFIG_DIR) + .join(DEFAULT_CONFIG_FILENAME) + } + + async fn wait_for_api_gateway<'a>( + &self, + ctx: &LocalClientCtx<'a>, + ) -> Result { + // create api client + // hehe, that's disgusting, but it's not meant to be used by users + let api_url = ctx.nym_api_url()?; + ctx.set_pb_message(format!( + "⌛waiting for any gateway to appear in the directory ({api_url})..." + )); + + let api_client = NymApiClient::new(api_url); + + let wait_fut = async { + let inner_fut = async { + loop { + let mut gateways = match api_client.nym_api.get_basic_gateways(None).await { + Ok(gateways) => gateways, + Err(err) => { + ctx.println(format!( + "❌ {} {err}", + style("[API QUERY FAILURE]: ").bold().dim() + )); + continue; + } + }; + + // if we explicitly specified some identity, find THIS node + if let Some(identity) = ctx.gateway.as_ref() { + if let Some(node) = gateways + .nodes + .iter() + .find(|gw| &gw.ed25519_identity_pubkey == identity) + { + return SocketAddr::new( + node.ip_addresses[0], + node.entry.clone().unwrap().ws_port, + ); + } + } + + // otherwise look for ANY node + if let Some(node) = gateways.nodes.pop() { + return SocketAddr::new(node.ip_addresses[0], node.entry.unwrap().ws_port); + } + sleep(Duration::from_secs(10)).await; + } + }; + tokio::time::timeout(Duration::from_secs(240), inner_fut).await + }; + + match ctx.async_with_progress(wait_fut).await { + Ok(endpoint) => { + ctx.println(format!( + "\twe finally got a gateway in the directory! it's at: {endpoint}" + )); + Ok(endpoint) + } + Err(_) => Err(NetworkManagerError::ApiGatewayWaitTimeout), + } + } + + async fn wait_for_gateway_endpoint<'a>( + &self, + ctx: &LocalClientCtx<'a>, + gateway: SocketAddr, + ) -> Result<(), NetworkManagerError> { + ctx.set_pb_message(format!( + "⌛waiting for gateway at {gateway} to start receiving traffic..." + )); + + let wait_fut = async { + let inner_fut = async { + loop { + if TcpStream::connect(gateway).await.is_ok() { + break; + } + sleep(Duration::from_secs(10)).await; + } + }; + tokio::time::timeout(Duration::from_secs(240), inner_fut).await + }; + + if ctx.async_with_progress(wait_fut).await.is_err() { + return Err(NetworkManagerError::GatewayWaitTimeout); + } + + ctx.println(format!( + "\tthe gateway at {gateway} has finally come online" + )); + + Ok(()) + } + + async fn wait_for_gateway<'a>( + &self, + ctx: &LocalClientCtx<'a>, + ) -> Result<(), NetworkManagerError> { + let endpoint = self.wait_for_api_gateway(ctx).await?; + self.wait_for_gateway_endpoint(ctx, endpoint).await + } + + async fn prepare_nym_client<'a>( + &self, + ctx: &LocalClientCtx<'a>, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "🔏 {}Initialising local nym-client...", + style("[1/1]").bold().dim() + )); + + let env = ctx.network.default_env_file_path(); + let id = &ctx.client_id; + + self.wait_for_gateway(ctx).await?; + let mut rng = thread_rng(); + let mut port = rng.next_u32(); + port = (port + 1000) % (u16::MAX as u32); + + ctx.set_pb_message(format!("initialising client {id}...")); + ctx.println(format!("\tinitialising client {id}...")); + let mut cmd = Command::new(&ctx.nym_client_binary); + cmd.args([ + "-c", + &env.display().to_string(), + "init", + "--id", + id, + "--enabled-credentials-mode", + "true", + "--port", + &port.to_string(), + ]) + .stdout(Stdio::null()) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .kill_on_drop(true); + + if let Some(gateway) = &ctx.gateway { + cmd.args(["--gateway", gateway]); + } + + let mut child = cmd.spawn()?; + + let child_fut = child.wait(); + let out = ctx.async_with_progress(child_fut).await?; + if !out.success() { + return Err(NetworkManagerError::NymClientExecutionFailure); + } + + ctx.println(format!("\tupdating client {id} config...")); + + let config_path = self.nym_client_config(id); + let mut config_file = OpenOptions::new().append(true).open(config_path)?; + + // make the client ignore the performance of the nodes since we're not running network monitor + writeln!( + config_file, + r#" + +[debug.topology] +minimum_mixnode_performance = 0 +minimum_gateway_performance = 0 +"# + )?; + + ctx.println(format!("\t✅client {id} is ready to use!")); + + Ok(()) + } + + fn prepare_client_run_command( + &self, + ctx: &LocalClientCtx, + ) -> Result { + let env_file = ctx.network.default_env_file_path(); + + let bin_canon = fs::canonicalize(&ctx.nym_client_binary)?; + let env_canon = fs::canonicalize(env_file)?; + let bin_canon_display = bin_canon.display(); + let env_canon_display = env_canon.display(); + + let id = &ctx.client_id; + + Ok(format!( + "{bin_canon_display} -c {env_canon_display} run --id {id}" + )) + } + + pub(crate) async fn init_local_nym_client>( + &self, + nym_client_binary: P, + network: &LoadedNetwork, + gateway: Option, + ) -> Result { + let ctx = LocalClientCtx::new(nym_client_binary.as_ref().to_path_buf(), gateway, network)?; + + let env_file = ctx.network.default_env_file_path(); + if !env_file.exists() { + return Err(NetworkManagerError::EnvFileNotGenerated); + } + + self.prepare_nym_client(&ctx).await?; + let cmd = self.prepare_client_run_command(&ctx)?; + + ctx.println("🏇 run the binary with the following commands:"); + ctx.println(&cmd); + + Ok(cmd) + } +} diff --git a/tools/internal/testnet-manager/src/manager/local_nodes.rs b/tools/internal/testnet-manager/src/manager/local_nodes.rs new file mode 100644 index 0000000000..c8c9cf1fd6 --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/local_nodes.rs @@ -0,0 +1,498 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NetworkManagerError; +use crate::helpers::{ProgressCtx, ProgressTracker, RunCommands}; +use crate::manager::network::LoadedNetwork; +use crate::manager::node::NymNode; +use crate::manager::NetworkManager; +use console::style; +use nym_mixnet_contract_common::{Layer, LayerAssignment}; +use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::ops::Deref; +use std::path::{Path, PathBuf}; +use std::process::Stdio; +use tokio::process::Command; +use zeroize::Zeroizing; + +struct LocalNodesCtx<'a> { + nym_node_binary: PathBuf, + + progress: ProgressTracker, + network: &'a LoadedNetwork, + admin: DirectSigningHttpRpcNyxdClient, + + mix_nodes: Vec, + gateways: Vec, +} + +impl<'a> ProgressCtx for LocalNodesCtx<'a> { + fn progress_tracker(&self) -> &ProgressTracker { + &self.progress + } +} + +impl<'a> LocalNodesCtx<'a> { + fn nym_node_id(&self, node: &NymNode) -> String { + format!("{}-{}", self.network.name, node.owner.address) + } + + fn new( + nym_node_binary: PathBuf, + network: &'a LoadedNetwork, + admin_mnemonic: bip39::Mnemonic, + ) -> Result { + let progress = ProgressTracker::new(format!( + "\n🚀 setting up new local nym-nodes for network '{}' over {}", + network.name, network.rpc_endpoint + )); + + Ok(LocalNodesCtx { + nym_node_binary, + network, + admin: DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + network.client_config()?, + network.rpc_endpoint.as_str(), + admin_mnemonic, + )?, + mix_nodes: Vec::new(), + progress, + gateways: Vec::new(), + }) + } + + fn signing_node_owner( + &self, + node: &NymNode, + ) -> Result { + Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + self.network.client_config()?, + self.network.rpc_endpoint.as_str(), + node.owner.mnemonic.clone(), + )?) + } + + fn signing_rewarder(&self) -> Result { + Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + self.network.client_config()?, + self.network.rpc_endpoint.as_str(), + self.network + .auxiliary_addresses + .mixnet_rewarder + .mnemonic + .clone(), + )?) + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "node_type")] +pub enum BondingInformationV1 { + Mixnode(MixnodeBondingInformation), + Gateway(GatewayBondingInformation), +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct MixnodeBondingInformation { + pub(crate) version: String, + pub(crate) host: String, + pub(crate) identity_key: String, + pub(crate) sphinx_key: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct GatewayBondingInformation { + pub(crate) version: String, + pub(crate) host: String, + pub(crate) location: String, + pub(crate) identity_key: String, + pub(crate) sphinx_key: String, +} + +#[derive(Deserialize)] +struct ReducedSignatureOut { + encoded_signature: String, +} + +impl NetworkManager { + async fn initialise_nym_node<'a>( + &self, + ctx: &mut LocalNodesCtx<'a>, + offset: u16, + is_gateway: bool, + ) -> Result<(), NetworkManagerError> { + let mut node = NymNode::new_empty(); + let env = ctx.network.default_env_file_path(); + let id = ctx.nym_node_id(&node); + + let output_dir = tempfile::tempdir()?; + let output_file_path = output_dir.path().join("bonding_info.json"); + + ctx.set_pb_message(format!("initialising node {id}...")); + let mix_port = 5000 + offset; + let verloc_port = 6000 + offset; + let clients_port = 7000 + offset; + let http_port = 8000 + offset; + + node.mix_port = mix_port; + node.verloc_port = verloc_port; + node.clients_port = clients_port; + node.http_port = http_port; + + let mut cmd = Command::new(&ctx.nym_node_binary); + cmd.args([ + "-c", + &env.display().to_string(), + "run", + "--id", + &id, + "--init-only", + "--public-ips", + "127.0.0.1", + "--http-bind-address", + &format!("127.0.0.1:{http_port}"), + "--mixnet-bind-address", + &format!("127.0.0.1:{mix_port}"), + "--verloc-bind-address", + &format!("127.0.0.1:{verloc_port}"), + "--entry-bind-address", + &format!("127.0.0.1:{clients_port}"), + "--mnemonic", + &Zeroizing::new(node.owner.mnemonic.to_string()), + "--local", + "--output", + "json", + "--bonding-information-output", + &output_file_path.display().to_string(), + ]) + .stdout(Stdio::null()) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .kill_on_drop(true); + + if is_gateway { + cmd.args(["--mode", "entry"]); + } + + let mut child = cmd.spawn()?; + let child_fut = child.wait(); + let out = ctx.async_with_progress(child_fut).await?; + if !out.success() { + return Err(NetworkManagerError::NymNodeExecutionFailure); + } + + let output_file = fs::File::open(&output_file_path)?; + let bonding_info: BondingInformationV1 = serde_json::from_reader(&output_file)?; + + match bonding_info { + BondingInformationV1::Mixnode(bonding_info) => { + node.identity_key = bonding_info.identity_key; + node.sphinx_key = bonding_info.sphinx_key; + node.version = bonding_info.version; + } + BondingInformationV1::Gateway(bonding_info) => { + node.identity_key = bonding_info.identity_key; + node.sphinx_key = bonding_info.sphinx_key; + node.version = bonding_info.version; + } + } + + ctx.set_pb_message(format!("generating bonding signature for node {id}...")); + + let msg = if is_gateway { + node.gateway_bonding_payload() + } else { + node.mixnode_bonding_payload() + }; + + let child = Command::new(&ctx.nym_node_binary) + .args([ + "--no-banner", + "sign", + "--id", + &id, + "--contract-msg", + &msg, + "--output", + "json", + ]) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .kill_on_drop(true) + .output(); + let out = ctx.async_with_progress(child).await?; + if !out.status.success() { + return Err(NetworkManagerError::NymNodeExecutionFailure); + } + let signature: ReducedSignatureOut = serde_json::from_slice(&out.stdout)?; + node.bonding_signature = signature.encoded_signature; + + ctx.println(format!( + "\tinitialised node {} (gateway: {})", + node.identity_key, is_gateway + )); + + if is_gateway { + ctx.gateways.push(node) + } else { + ctx.mix_nodes.push(node) + } + Ok(()) + } + + async fn initialise_nym_nodes<'a>( + &self, + ctx: &mut LocalNodesCtx<'a>, + mixnodes: u16, + gateways: u16, + ) -> Result<(), NetworkManagerError> { + const OFFSET: u16 = 100; + if mixnodes > OFFSET { + panic!("seriously? over 100 mixnodes?") + } + + ctx.println(format!( + "🔏 {}Initialising local nym-nodes...", + style("[1/5]").bold().dim() + )); + + for i in 0..mixnodes { + self.initialise_nym_node(ctx, i, false).await?; + } + for i in 0..gateways { + self.initialise_nym_node(ctx, i + OFFSET, true).await?; + } + + ctx.println("\t✅ all nym nodes got initialised!"); + + Ok(()) + } + + async fn transfer_bonding_tokens<'a>( + &self, + ctx: &LocalNodesCtx<'a>, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "💸 {}Transferring tokens to the bond owners...", + style("[2/5]").bold().dim() + )); + + let mut receivers = Vec::new(); + for node in ctx.mix_nodes.iter().chain(ctx.gateways.iter()) { + // send 101nym to the owner + receivers.push((node.owner.address.clone(), ctx.admin.mix_coins(101_000000))) + } + + ctx.set_pb_message("attempting to send signer tokens..."); + + let send_future = ctx.admin.send_multiple( + receivers, + "bond owners token transfer from testnet-manager", + None, + ); + let res = ctx.async_with_progress(send_future).await?; + + ctx.println(format!( + "\t✅ sent tokens in transaction: {} (height {})", + res.hash, res.height + )); + Ok(()) + } + + async fn bond_node<'a>( + &self, + ctx: &LocalNodesCtx<'a>, + node: &NymNode, + is_gateway: bool, + ) -> Result<(), NetworkManagerError> { + let prefix = if is_gateway { "[gateway]" } else { "[mixnode]" }; + ctx.set_pb_prefix(prefix); + + let id = ctx.nym_node_id(node); + ctx.set_pb_message(format!("attempting to bond node {id}...")); + + let owner = ctx.signing_node_owner(node)?; + + let (bonding_fut, typ) = if is_gateway { + ( + owner.bond_gateway( + node.gateway(), + node.bonding_signature(), + node.pledge().into(), + None, + ), + "gateway", + ) + } else { + ( + owner.bond_mixnode( + node.mixnode(), + node.cost_params(), + node.bonding_signature(), + node.pledge().into(), + None, + ), + "mixnode", + ) + }; + let res = ctx.async_with_progress(bonding_fut).await?; + ctx.println(format!( + "\t{id} ({typ}) bonded in transaction: {}", + res.transaction_hash + )); + + Ok(()) + } + + async fn bond_nym_nodes<'a>(&self, ctx: &LocalNodesCtx<'a>) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "⛓️ {}Bonding the local nym-nodes...", + style("[3/5]").bold().dim() + )); + + for mix_node in &ctx.mix_nodes { + self.bond_node(ctx, mix_node, false).await?; + } + for gateway in &ctx.gateways { + self.bond_node(ctx, gateway, true).await?; + } + + ctx.println("\t✅ all nym nodes got bonded!"); + + Ok(()) + } + + async fn assign_to_active_set<'a>( + &self, + ctx: &LocalNodesCtx<'a>, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "🔌 {}Assigning mixnodes to the active set...", + style("[4/5]").bold().dim() + )); + + let rewarder = ctx.signing_rewarder()?; + + ctx.set_pb_message("starting epoch transition..."); + let fut = rewarder.begin_epoch_transition(None); + ctx.async_with_progress(fut).await?; + + ctx.set_pb_message("reconciling (no) epoch events..."); + let fut = rewarder.reconcile_epoch_events(None, None); + ctx.async_with_progress(fut).await?; + + ctx.set_pb_message("finally assigning the active set..."); + let fut = rewarder.get_rewarding_parameters(); + let rewarding_params = ctx.async_with_progress(fut).await?; + let active_set_size = rewarding_params.active_set_size; + + let layer_assignment = vec![ + LayerAssignment::new(1, Layer::One), + LayerAssignment::new(2, Layer::Two), + LayerAssignment::new(3, Layer::Three), + ]; + let fut = rewarder.advance_current_epoch(layer_assignment, active_set_size, None); + ctx.async_with_progress(fut).await?; + + Ok(()) + } + + fn prepare_nym_nodes_run_commands( + &self, + ctx: &LocalNodesCtx, + ) -> Result { + let env_file = ctx.network.default_env_file_path(); + + let bin_canon = fs::canonicalize(&ctx.nym_node_binary)?; + let env_canon = fs::canonicalize(env_file)?; + let bin_canon_display = bin_canon.display(); + let env_canon_display = env_canon.display(); + + let mut cmds = Vec::new(); + for mixnode in ctx.mix_nodes.iter() { + ctx.println(format!( + "\tpreparing node {} (mixnode)", + mixnode.identity_key + )); + let id = ctx.nym_node_id(mixnode); + cmds.push(format!( + "{bin_canon_display} -c {env_canon_display} run --id {id} --local" + )); + } + + for gateway in ctx.gateways.iter() { + ctx.println(format!( + "\tpreparing node {} (gateway)", + gateway.identity_key + )); + let id = ctx.nym_node_id(gateway); + cmds.push(format!( + "{bin_canon_display} -c {env_canon_display} run --id {id} --local" + )); + } + + Ok(RunCommands(cmds)) + } + + fn output_nym_nodes_run_commands(&self, ctx: &LocalNodesCtx, cmds: &RunCommands) { + ctx.progress.output_run_commands(cmds) + } + + async fn persist_nodes_in_database<'a>( + &self, + ctx: &LocalNodesCtx<'a>, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "📦 {}Storing the node information in the database", + style("[5/5]").bold().dim() + )); + + ctx.set_pb_message("attempting to persist node information..."); + let mix_save_future = self + .storage + .persist_mixnodes(&ctx.mix_nodes, ctx.network.id); + let gw_save_future = self.storage.persist_gateways(&ctx.gateways, ctx.network.id); + ctx.async_with_progress(mix_save_future).await?; + ctx.async_with_progress(gw_save_future).await?; + + ctx.println( + "\t✅ the bonded node information got persisted in the database for future use", + ); + + Ok(()) + } + + pub(crate) async fn init_local_nym_nodes>( + &self, + nym_node_binary: P, + network: &LoadedNetwork, + mixnodes: u16, + gateways: u16, + ) -> Result { + let mut ctx = LocalNodesCtx::new( + nym_node_binary.as_ref().to_path_buf(), + network, + self.admin.deref().clone(), + )?; + + let env_file = ctx.network.default_env_file_path(); + if !env_file.exists() { + return Err(NetworkManagerError::EnvFileNotGenerated); + } + + self.initialise_nym_nodes(&mut ctx, mixnodes, gateways) + .await?; + self.transfer_bonding_tokens(&ctx).await?; + self.bond_nym_nodes(&ctx).await?; + self.assign_to_active_set(&ctx).await?; + self.persist_nodes_in_database(&ctx).await?; + let cmds = self.prepare_nym_nodes_run_commands(&ctx)?; + self.output_nym_nodes_run_commands(&ctx, &cmds); + + Ok(cmds) + } +} diff --git a/tools/internal/testnet-manager/src/manager/mod.rs b/tools/internal/testnet-manager/src/manager/mod.rs new file mode 100644 index 0000000000..d7499328e2 --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/mod.rs @@ -0,0 +1,129 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NetworkManagerError; +use crate::helpers::{async_with_progress, default_storage_dir, wasm_code}; +use crate::manager::network::LoadedNetwork; +use crate::manager::storage::NetworkManagerStorage; +use bip39::rand::prelude::SliceRandom; +use bip39::rand::thread_rng; +use indicatif::ProgressBar; +use nym_config::defaults::NymNetworkDetails; +use nym_validator_client::nyxd::cosmwasm_client::types::UploadResult; +use nym_validator_client::nyxd::Config; +use nym_validator_client::{DirectSigningHttpRpcNyxdClient, QueryHttpRpcNyxdClient}; +use std::path::{Path, PathBuf}; +use url::Url; +use zeroize::Zeroizing; + +mod contract; +mod dkg_skip; +pub(crate) mod env; +mod local_apis; +mod local_client; +mod local_nodes; +pub(crate) mod network; +mod network_init; +mod node; +pub(crate) mod storage; + +pub(crate) struct NetworkManager { + admin: Zeroizing, + storage: NetworkManagerStorage, + rpc_endpoint: Url, +} + +impl NetworkManager { + pub(crate) async fn new>( + database_path: P, + mnemonic: Option, + rpc_endpoint: Option, + ) -> Result { + let storage = NetworkManagerStorage::init(database_path).await?; + + let (mnemonic, rpc_endpoint) = if !storage.metadata_set().await? { + let mnemonic = mnemonic.ok_or(NetworkManagerError::MnemonicNotSet)?; + let rpc_endpoint = rpc_endpoint.ok_or(NetworkManagerError::RpcEndpointNotSet)?; + + storage + .set_initial_metadata(&mnemonic, &rpc_endpoint) + .await?; + (mnemonic, rpc_endpoint) + } else { + let mnemonic = storage + .get_master_mnemonic() + .await? + .ok_or(NetworkManagerError::MnemonicNotSet)?; + + let rpc_endpoint = storage + .get_rpc_endpoint() + .await? + .ok_or(NetworkManagerError::RpcEndpointNotSet)?; + + (mnemonic, rpc_endpoint) + }; + + Ok(NetworkManager { + admin: Zeroizing::new(mnemonic), + storage, + rpc_endpoint, + }) + } + + pub fn default_latest_env_file_path(&self) -> PathBuf { + default_storage_dir().join("latest.env") + } + + #[allow(unused)] + pub(crate) fn query_client( + &self, + network: &LoadedNetwork, + ) -> Result { + let network_details = NymNetworkDetails::from(network); + let config = Config::try_from_nym_network_details(&network_details)?; + + Ok(QueryHttpRpcNyxdClient::connect( + config, + self.rpc_endpoint.as_str(), + )?) + } + + fn get_network_name(&self, user_provided: Option) -> String { + user_provided.unwrap_or_else(|| { + // a hack to get human-readable words without extra deps : ) + let mut rng = thread_rng(); + + let words = bip39::Language::English.word_list(); + let first = words.choose(&mut rng).unwrap(); + let second = words.choose(&mut rng).unwrap(); + format!("{first}-{second}") + }) + } + + async fn upload_contract>( + &self, + admin: &DirectSigningHttpRpcNyxdClient, + pb: &ProgressBar, + path: P, + ) -> Result { + let wasm = wasm_code(path)?; + let upload_future = admin.upload(wasm, "contract upload from testnet-manager", None); + + async_with_progress(upload_future, pb) + .await + .map_err(Into::into) + } + + pub(crate) async fn load_existing_network( + &self, + network_name: Option, + ) -> Result { + let network_name = if let Some(explicit) = network_name { + explicit + } else { + self.storage.get_latest_network_name().await? + }; + + self.storage.try_load_network(&network_name).await + } +} diff --git a/tools/internal/testnet-manager/src/manager/network.rs b/tools/internal/testnet-manager/src/manager/network.rs new file mode 100644 index 0000000000..6c65eda06c --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/network.rs @@ -0,0 +1,145 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NetworkManagerError; +use crate::helpers::default_storage_dir; +use crate::manager::contract::{Account, LoadedNymContracts, NymContracts}; +use nym_config::defaults::{NymNetworkDetails, ValidatorDetails}; +use nym_validator_client::nyxd::Config; +use nym_validator_client::{DirectSigningHttpRpcNyxdClient, QueryHttpRpcNyxdClient}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use time::OffsetDateTime; +use url::Url; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Network { + pub name: String, + + pub rpc_endpoint: Url, + + #[serde(with = "time::serde::rfc3339")] + pub created_at: OffsetDateTime, + + pub contracts: NymContracts, + + pub auxiliary_addresses: SpecialAddresses, +} + +impl Network { + pub fn into_loaded(self) -> LoadedNetwork { + self.into() + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct LoadedNetwork { + pub(crate) id: i64, + pub(crate) name: String, + + pub(crate) rpc_endpoint: Url, + + #[serde(with = "time::serde::rfc3339")] + pub(crate) created_at: OffsetDateTime, + + pub(crate) contracts: LoadedNymContracts, + + pub(crate) auxiliary_addresses: SpecialAddresses, +} + +impl From for LoadedNetwork { + fn from(value: Network) -> Self { + LoadedNetwork { + id: i64::MAX, + name: value.name, + rpc_endpoint: value.rpc_endpoint, + created_at: value.created_at, + contracts: value.contracts.into(), + auxiliary_addresses: value.auxiliary_addresses, + } + } +} + +impl<'a> From<&'a LoadedNetwork> for nym_config::defaults::NymNetworkDetails { + fn from(value: &'a LoadedNetwork) -> Self { + let contracts = nym_config::defaults::NymContracts { + mixnet_contract_address: Some(value.contracts.mixnet.address.to_string()), + vesting_contract_address: Some(value.contracts.vesting.address.to_string()), + ecash_contract_address: Some(value.contracts.ecash.address.to_string()), + group_contract_address: Some(value.contracts.cw4_group.address.to_string()), + multisig_contract_address: Some(value.contracts.cw3_multisig.address.to_string()), + coconut_dkg_contract_address: Some(value.contracts.dkg.address.to_string()), + }; + // ASSUMPTION: same chain details like prefix, denoms, etc. as mainnet + let mainnet = NymNetworkDetails::new_mainnet(); + NymNetworkDetails { + chain_details: mainnet.chain_details, + network_name: "foomp".to_string(), + endpoints: vec![ValidatorDetails { + nyxd_url: value.rpc_endpoint.to_string(), + websocket_url: None, + api_url: None, + }], + contracts, + explorer_api: None, + nym_vpn_api_url: None, + } + } +} + +impl LoadedNetwork { + pub fn default_env_file_path(&self) -> PathBuf { + default_storage_dir() + .join(&self.name) + .join(format!("{}.env", &self.name)) + } + + #[allow(dead_code)] + pub fn query_client(&self) -> Result { + Ok(QueryHttpRpcNyxdClient::connect( + self.client_config()?, + self.rpc_endpoint.as_str(), + )?) + } + + pub fn dkg_signing_client( + &self, + ) -> Result { + Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + self.client_config()?, + self.rpc_endpoint.as_str(), + self.contracts.dkg.admin_mnemonic.clone(), + )?) + } + + pub fn client_config(&self) -> Result { + let network_details = NymNetworkDetails::from(self); + let config = Config::try_from_nym_network_details(&network_details)?; + Ok(config) + } + + pub fn cw4_group_signing_client( + &self, + ) -> Result { + Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + self.client_config()?, + self.rpc_endpoint.as_str(), + self.contracts.cw4_group.admin_mnemonic.clone(), + )?) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SpecialAddresses { + pub ecash_holding_account: Account, + pub mixnet_rewarder: Account, +} + +impl Default for SpecialAddresses { + fn default() -> Self { + SpecialAddresses { + ecash_holding_account: Account::new(), + mixnet_rewarder: Account::new(), + } + } +} diff --git a/tools/internal/testnet-manager/src/manager/network_init.rs b/tools/internal/testnet-manager/src/manager/network_init.rs new file mode 100644 index 0000000000..568632f734 --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/network_init.rs @@ -0,0 +1,704 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NetworkManagerError; +use crate::helpers::{async_with_progress, ProgressCtx, ProgressTracker}; +use crate::manager::contract::Account; +use crate::manager::network::Network; +use crate::manager::NetworkManager; +use console::style; +use cw_utils::Threshold; +use indicatif::HumanDuration; +use nym_coconut_dkg_common::types::TimeConfiguration; +use nym_config::defaults::NymNetworkDetails; +use nym_mixnet_contract_common::{Decimal, InitialRewardingParams, Percent}; +use nym_validator_client::nyxd::cosmwasm_client::types::InstantiateOptions; +use nym_validator_client::nyxd::Config; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use std::ops::Deref; +use std::path::Path; +use std::time::Duration; +use time::format_description::well_known::Rfc3339; +use time::OffsetDateTime; +use url::Url; + +struct InitCtx { + progress: ProgressTracker, + network: Network, + admin: DirectSigningHttpRpcNyxdClient, +} + +impl InitCtx { + fn dummy_client_config() -> Result { + // ASSUMPTION: same chain details like prefix, denoms, etc. as mainnet + let mainnet = NymNetworkDetails::new_mainnet(); + let network_details = NymNetworkDetails { + chain_details: mainnet.chain_details, + network_name: "foomp".to_string(), // does this matter? + endpoints: vec![], + contracts: Default::default(), + explorer_api: None, + nym_vpn_api_url: None, + }; + Ok(Config::try_from_nym_network_details(&network_details)?) + } + + fn new( + network_name: String, + admin_mnemonic: bip39::Mnemonic, + rpc_endpoint: &Url, + ) -> Result { + let admin = DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + Self::dummy_client_config()?, + rpc_endpoint.as_str(), + admin_mnemonic, + )?; + + let progress = ProgressTracker::new(format!( + "\n🚀 setting up new testnet '{network_name}' over {rpc_endpoint}", + )); + + Ok(InitCtx { + progress, + network: Network { + name: network_name, + rpc_endpoint: rpc_endpoint.clone(), + created_at: OffsetDateTime::now_utc(), + contracts: Default::default(), + auxiliary_addresses: Default::default(), + }, + admin, + }) + } + + fn mixnet_signing_client(&self) -> Result { + Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + Self::dummy_client_config()?, + self.network.rpc_endpoint.as_str(), + self.network.contracts.mixnet.admin()?.mnemonic.clone(), + )?) + } + + fn multisig_signing_client( + &self, + ) -> Result { + Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + Self::dummy_client_config()?, + self.network.rpc_endpoint.as_str(), + self.network + .contracts + .cw3_multisig + .admin()? + .mnemonic + .clone(), + )?) + } +} + +impl ProgressCtx for InitCtx { + fn progress_tracker(&self) -> &ProgressTracker { + &self.progress + } +} + +impl NetworkManager { + fn mixnet_migrate_message( + &self, + ctx: &InitCtx, + ) -> Result { + Ok(nym_mixnet_contract_common::MigrateMsg { + vesting_contract_address: Some(ctx.network.contracts.vesting.address()?.to_string()), + }) + } + + fn multisig_migrate_message( + &self, + ctx: &InitCtx, + ) -> Result { + Ok(nym_multisig_contract_common::msg::MigrateMsg { + coconut_bandwidth_address: ctx.network.contracts.ecash.address()?.to_string(), + coconut_dkg_address: ctx.network.contracts.dkg.address()?.to_string(), + }) + } + + fn mixnet_init_message( + &self, + ctx: &InitCtx, + custom_epoch_duration: Option, + ) -> Result { + Ok(nym_mixnet_contract_common::InstantiateMsg { + rewarding_validator_address: ctx + .network + .auxiliary_addresses + .mixnet_rewarder + .address + .to_string(), + // PLACEHOLDER \/ + vesting_contract_address: ctx + .network + .auxiliary_addresses + .mixnet_rewarder + .address + .to_string(), + // PLACEHOLDER /\ + rewarding_denom: ctx.admin.mix_coin(0).denom, + epochs_in_interval: 720, + epoch_duration: custom_epoch_duration.unwrap_or(Duration::from_secs(60 * 60)), + initial_rewarding_params: InitialRewardingParams { + initial_reward_pool: Decimal::from_atomics(250_000_000_000_000u128, 0).unwrap(), + initial_staking_supply: Decimal::from_atomics(100_000_000_000_000u128, 0).unwrap(), + staking_supply_scale_factor: Percent::from_percentage_value(50).unwrap(), + sybil_resistance: Percent::from_percentage_value(30).unwrap(), + active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), + interval_pool_emission: Percent::from_percentage_value(2).unwrap(), + rewarded_set_size: 240, + active_set_size: 240, + }, + profit_margin: Default::default(), + interval_operating_cost: Default::default(), + }) + } + + fn vesting_init_message( + &self, + ctx: &InitCtx, + ) -> Result { + Ok(nym_vesting_contract_common::InitMsg { + mixnet_contract_address: ctx.network.contracts.mixnet.address()?.to_string(), + mix_denom: ctx.admin.mix_coin(0).denom, + }) + } + + fn dkg_init_message( + &self, + ctx: &InitCtx, + ) -> Result { + Ok(nym_coconut_dkg_common::msg::InstantiateMsg { + group_addr: ctx.network.contracts.cw4_group.address()?.to_string(), + multisig_addr: ctx.network.contracts.cw3_multisig.address()?.to_string(), + time_configuration: Some(TimeConfiguration { + public_key_submission_time_secs: 3600, + dealing_exchange_time_secs: 3600, + verification_key_submission_time_secs: 3600, + verification_key_validation_time_secs: 3600, + verification_key_finalization_time_secs: 3600, + in_progress_time_secs: 10000000000, + }), + mix_denom: ctx.admin.mix_coin(0).denom, + key_size: 5, + }) + } + + fn ecash_init_message( + &self, + ctx: &InitCtx, + ) -> Result { + Ok(nym_ecash_contract_common::msg::InstantiateMsg { + holding_account: ctx + .network + .auxiliary_addresses + .ecash_holding_account + .address + .to_string(), + multisig_addr: ctx.network.contracts.cw3_multisig.address()?.to_string(), + group_addr: ctx.network.contracts.cw4_group.address()?.to_string(), + deposit_amount: ctx.admin.mix_coin(75_000_000).into(), + }) + } + + fn cw3_multisig_init_message( + &self, + ctx: &InitCtx, + ) -> Result { + Ok(nym_multisig_contract_common::msg::InstantiateMsg { + group_addr: ctx.network.contracts.cw4_group.address()?.to_string(), + + // PLACEHOLDER \/ + coconut_bandwidth_contract_address: ctx + .network + .contracts + .cw4_group + .address()? + .to_string(), + coconut_dkg_contract_address: ctx.network.contracts.cw4_group.address()?.to_string(), + // PLACEHOLDER /\ + threshold: Threshold::AbsolutePercentage { + percentage: "0.67".parse().unwrap(), + }, + max_voting_period: cw_utils::Duration::Time(3600), + executor: None, + proposal_deposit: None, + }) + } + + fn cw4_group_init_message( + &self, + ctx: &InitCtx, + ) -> Result { + Ok(nym_group_contract_common::msg::InstantiateMsg { + admin: Some( + ctx.network + .contracts + .cw4_group + .admin()? + .address() + .to_string(), + ), + // TODO: prepopulate + members: vec![], + }) + } + + fn find_contracts>( + &self, + ctx: &mut InitCtx, + base_dir: P, + ) -> Result<(), NetworkManagerError> { + ctx.network.contracts.discover_paths(base_dir)?; + + ctx.println(format!( + "🔍 {}Locating .wasm files...", + style("[1/8]").bold().dim() + )); + ctx.println(format!( + "\tdiscovered mixnet contract at '{}'", + ctx.network.contracts.mixnet.wasm_path()?.display() + )); + ctx.println(format!( + "\tdiscovered vesting contract at '{}'", + ctx.network.contracts.vesting.wasm_path()?.display() + )); + ctx.println(format!( + "\tdiscovered ecash contract at '{}'", + ctx.network.contracts.ecash.wasm_path()?.display() + )); + ctx.println(format!( + "\tdiscovered cw4_group contract at '{}'", + ctx.network.contracts.cw4_group.wasm_path()?.display() + )); + ctx.println(format!( + "\tdiscovered cw3_multisig contract at '{}'", + ctx.network.contracts.cw3_multisig.wasm_path()?.display() + )); + ctx.println(format!( + "\tdiscovered dkg contract at '{}'", + ctx.network.contracts.dkg.wasm_path()?.display() + )); + + ctx.println("\t✅ found all the contracts!"); + + Ok(()) + } + + async fn upload_contracts(&self, ctx: &mut InitCtx) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "🚚 {}Uploading contracts...", + style("[2/8]").bold().dim() + )); + + let total = ctx.network.contracts.count() as u64; + let pb = &ctx.progress.progress_bar; + + for (progress, contract) in ctx + .network + .contracts + .fake_iter_mut() + .into_iter() + .enumerate() + { + pb.set_prefix(format!("[{}/{total}]", progress + 1)); + let name = &contract.name; + pb.set_message(format!("uploading {name} contract...")); + let upload_res = self + .upload_contract( + &ctx.admin, + &ctx.progress.progress_bar, + &contract.wasm_path()?, + ) + .await?; + pb.println(format!( + "\t{name} contract uploaded with code: {}", + upload_res.code_id + )); + contract.upload_info = Some(upload_res.into()); + } + + ctx.println("\t✅ uploaded all the contracts!"); + + Ok(()) + } + + fn create_contract_admins_mnemonics( + &self, + ctx: &mut InitCtx, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "📝 {}Generating admin mnemonics...", + style("[3/8]").bold().dim() + )); + + let total = ctx.network.contracts.count() as u64; + let pb = &ctx.progress.progress_bar; + for (progress, contract) in ctx + .network + .contracts + .fake_iter_mut() + .into_iter() + .enumerate() + { + pb.set_prefix(format!("[{}/{total}]", progress + 1)); + let name = &contract.name; + pb.set_message(format!("generating admin mnemonic for {name} contract...")); + let admin = Account::new(); + pb.println(format!( + "\t{} is going to be admin for the {name} contract", + admin.address + )); + contract.admin = Some(admin) + } + + ctx.println("\t✅ generated all admin mnemonics!"); + + Ok(()) + } + + async fn transfer_admin_tokens(&self, ctx: &InitCtx) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "💸 {}Transferring tokens to the admin accounts...", + style("[4/8]").bold().dim() + )); + + let mut receivers = Vec::new(); + for contract in ctx.network.contracts.fake_iter() { + // send 10nym to the admin + receivers.push((contract.admin()?.address(), ctx.admin.mix_coins(10_000000))) + } + + // also send them to the rewarder + receivers.push(( + ctx.network.auxiliary_addresses.mixnet_rewarder.address(), + ctx.admin.mix_coins(10_000000), + )); + + ctx.set_pb_message("attempting to send admin tokens..."); + + let send_future = + ctx.admin + .send_multiple(receivers, "admin token transfer from testnet-manager", None); + let res = ctx.async_with_progress(send_future).await?; + + ctx.println(format!( + "\t✅ sent tokens in transaction: {} (height {})", + res.hash, res.height + )); + + Ok(()) + } + + async fn instantiate_contracts( + &self, + ctx: &mut InitCtx, + custom_epoch_duration: Option, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "💽 {}Instantiating all the contracts...", + style("[5/8]").bold().dim() + )); + + let total = ctx.network.contracts.count() as u64; + + // mixnet + ctx.set_pb_prefix(format!("[1/{total}]")); + let name = &ctx.network.contracts.mixnet.name; + let code_id = ctx.network.contracts.mixnet.upload_info()?.code_id; + let admin = ctx.network.contracts.mixnet.admin()?.address.clone(); + ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); + let init_msg = self.mixnet_init_message(ctx, custom_epoch_duration)?; + let init_fut = ctx.admin.instantiate( + code_id, + &init_msg, + format!("{name} contract"), + "contract instantiation from testnet-manager", + Some(InstantiateOptions::default().with_admin(admin)), + None, + ); + let res = ctx.async_with_progress(init_fut).await?; + let address = &res.contract_address; + ctx.println(format!( + "\t{name} contract instantiated with address: {address}", + )); + ctx.network.contracts.mixnet.init_info = Some(res.into()); + + // vesting + ctx.set_pb_prefix(format!("[2/{total}]")); + let name = &ctx.network.contracts.vesting.name; + let code_id = ctx.network.contracts.vesting.upload_info()?.code_id; + let admin = ctx.network.contracts.vesting.admin()?.address.clone(); + ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); + let init_msg = self.vesting_init_message(ctx)?; + let init_fut = ctx.admin.instantiate( + code_id, + &init_msg, + format!("{name} contract"), + "contract instantiation from testnet-manager", + Some(InstantiateOptions::default().with_admin(admin)), + None, + ); + let res = ctx.async_with_progress(init_fut).await?; + let address = &res.contract_address; + ctx.println(format!( + "\t{name} contract instantiated with address: {address}", + )); + ctx.network.contracts.vesting.init_info = Some(res.into()); + + // group + ctx.set_pb_prefix(format!("[3/{total}]")); + let name = &ctx.network.contracts.cw4_group.name; + let code_id = ctx.network.contracts.cw4_group.upload_info()?.code_id; + let admin = ctx.network.contracts.cw4_group.admin()?.address.clone(); + ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); + let init_msg = self.cw4_group_init_message(ctx)?; + let init_fut = ctx.admin.instantiate( + code_id, + &init_msg, + format!("{name} contract"), + "contract instantiation from testnet-manager", + Some(InstantiateOptions::default().with_admin(admin)), + None, + ); + let res = ctx.async_with_progress(init_fut).await?; + let address = &res.contract_address; + ctx.println(format!( + "\t{name} contract instantiated with address: {address}", + )); + ctx.network.contracts.cw4_group.init_info = Some(res.into()); + + // multisig + ctx.set_pb_prefix(format!("[4/{total}]")); + let name = &ctx.network.contracts.cw3_multisig.name; + let code_id = ctx.network.contracts.cw3_multisig.upload_info()?.code_id; + let admin = ctx.network.contracts.cw3_multisig.admin()?.address.clone(); + ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); + let init_msg = self.cw3_multisig_init_message(ctx)?; + let init_fut = ctx.admin.instantiate( + code_id, + &init_msg, + format!("{name} contract"), + "contract instantiation from testnet-manager", + Some(InstantiateOptions::default().with_admin(admin)), + None, + ); + let res = ctx.async_with_progress(init_fut).await?; + let address = &res.contract_address; + ctx.println(format!( + "\t{name} contract instantiated with address: {address}", + )); + ctx.network.contracts.cw3_multisig.init_info = Some(res.into()); + + // dkg + ctx.set_pb_prefix(format!("[5/{total}]")); + let name = &ctx.network.contracts.dkg.name; + let code_id = ctx.network.contracts.dkg.upload_info()?.code_id; + let admin = ctx.network.contracts.dkg.admin()?.address.clone(); + ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); + let init_msg = self.dkg_init_message(ctx)?; + let init_fut = ctx.admin.instantiate( + code_id, + &init_msg, + format!("{name} contract"), + "contract instantiation from testnet-manager", + Some(InstantiateOptions::default().with_admin(admin)), + None, + ); + let res = ctx.async_with_progress(init_fut).await?; + let address = &res.contract_address; + ctx.println(format!( + "\t{name} contract instantiated with address: {address}", + )); + ctx.network.contracts.dkg.init_info = Some(res.into()); + + // ecash + ctx.set_pb_prefix(format!("[6/{total}]")); + let name = &ctx.network.contracts.ecash.name; + let code_id = ctx.network.contracts.ecash.upload_info()?.code_id; + let admin = ctx.network.contracts.ecash.admin()?.address.clone(); + ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); + let init_msg = self.ecash_init_message(ctx)?; + let init_fut = ctx.admin.instantiate( + code_id, + &init_msg, + format!("{name} contract"), + "contract instantiation from testnet-manager", + Some(InstantiateOptions::default().with_admin(admin)), + None, + ); + let res = ctx.async_with_progress(init_fut).await?; + let address = &res.contract_address; + ctx.println(format!( + "\t{name} contract instantiated with address: {address}", + )); + ctx.network.contracts.ecash.init_info = Some(res.into()); + + ctx.println("\t✅ instantiated all the contracts!"); + + Ok(()) + } + + async fn perform_final_migrations(&self, ctx: &mut InitCtx) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "🧹 {}Performing final migrations and contract cleanup...", + style("[6/8]").bold().dim() + )); + + // migrate mixnet + ctx.set_pb_prefix("[1/2]"); + let name = &ctx.network.contracts.mixnet.name; + let code_id = ctx.network.contracts.mixnet.upload_info()?.code_id; + let address = ctx.network.contracts.mixnet.address()?; + ctx.set_pb_message(format!("attempting to migrate {name} contract...")); + let migrate_msg = self.mixnet_migrate_message(ctx)?; + let client = ctx.mixnet_signing_client()?; + let migrate_fut = client.migrate( + address, + code_id, + &migrate_msg, + "contract migration from testnet-manager", + None, + ); + let migrate_res = ctx.async_with_progress(migrate_fut).await?; + ctx.network.contracts.mixnet.migrate_info = Some(migrate_res.into()); + ctx.println(format!("\t{name} contract has been migrated")); + + // migrate multisig + ctx.set_pb_prefix("[2/2]"); + let name = &ctx.network.contracts.cw3_multisig.name; + let code_id = ctx.network.contracts.cw3_multisig.upload_info()?.code_id; + let address = ctx.network.contracts.cw3_multisig.address()?; + ctx.set_pb_message(format!("attempting to migrate {name} contract...")); + let migrate_msg = self.multisig_migrate_message(ctx)?; + let client = ctx.multisig_signing_client()?; + let migrate_fut = client.migrate( + address, + code_id, + &migrate_msg, + "contract migration from testnet-manager", + None, + ); + let migrate_res = ctx.async_with_progress(migrate_fut).await?; + ctx.network.contracts.cw3_multisig.migrate_info = Some(migrate_res.into()); + ctx.println(format!("\t{name} contract has been migrated")); + + ctx.println("\t✅ performed all the needed migrations!"); + + Ok(()) + } + + async fn get_build_info(&self, ctx: &mut InitCtx) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "🏗️ {}Obtaining contracts build information", + style("[7/8]").bold().dim() + )); + + let total = ctx.network.contracts.count() as u64; + + let pb = &ctx.progress.progress_bar; + for (progress, contract) in ctx + .network + .contracts + .fake_iter_mut() + .into_iter() + .enumerate() + { + pb.set_prefix(format!("[{}/{total}]", progress + 1)); + let name = &contract.name; + let address = contract.address()?; + pb.set_message(format!("querying {name} contract...")); + let build_info_fut = ctx.admin.try_get_contract_build_information(address); + let build_info = async_with_progress(build_info_fut, &ctx.progress.progress_bar) + .await + .ok_or_else(|| NetworkManagerError::MissingBuildInfo { + name: name.to_string(), + })?; + + let now = OffsetDateTime::now_utc(); + // SAFETY: all the information saved in our contracts should be well-formed + let commit_timestamp = OffsetDateTime::parse(&build_info.commit_timestamp, &Rfc3339) + .expect("malformed commit timestamp"); + + let age = now - commit_timestamp; + + pb.println(format!( + "\t{name} contract was built from branch: {} (sha: {}); age: {}", + build_info.commit_branch, + build_info.commit_sha, + HumanDuration(age.unsigned_abs()) + )); + + if age > time::Duration::days(30) { + pb.println(format!( + "\t\t️☠️️ {}", + style("this commit is ANCIENT - please double check if this is intended") + .bold() + .red() + )) + } else if age > time::Duration::days(7) { + pb.println(format!( + "\t\t️❗️ {}", + style("this commit is rather old - please double check if this is intended") + .bold() + .red() + )) + } else if age > time::Duration::days(1) { + pb.println(format!( + "\t\t️️⚠️ {}", + style("this commit seems outdated - please double check if this is intended") + .bold() + .yellow() + )) + } + + contract.build_info = Some(build_info); + } + + ctx.println("\t✅ updated all contract metadata!"); + + Ok(()) + } + + async fn persist_network_in_database(&self, ctx: &InitCtx) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "📦 {}Storing all the results in the database", + style("[8/8]").bold().dim() + )); + + ctx.set_pb_message("attempting to persist network data..."); + let save_future = self.storage.persist_network(&ctx.network); + ctx.async_with_progress(save_future).await?; + + ctx.println("\t✅ the network information got persisted in the database for future use"); + + Ok(()) + } + + pub(crate) async fn initialise_new_network>( + &self, + contracts: P, + network_name: Option, + custom_epoch_duration: Option, + ) -> Result { + let network_name = self.get_network_name(network_name); + let mut ctx = InitCtx::new(network_name, self.admin.deref().clone(), &self.rpc_endpoint)?; + + self.find_contracts(&mut ctx, contracts)?; + self.upload_contracts(&mut ctx).await?; + self.create_contract_admins_mnemonics(&mut ctx)?; + self.transfer_admin_tokens(&ctx).await?; + self.instantiate_contracts(&mut ctx, custom_epoch_duration) + .await?; + self.perform_final_migrations(&mut ctx).await?; + self.get_build_info(&mut ctx).await?; + self.persist_network_in_database(&ctx).await?; + + Ok(ctx.network.clone()) + } +} diff --git a/tools/internal/testnet-manager/src/manager/node.rs b/tools/internal/testnet-manager/src/manager/node.rs new file mode 100644 index 0000000000..e0c6d6981f --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/node.rs @@ -0,0 +1,103 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::manager::contract::Account; +use nym_coconut_dkg_common::types::Addr; +use nym_contracts_common::signing::MessageSignature; +use nym_contracts_common::Percent; +use nym_mixnet_contract_common::{ + construct_gateway_bonding_sign_payload, construct_mixnode_bonding_sign_payload, Gateway, + MixNode, MixNodeCostParams, +}; +use nym_validator_client::nyxd::CosmWasmCoin; + +pub(crate) struct NymNode { + // host is always 127.0.0.1 + pub(crate) mix_port: u16, + pub(crate) verloc_port: u16, + pub(crate) http_port: u16, + pub(crate) clients_port: u16, + pub(crate) sphinx_key: String, + pub(crate) identity_key: String, + pub(crate) version: String, + + pub(crate) owner: Account, + pub(crate) bonding_signature: String, +} + +impl NymNode { + pub(crate) fn new_empty() -> NymNode { + NymNode { + mix_port: 0, + verloc_port: 0, + http_port: 0, + clients_port: 0, + sphinx_key: "".to_string(), + identity_key: "".to_string(), + version: "".to_string(), + owner: Account::new(), + bonding_signature: "".to_string(), + } + } + + pub(crate) fn pledge(&self) -> CosmWasmCoin { + CosmWasmCoin::new(100_000000, "unym") + } + + pub(crate) fn gateway(&self) -> Gateway { + Gateway { + host: "127.0.0.1".to_string(), + mix_port: self.mix_port, + clients_port: self.clients_port, + location: "foomp".to_string(), + sphinx_key: self.sphinx_key.clone(), + identity_key: self.identity_key.clone(), + version: self.version.clone(), + } + } + + pub(crate) fn mixnode(&self) -> MixNode { + MixNode { + host: "127.0.0.1".to_string(), + mix_port: self.mix_port, + verloc_port: self.verloc_port, + http_api_port: self.http_port, + sphinx_key: self.sphinx_key.clone(), + identity_key: self.identity_key.clone(), + version: self.version.clone(), + } + } + + pub(crate) fn cost_params(&self) -> MixNodeCostParams { + MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(10).unwrap(), + interval_operating_cost: CosmWasmCoin::new(40_000000, "unym"), + } + } + + pub(crate) fn bonding_signature(&self) -> MessageSignature { + // this is a valid bs58 string + self.bonding_signature.parse().unwrap() + } + + pub(crate) fn mixnode_bonding_payload(&self) -> String { + let payload = construct_mixnode_bonding_sign_payload( + 0, + Addr::unchecked(self.owner.address.to_string()), + self.pledge(), + self.mixnode(), + self.cost_params(), + ); + payload.to_base58_string().unwrap() + } + + pub(crate) fn gateway_bonding_payload(&self) -> String { + let payload = construct_gateway_bonding_sign_payload( + 0, + Addr::unchecked(self.owner.address.to_string()), + self.pledge(), + self.gateway(), + ); + payload.to_base58_string().unwrap() + } +} diff --git a/tools/internal/testnet-manager/src/manager/storage/manager.rs b/tools/internal/testnet-manager/src/manager/storage/manager.rs new file mode 100644 index 0000000000..24132db687 --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/storage/manager.rs @@ -0,0 +1,205 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only +use crate::manager::storage::models::{RawAccount, RawContract, RawNetwork}; +use time::OffsetDateTime; + +#[derive(Clone)] +pub(crate) struct StorageManager { + pub(crate) connection_pool: sqlx::SqlitePool, +} + +// all SQL goes here +impl StorageManager { + pub(crate) async fn metadata_set(&self) -> Result { + Ok(sqlx::query("SELECT id FROM metadata") + .fetch_optional(&self.connection_pool) + .await? + .is_some()) + } + + pub(crate) async fn get_master_mnemonic(&self) -> Result, sqlx::Error> { + sqlx::query!("SELECT master_mnemonic FROM metadata") + .fetch_optional(&self.connection_pool) + .await + .map(|maybe_record| maybe_record.map(|r| r.master_mnemonic)) + } + + pub(crate) async fn get_rpc_endpoint(&self) -> Result, sqlx::Error> { + sqlx::query!("SELECT rpc_endpoint FROM metadata") + .fetch_optional(&self.connection_pool) + .await + .map(|maybe_record| maybe_record.map(|r| r.rpc_endpoint)) + } + + pub(crate) async fn get_latest_network_id(&self) -> Result, sqlx::Error> { + let maybe_record = sqlx::query!("SELECT latest_network_id FROM metadata") + .fetch_optional(&self.connection_pool) + .await?; + Ok(maybe_record.and_then(|r| r.latest_network_id)) + } + + pub(crate) async fn get_network_name(&self, network_id: i64) -> Result { + sqlx::query!("SELECT name FROM network WHERE id = ?", network_id) + .fetch_one(&self.connection_pool) + .await + .map(|record| record.name) + } + + pub(crate) async fn set_initial_metadata( + &self, + master_mnemonic: &str, + rpc_endpoint: &str, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO metadata (id, master_mnemonic, rpc_endpoint) VALUES (0, ?, ?)", + master_mnemonic, + rpc_endpoint + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn save_latest_network_id( + &self, + latest_network_id: i64, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE metadata SET latest_network_id = ?", + latest_network_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) async fn save_network( + &self, + name: &str, + created_at: OffsetDateTime, + mixnet_id: i64, + vesting_id: i64, + ecash_id: i64, + cw3_id: i64, + cw4_id: i64, + dkg_id: i64, + rewarder_address: &str, + ecash_holding_address: &str, + ) -> Result { + let network_id = sqlx::query!( + r#" + INSERT INTO network ( + name, + created_at, + mixnet_contract_id, + vesting_contract_id, + ecash_contract_id, + cw3_multisig_contract_id, + cw4_group_contract_id, + dkg_contract_id, + rewarder_address, + ecash_holding_account_address + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + "#, + name, + created_at, + mixnet_id, + vesting_id, + ecash_id, + cw3_id, + cw4_id, + dkg_id, + rewarder_address, + ecash_holding_address, + ) + .execute(&self.connection_pool) + .await? + .last_insert_rowid(); + Ok(network_id) + } + + pub(crate) async fn load_network(&self, name: &str) -> Result { + sqlx::query_as("SELECT * FROM network WHERE name = ?") + .bind(name) + .fetch_one(&self.connection_pool) + .await + } + + pub(crate) async fn save_contract( + &self, + name: &str, + address: &str, + admin_address: &str, + ) -> Result { + let id = sqlx::query!( + "INSERT INTO contract (name, address, admin_address) VALUES (?, ?, ?)", + name, + address, + admin_address + ) + .execute(&self.connection_pool) + .await? + .last_insert_rowid(); + Ok(id) + } + + pub(crate) async fn load_contract(&self, id: i64) -> Result { + sqlx::query_as( + r#" + SELECT t1.id, t1.name, t1.address, t1.admin_address, t2.mnemonic + FROM contract t1 + JOIN account t2 ON t1.admin_address = t2.address + WHERE t1.id = ?"#, + ) + .bind(id) + .fetch_one(&self.connection_pool) + .await + } + + pub(crate) async fn save_account( + &self, + address: &str, + mnemonic: &str, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO account (address, mnemonic) VALUES (?, ?)", + address, + mnemonic + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn load_account(&self, address: &str) -> Result { + sqlx::query_as("SELECT * FROM account WHERE address = ?") + .bind(address) + .fetch_one(&self.connection_pool) + .await + } + + pub(crate) async fn save_node( + &self, + identity_key: &str, + network_id: i64, + bonded_type: &str, + owner_address: &str, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO node(identity_key, network_id, bonded_type, owner_address) + VALUES (?, ?, ?, ?) + "#, + identity_key, + network_id, + bonded_type, + owner_address + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } +} diff --git a/tools/internal/testnet-manager/src/manager/storage/mod.rs b/tools/internal/testnet-manager/src/manager/storage/mod.rs new file mode 100644 index 0000000000..6faaab0070 --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/storage/mod.rs @@ -0,0 +1,301 @@ +use std::fs; +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only +use crate::error::NetworkManagerError; +use crate::manager::contract::{Account, Contract, LoadedNymContracts}; +use crate::manager::network::{LoadedNetwork, Network, SpecialAddresses}; +use crate::manager::node::NymNode; +use crate::manager::storage::manager::StorageManager; +use sqlx::ConnectOptions; +use std::path::Path; +use tracing::{error, info}; +use url::Url; +use zeroize::Zeroizing; + +mod manager; +mod models; + +#[derive(Clone)] +pub(crate) struct NetworkManagerStorage { + manager: StorageManager, +} + +impl NetworkManagerStorage { + pub async fn init>(database_path: P) -> Result { + let database_path = database_path.as_ref(); + info!( + "attempting to initialise storage at {}", + database_path.display() + ); + + if let Some(parent) = database_path.parent() { + fs::create_dir_all(parent)?; + } + + // TODO: we can inject here more stuff based on our nym-api global config + // struct. Maybe different pool size or timeout intervals? + let mut opts = sqlx::sqlite::SqliteConnectOptions::new() + .filename(database_path) + .create_if_missing(true); + + // TODO: do we want auto_vacuum ? + + opts.disable_statement_logging(); + + let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { + Ok(db) => db, + Err(err) => { + error!("Failed to connect to SQLx database: {err}"); + return Err(err.into()); + } + }; + + if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await { + error!("Failed to initialize SQLx database: {err}"); + return Err(err.into()); + } + + info!("Database migration finished!"); + + let storage = NetworkManagerStorage { + manager: StorageManager { connection_pool }, + }; + + Ok(storage) + } + + pub(crate) async fn metadata_set(&self) -> Result { + Ok(self.manager.metadata_set().await?) + } + + pub(crate) async fn get_master_mnemonic( + &self, + ) -> Result, NetworkManagerError> { + Ok(self + .manager + .get_master_mnemonic() + .await? + .map(|m| m.parse()) + .transpose()?) + } + + pub(crate) async fn get_rpc_endpoint(&self) -> Result, NetworkManagerError> { + Ok(self + .manager + .get_rpc_endpoint() + .await? + .map(|m| m.parse()) + .transpose()?) + } + + pub(crate) async fn get_latest_network_name(&self) -> Result { + let Some(id) = self.manager.get_latest_network_id().await? else { + return Err(NetworkManagerError::NoNetworksInitialised); + }; + Ok(self.manager.get_network_name(id).await?) + } + + pub(crate) async fn set_initial_metadata( + &self, + master_mnemonic: &bip39::Mnemonic, + rpc_endpoint: &Url, + ) -> Result<(), NetworkManagerError> { + let master = Zeroizing::new(master_mnemonic.to_string()); + Ok(self + .manager + .set_initial_metadata(master.as_str(), rpc_endpoint.as_str()) + .await?) + } + + async fn persist_contract(&self, contract: &Contract) -> Result { + Ok(self + .manager + .save_contract( + &contract.name, + contract.init_info()?.contract_address.as_ref(), + contract.admin()?.address.as_ref(), + ) + .await?) + } + + async fn persist_mixnode( + &self, + node: &NymNode, + network_id: i64, + ) -> Result<(), NetworkManagerError> { + Ok(self + .manager + .save_node( + &node.identity_key, + network_id, + "mixnode", + node.owner.address.as_ref(), + ) + .await?) + } + + async fn persist_gateway( + &self, + node: &NymNode, + network_id: i64, + ) -> Result<(), NetworkManagerError> { + Ok(self + .manager + .save_node( + &node.identity_key, + network_id, + "gateway", + node.owner.address.as_ref(), + ) + .await?) + } + + async fn persist_account(&self, account: &Account) -> Result<(), NetworkManagerError> { + let as_str = Zeroizing::new(account.mnemonic.to_string()); + Ok(self + .manager + .save_account(account.address.as_ref(), as_str.as_str()) + .await?) + } + + pub(crate) async fn persist_mixnodes( + &self, + nodes: &[NymNode], + network_id: i64, + ) -> Result<(), NetworkManagerError> { + for node in nodes { + self.persist_account(&node.owner).await?; + self.persist_mixnode(node, network_id).await?; + } + Ok(()) + } + + pub(crate) async fn persist_gateways( + &self, + nodes: &[NymNode], + network_id: i64, + ) -> Result<(), NetworkManagerError> { + for node in nodes { + self.persist_account(&node.owner).await?; + self.persist_gateway(node, network_id).await?; + } + Ok(()) + } + + pub(crate) async fn persist_network( + &self, + network: &Network, + ) -> Result<(), NetworkManagerError> { + self.persist_account(network.contracts.mixnet.admin()?) + .await?; + self.persist_account(network.contracts.vesting.admin()?) + .await?; + self.persist_account(network.contracts.ecash.admin()?) + .await?; + self.persist_account(network.contracts.cw3_multisig.admin()?) + .await?; + self.persist_account(network.contracts.cw4_group.admin()?) + .await?; + self.persist_account(network.contracts.dkg.admin()?).await?; + + self.persist_account(&network.auxiliary_addresses.mixnet_rewarder) + .await?; + self.persist_account(&network.auxiliary_addresses.ecash_holding_account) + .await?; + + let mixnet_id = self.persist_contract(&network.contracts.mixnet).await?; + let vesting_id = self.persist_contract(&network.contracts.vesting).await?; + let ecash_id = self.persist_contract(&network.contracts.ecash).await?; + let cw3_multisig_id = self + .persist_contract(&network.contracts.cw3_multisig) + .await?; + let cw4_group_id = self.persist_contract(&network.contracts.cw4_group).await?; + let dkg_id = self.persist_contract(&network.contracts.dkg).await?; + + let network_id = self + .manager + .save_network( + &network.name, + network.created_at, + mixnet_id, + vesting_id, + ecash_id, + cw3_multisig_id, + cw4_group_id, + dkg_id, + network.auxiliary_addresses.mixnet_rewarder.address.as_ref(), + network + .auxiliary_addresses + .ecash_holding_account + .address + .as_ref(), + ) + .await?; + + self.manager.save_latest_network_id(network_id).await?; + + Ok(()) + } + + pub(crate) async fn try_load_network( + &self, + name: &str, + ) -> Result { + let base_network = self.manager.load_network(name).await?; + let rpc_endpoint = self + .get_rpc_endpoint() + .await? + .ok_or_else(|| NetworkManagerError::RpcEndpointNotSet)?; + + Ok(LoadedNetwork { + id: base_network.id, + name: base_network.name, + rpc_endpoint, + created_at: base_network.created_at, + contracts: LoadedNymContracts { + mixnet: self + .manager + .load_contract(base_network.mixnet_contract_id) + .await? + .try_into()?, + vesting: self + .manager + .load_contract(base_network.vesting_contract_id) + .await? + .try_into()?, + ecash: self + .manager + .load_contract(base_network.ecash_contract_id) + .await? + .try_into()?, + cw3_multisig: self + .manager + .load_contract(base_network.cw3_multisig_contract_id) + .await? + .try_into()?, + cw4_group: self + .manager + .load_contract(base_network.cw4_group_contract_id) + .await? + .try_into()?, + dkg: self + .manager + .load_contract(base_network.dkg_contract_id) + .await? + .try_into()?, + }, + auxiliary_addresses: SpecialAddresses { + ecash_holding_account: self + .manager + .load_account(&base_network.ecash_holding_account_address) + .await? + .try_into()?, + mixnet_rewarder: self + .manager + .load_account(&base_network.rewarder_address) + .await? + .try_into()?, + }, + }) + } +} diff --git a/tools/internal/testnet-manager/src/manager/storage/models.rs b/tools/internal/testnet-manager/src/manager/storage/models.rs new file mode 100644 index 0000000000..c224fc36d5 --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/storage/models.rs @@ -0,0 +1,76 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NetworkManagerError; +use crate::manager::contract::{Account, LoadedContract}; +use sqlx::FromRow; +use time::OffsetDateTime; + +#[derive(FromRow)] +pub(crate) struct RawAccount { + pub(crate) address: String, + pub(crate) mnemonic: String, +} + +impl TryFrom for Account { + type Error = NetworkManagerError; + + fn try_from(value: RawAccount) -> Result { + Ok(Account { + address: value + .address + .parse() + .map_err(|_| NetworkManagerError::MalformedAccountAddress)?, + mnemonic: value.mnemonic.parse()?, + }) + } +} + +#[derive(FromRow)] +pub(crate) struct RawContract { + #[allow(unused)] + pub(crate) id: i64, + pub(crate) name: String, + pub(crate) address: String, + pub(crate) admin_address: String, + pub(crate) mnemonic: String, +} + +impl TryFrom for LoadedContract { + type Error = NetworkManagerError; + + fn try_from(value: RawContract) -> Result { + Ok(LoadedContract { + name: value.name, + address: value + .address + .parse() + .map_err(|_| NetworkManagerError::MalformedAccountAddress)?, + admin_address: value + .admin_address + .parse() + .map_err(|_| NetworkManagerError::MalformedAccountAddress)?, + admin_mnemonic: value + .mnemonic + .parse() + .map_err(|_| NetworkManagerError::MalformedAccountAddress)?, + }) + } +} + +#[derive(FromRow)] +pub(crate) struct RawNetwork { + pub(crate) id: i64, + pub(crate) name: String, + pub(crate) created_at: OffsetDateTime, + + pub(crate) mixnet_contract_id: i64, + pub(crate) vesting_contract_id: i64, + pub(crate) ecash_contract_id: i64, + pub(crate) cw3_multisig_contract_id: i64, + pub(crate) cw4_group_contract_id: i64, + pub(crate) dkg_contract_id: i64, + + pub(crate) rewarder_address: String, + pub(crate) ecash_holding_account_address: String, +} diff --git a/tools/nym-cli/Cargo.toml b/tools/nym-cli/Cargo.toml index e590e45141..61848f161d 100644 --- a/tools/nym-cli/Cargo.toml +++ b/tools/nym-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-cli" -version = "1.1.40" +version = "1.1.41" authors.workspace = true edition = "2021" license.workspace = true diff --git a/tools/nym-cli/src/coconut/mod.rs b/tools/nym-cli/src/coconut/mod.rs deleted file mode 100644 index 2939175aed..0000000000 --- a/tools/nym-cli/src/coconut/mod.rs +++ /dev/null @@ -1,36 +0,0 @@ -use nym_cli_commands::context::{create_query_client, create_signing_client, ClientArgs}; -use nym_network_defaults::NymNetworkDetails; - -pub(crate) async fn execute( - global_args: ClientArgs, - coconut: nym_cli_commands::coconut::Coconut, - network_details: &NymNetworkDetails, -) -> anyhow::Result<()> { - match coconut.command { - nym_cli_commands::coconut::CoconutCommands::GenerateFreepass(args) => { - nym_cli_commands::coconut::generate_freepass::execute( - args, - create_signing_client(global_args, network_details)?, - ) - .await? - } - nym_cli_commands::coconut::CoconutCommands::IssueCredentials(args) => { - nym_cli_commands::coconut::issue_credentials::execute( - args, - create_signing_client(global_args, network_details)?, - ) - .await? - } - nym_cli_commands::coconut::CoconutCommands::RecoverCredentials(args) => { - nym_cli_commands::coconut::recover_credentials::execute( - args, - create_query_client(network_details)?, - ) - .await? - } - nym_cli_commands::coconut::CoconutCommands::ImportCredential(args) => { - nym_cli_commands::coconut::import_credential::execute(args).await? - } - } - Ok(()) -} diff --git a/tools/nym-cli/src/ecash/mod.rs b/tools/nym-cli/src/ecash/mod.rs new file mode 100644 index 0000000000..1cc3d00020 --- /dev/null +++ b/tools/nym-cli/src/ecash/mod.rs @@ -0,0 +1,32 @@ +use nym_cli_commands::context::{create_query_client, create_signing_client, ClientArgs}; +use nym_network_defaults::NymNetworkDetails; + +pub(crate) async fn execute( + global_args: ClientArgs, + coconut: nym_cli_commands::ecash::Ecash, + network_details: &NymNetworkDetails, +) -> anyhow::Result<()> { + match coconut.command { + nym_cli_commands::ecash::EcashCommands::IssueTicketBook(args) => { + nym_cli_commands::ecash::issue_ticket_book::execute( + args, + create_signing_client(global_args, network_details)?, + ) + .await? + } + nym_cli_commands::ecash::EcashCommands::RecoverTicketBook(args) => { + nym_cli_commands::ecash::recover_ticket_book::execute( + args, + create_query_client(network_details)?, + ) + .await? + } + nym_cli_commands::ecash::EcashCommands::ImportTicketBook(args) => { + nym_cli_commands::ecash::import_ticket_book::execute(args).await? + } + nym_cli_commands::ecash::EcashCommands::GenerateTicket(args) => { + nym_cli_commands::ecash::generate_ticket::execute(args).await? + } + } + Ok(()) +} diff --git a/tools/nym-cli/src/main.rs b/tools/nym-cli/src/main.rs index 16b771d33e..c5b4cc51e3 100644 --- a/tools/nym-cli/src/main.rs +++ b/tools/nym-cli/src/main.rs @@ -7,8 +7,8 @@ use nym_bin_common::logging::setup_logging; use nym_cli_commands::context::{get_network_details, ClientArgs}; use nym_validator_client::nyxd::AccountId; -mod coconut; mod completion; +mod ecash; mod validator; #[derive(Debug, Parser)] @@ -62,8 +62,8 @@ pub(crate) enum Commands { Account(nym_cli_commands::validator::account::Account), /// Sign and verify messages Signature(nym_cli_commands::validator::signature::Signature), - /// Coconut related stuff - Coconut(nym_cli_commands::coconut::Coconut), + /// Ecash related stuff + Ecash(nym_cli_commands::ecash::Ecash), /// Query chain blocks Block(nym_cli_commands::validator::block::Block), /// Manage and execute WASM smart contracts @@ -104,7 +104,7 @@ async fn execute(cli: Cli) -> anyhow::Result<()> { Commands::Signature(signature) => { validator::signature::execute(signature, &network_details, mnemonic).await? } - Commands::Coconut(coconut) => coconut::execute(args, coconut, &network_details).await?, + Commands::Ecash(coconut) => ecash::execute(args, coconut, &network_details).await?, Commands::Block(block) => validator::block::execute(block, &network_details).await?, Commands::Cosmwasm(cosmwasm) => { validator::cosmwasm::execute(args, cosmwasm, &network_details).await? diff --git a/tools/nym-cli/src/validator/cosmwasm/generators/mod.rs b/tools/nym-cli/src/validator/cosmwasm/generators/mod.rs index aba0aaa9a5..5c576e5991 100644 --- a/tools/nym-cli/src/validator/cosmwasm/generators/mod.rs +++ b/tools/nym-cli/src/validator/cosmwasm/generators/mod.rs @@ -10,9 +10,9 @@ pub(crate) async fn execute( args, ) => nym_cli_commands::validator::cosmwasm::generators::vesting::generate(args).await, - nym_cli_commands::validator::cosmwasm::generators::GenerateMessageCommands::CoconutBandwidth( + nym_cli_commands::validator::cosmwasm::generators::GenerateMessageCommands::EcashBandwidth( args, - ) => nym_cli_commands::validator::cosmwasm::generators::coconut_bandwidth::generate(args).await, + ) => nym_cli_commands::validator::cosmwasm::generators::ecash_bandwidth::generate(args).await, nym_cli_commands::validator::cosmwasm::generators::GenerateMessageCommands::CoconutDKG( args, diff --git a/tools/nym-cli/src/validator/mixnet/delegators/mod.rs b/tools/nym-cli/src/validator/mixnet/delegators/mod.rs index 8ccfbd59a3..08e5df592a 100644 --- a/tools/nym-cli/src/validator/mixnet/delegators/mod.rs +++ b/tools/nym-cli/src/validator/mixnet/delegators/mod.rs @@ -34,7 +34,10 @@ pub(crate) async fn execute( } nym_cli_commands::validator::mixnet::delegators::MixnetDelegatorsCommands::List(args) => { nym_cli_commands::validator::mixnet::delegators::query_for_delegations::execute(args, create_signing_client_with_nym_api(global_args, network_details)?).await - } + }, + nym_cli_commands::validator::mixnet::delegators::MixnetDelegatorsCommands::MigrateVestedDelegation(args) => { + nym_cli_commands::validator::mixnet::delegators::migrate_vested_delegation::migrate_vested_delegation(args, create_signing_client(global_args, network_details)?).await + }, } Ok(()) } diff --git a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/mod.rs b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/mod.rs index b3cf6d4e28..ac2d301f4f 100644 --- a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/mod.rs +++ b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/mod.rs @@ -48,7 +48,10 @@ pub(crate) async fn execute( nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::DecreasePledgeVesting(args) => { nym_cli_commands::validator::mixnet::operators::mixnode::vesting_decrease_pledge::vesting_decrease_pledge(args, create_signing_client(global_args, network_details)?).await } - _ => unreachable!(), + nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::MigrateVestedNode(args) => { + nym_cli_commands::validator::mixnet::operators::mixnode::migrate_vested_mixnode::migrate_vested_mixnode(args, create_signing_client(global_args, network_details)?).await + } + _ => unreachable!() } Ok(()) } diff --git a/tools/nymvisor/Cargo.toml b/tools/nymvisor/Cargo.toml index 8f5658c8d3..6fb6a36e5e 100644 --- a/tools/nymvisor/Cargo.toml +++ b/tools/nymvisor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nymvisor" -version = "0.1.5" +version = "0.1.6" authors.workspace = true repository.workspace = true homepage.workspace = true diff --git a/tools/ts-rs-cli/src/main.rs b/tools/ts-rs-cli/src/main.rs index d5a5878008..e3728bd234 100644 --- a/tools/ts-rs-cli/src/main.rs +++ b/tools/ts-rs-cli/src/main.rs @@ -27,7 +27,9 @@ use nym_types::transaction::{ }; use nym_types::vesting::{OriginalVestingResponse, PledgeData, VestingAccountInfo, VestingPeriod}; use nym_vesting_contract_common::Period; -use nym_wallet_types::admin::TauriContractStateParams; +use nym_wallet_types::admin::{ + TauriContractStateParams, TauriOperatingCostRange, TauriProfitMarginRange, +}; use nym_wallet_types::app::AppEnv; use nym_wallet_types::app::AppVersion; use nym_wallet_types::interval::Interval; @@ -131,6 +133,8 @@ fn main() { do_export!(Interval); do_export!(Network); do_export!(TauriContractStateParams); + do_export!(TauriOperatingCostRange); + do_export!(TauriProfitMarginRange); do_export!(Validator); do_export!(ValidatorUrl); do_export!(ValidatorUrls); diff --git a/wasm/node-tester/Cargo.toml b/wasm/node-tester/Cargo.toml index b2330c04ae..6d3cd6042e 100644 --- a/wasm/node-tester/Cargo.toml +++ b/wasm/node-tester/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" keywords = ["nym", "sphinx", "webassembly", "privacy", "tester"] license = "Apache-2.0" repository = "https://github.com/nymtech/nym" -rust-version = "1.56" +rust-version = "1.64" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/wasm/node-tester/src/tester.rs b/wasm/node-tester/src/tester.rs index 852e03ffbe..b718e9c082 100644 --- a/wasm/node-tester/src/tester.rs +++ b/wasm/node-tester/src/tester.rs @@ -33,8 +33,9 @@ use wasm_client_core::helpers::{ use wasm_client_core::storage::ClientStorage; use wasm_client_core::topology::SerializableNymTopology; use wasm_client_core::{ - nym_task, BandwidthController, ClientKeys, GatewayClient, GatewayConfig, IdentityKey, - InitialisationResult, NodeIdentity, NymTopology, QueryReqwestRpcNyxdClient, Recipient, + nym_task, BandwidthController, ClientKeys, GatewayClient, GatewayClientConfig, GatewayConfig, + IdentityKey, InitialisationResult, NodeIdentity, NymTopology, QueryReqwestRpcNyxdClient, + Recipient, }; use wasm_utils::check_promise_result; use wasm_utils::error::PromisableResult; @@ -201,6 +202,7 @@ impl NymNodeTesterBuilder { gateway_info.gateway_listener.to_string(), ); GatewayClient::new( + GatewayClientConfig::new_default().with_disabled_credentials_mode(true), cfg, managed_keys.identity_keypair(), Some(gateway_info.derived_aes128_ctr_blake3_hmac_keys), @@ -208,8 +210,7 @@ impl NymNodeTesterBuilder { self.bandwidth_controller.take(), gateway_task, ) - } - .with_disabled_credentials_mode(true); + }; gateway_client.authenticate_and_start().await?; diff --git a/wasm/zknym-lib/Cargo.toml b/wasm/zknym-lib/Cargo.toml index 6fdfb431bb..5ef55b0314 100644 --- a/wasm/zknym-lib/Cargo.toml +++ b/wasm/zknym-lib/Cargo.toml @@ -27,11 +27,12 @@ reqwest = { workspace = true } wasmtimer = { workspace = true } zeroize.workspace = true -rand = { version = "0.7.3", features = ["wasm-bindgen"] } +rand = { workspace = true } nym-bin-common = { path = "../../common/bin-common" } nym-coconut = { path = "../../common/nymcoconut", features = ["key-zeroize"] } +nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } nym-credentials = { path = "../../common/credentials" } nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } nym-http-api-client = { path = "../../common/http-api-client" } diff --git a/wasm/zknym-lib/src/bandwidth_voucher.rs b/wasm/zknym-lib/src/bandwidth_voucher.rs index b047550c1e..d64fdd58f3 100644 --- a/wasm/zknym-lib/src/bandwidth_voucher.rs +++ b/wasm/zknym-lib/src/bandwidth_voucher.rs @@ -1,201 +1,201 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::error::ZkNymError; -use crate::generic_scheme::get_params; -use crate::types::{CredentialWrapper, ParametersWrapper, UnblindableShare}; -use crate::vpn_api_client::types::{ - AttributesResponse, BandwidthVoucherResponse, PartialVerificationKeysResponse, -}; -use nym_coconut::{ - Base58, BlindSignRequest, BlindedSignature, PrivateAttribute, PublicAttribute, Scalar, - SignatureShare, VerificationKey, -}; -use nym_credentials::coconut::bandwidth::issuance::Coin; -use nym_credentials::coconut::bandwidth::issued::BandwidthCredentialIssuedDataVariant; -use nym_credentials::coconut::bandwidth::voucher::BandwidthVoucherIssuedData; -use nym_credentials::IssuedBandwidthCredential; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use tsify::Tsify; -use wasm_bindgen::prelude::wasm_bindgen; -use wasm_utils::console_error; -use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; - -// tiny 'hacks' to just allow passing responses from vpn-api queries -pub type NymIssuanceBandwidthVoucherOpts = AttributesResponse; -pub type VoucherShares = BandwidthVoucherResponse; -pub type VoucherIssuers = PartialVerificationKeysResponse; - -#[wasm_bindgen] -#[derive(Debug)] -#[allow(dead_code)] -pub struct NymIssuanceBandwidthVoucher { - serial_number: PrivateAttribute, - - binding_number: PrivateAttribute, - - credential_amount: Coin, - - prehashed_amount: PublicAttribute, - prehashed_type: PublicAttribute, - - blind_sign_request: BlindSignRequest, - pedersen_commitments_openings: Zeroizing>, -} - -#[wasm_bindgen] -impl NymIssuanceBandwidthVoucher { - #[wasm_bindgen(js_name = "prepareNew", constructor)] - pub fn prepare_new( - opts: NymIssuanceBandwidthVoucherOpts, - parameters: Option, - ) -> Result { - let deposit_amount: u128 = opts - .credential_amount_string - .parse() - .map_err(|_| ZkNymError::InvalidDepositAmount)?; - let credential_amount = Coin::new(deposit_amount, opts.credential_amount_denom); - - let params = get_params(¶meters); - let serial_number = params.random_scalar(); - let binding_number = params.random_scalar(); - - let prehashed_amount = Scalar::try_from_bs58(opts.bs58_prehashed_amount)?; - let prehashed_type = Scalar::try_from_bs58(opts.bs58_prehashed_type)?; - - let public_attributes = vec![&prehashed_amount, &prehashed_type]; - let private_attributes = vec![&serial_number, &binding_number]; - - let (pedersen_commitments_openings, blind_sign_request) = - nym_coconut::prepare_blind_sign(params, &private_attributes, &public_attributes)?; - - Ok(NymIssuanceBandwidthVoucher { - serial_number, - binding_number, - credential_amount, - prehashed_amount, - prehashed_type, - blind_sign_request, - pedersen_commitments_openings: Zeroizing::new(pedersen_commitments_openings), - }) - } - - #[wasm_bindgen(js_name = "getBlindSignRequest")] - pub fn get_blind_sign_request(&self) -> String { - self.blind_sign_request.to_bs58() - } - - #[wasm_bindgen(js_name = "unblindShare")] - pub fn unblind_share(&self, share: UnblindableShare) -> Result { - let blinded_sig = BlindedSignature::try_from_bs58(share.blinded_share_bs58)?; - let vk = VerificationKey::try_from_bs58(share.issuer_key_bs58)?; - - Ok(blinded_sig - .unblind(&vk, &self.pedersen_commitments_openings) - .into()) - } - - #[wasm_bindgen(js_name = "unblindShares")] - pub fn unblind_shares( - self, - shares: VoucherShares, - issuers: VoucherIssuers, - ) -> Result { - if shares.epoch_id != issuers.epoch_id { - console_error!( - "the provided shares and issuers are not from the same epoch! {} and {}", - shares.epoch_id, - issuers.epoch_id - ); - return Err(ZkNymError::InconsistentEpochId { - shares: shares.epoch_id, - issuers: issuers.epoch_id, - }); - } - - let mut decoded_keys = HashMap::new(); - for key in issuers.keys { - let vk = VerificationKey::try_from_bs58(key.bs58_encoded_key)?; - decoded_keys.insert(key.node_index, vk); - } - - let mut credential_shares = Vec::new(); - for share in shares.shares { - let blinded_sig = BlindedSignature::try_from_bs58(share.bs58_encoded_share)?; - let Some(vk) = decoded_keys.get(&share.node_index) else { - console_error!("received a share from issuer {} but did not receive a corresponding verification key!", share.node_index); - continue; - }; - let unblinded_sig = blinded_sig.unblind(vk, &self.pedersen_commitments_openings); - credential_shares.push(SignatureShare::new(unblinded_sig, share.node_index)); - } - - let signature = nym_coconut::aggregate_signature_shares(&credential_shares)?; - - let voucher_data = BandwidthCredentialIssuedDataVariant::Voucher( - BandwidthVoucherIssuedData::new(self.credential_amount), - ); - - Ok(NymIssuedBandwidthVoucher { - inner: IssuedBandwidthCredential::new( - self.serial_number, - self.binding_number, - signature, - voucher_data, - self.prehashed_type, - shares.epoch_id, - ), - }) - } -} - -#[wasm_bindgen] -pub struct NymIssuedBandwidthVoucher { - inner: IssuedBandwidthCredential, -} - -#[wasm_bindgen] -impl NymIssuedBandwidthVoucher { - #[wasm_bindgen(js_name = "ensureIsValid")] - pub fn ensure_is_valid( - &self, - master_vk: String, - parameters: Option, - ) -> bool { - let params = get_params(¶meters); - let Ok(master_vk) = VerificationKey::try_from_bs58(master_vk) else { - console_error!("malformed master verification key"); - return false; - }; - - let spending_req = match self.inner.prepare_for_spending(&master_vk) { - Ok(req) => req, - Err(err) => { - console_error!("failed to prepare spending request: {err}"); - return false; - } - }; - - spending_req.verify(params, &master_vk) - } - - pub fn serialise(self) -> SerialisedNymIssuedBandwidthVoucher { - SerialisedNymIssuedBandwidthVoucher { - serialisation_revision: - nym_credentials::coconut::bandwidth::issued::CURRENT_SERIALIZATION_REVISION, - bs58_encoded_data: bs58::encode(&self.inner.pack_v1()).into_string(), - } - } -} - -#[derive(Tsify, Serialize, Deserialize, Debug, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[serde(rename_all = "camelCase")] -pub struct SerialisedNymIssuedBandwidthVoucher { - pub serialisation_revision: u8, - pub bs58_encoded_data: String, -} +// use crate::error::ZkNymError; +// use crate::generic_scheme::get_params; +// use crate::types::{CredentialWrapper, ParametersWrapper, UnblindableShare}; +// use crate::vpn_api_client::types::{ +// AttributesResponse, BandwidthVoucherResponse, PartialVerificationKeysResponse, +// }; +// use nym_coconut::{ +// Base58, BlindSignRequest, BlindedSignature, PrivateAttribute, PublicAttribute, Scalar, +// SignatureShare, VerificationKey, +// }; +// use nym_credentials::ecash::bandwidth::issuance::Coin; +// use nym_credentials::ecash::bandwidth::issued::BandwidthCredentialIssuedDataVariant; +// use nym_credentials::ecash::bandwidth::voucher::BandwidthVoucherIssuedData; +// use nym_credentials::IssuedBandwidthCredential; +// use serde::{Deserialize, Serialize}; +// use std::collections::HashMap; +// use tsify::Tsify; +// use wasm_bindgen::prelude::wasm_bindgen; +// use wasm_utils::console_error; +// use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; +// +// // tiny 'hacks' to just allow passing responses from vpn-api queries +// pub type NymIssuanceBandwidthVoucherOpts = AttributesResponse; +// pub type VoucherShares = BandwidthVoucherResponse; +// pub type VoucherIssuers = PartialVerificationKeysResponse; +// +// #[wasm_bindgen] +// #[derive(Debug)] +// #[allow(dead_code)] +// pub struct NymIssuanceBandwidthVoucher { +// serial_number: PrivateAttribute, +// +// binding_number: PrivateAttribute, +// +// credential_amount: Coin, +// +// prehashed_amount: PublicAttribute, +// prehashed_type: PublicAttribute, +// +// blind_sign_request: BlindSignRequest, +// pedersen_commitments_openings: Zeroizing>, +// } +// +// #[wasm_bindgen] +// impl NymIssuanceBandwidthVoucher { +// #[wasm_bindgen(js_name = "prepareNew", constructor)] +// pub fn prepare_new( +// opts: NymIssuanceBandwidthVoucherOpts, +// parameters: Option, +// ) -> Result { +// let deposit_amount: u128 = opts +// .credential_amount_string +// .parse() +// .map_err(|_| ZkNymError::InvalidDepositAmount)?; +// let credential_amount = Coin::new(deposit_amount, opts.credential_amount_denom); +// +// let params = get_params(¶meters); +// let serial_number = params.random_scalar(); +// let binding_number = params.random_scalar(); +// +// let prehashed_amount = Scalar::try_from_bs58(opts.bs58_prehashed_amount)?; +// let prehashed_type = Scalar::try_from_bs58(opts.bs58_prehashed_type)?; +// +// let public_attributes = vec![&prehashed_amount, &prehashed_type]; +// let private_attributes = vec![&serial_number, &binding_number]; +// +// let (pedersen_commitments_openings, blind_sign_request) = +// nym_coconut::prepare_blind_sign(params, &private_attributes, &public_attributes)?; +// +// Ok(NymIssuanceBandwidthVoucher { +// serial_number, +// binding_number, +// credential_amount, +// prehashed_amount, +// prehashed_type, +// blind_sign_request, +// pedersen_commitments_openings: Zeroizing::new(pedersen_commitments_openings), +// }) +// } +// +// #[wasm_bindgen(js_name = "getBlindSignRequest")] +// pub fn get_blind_sign_request(&self) -> String { +// self.blind_sign_request.to_bs58() +// } +// +// #[wasm_bindgen(js_name = "unblindShare")] +// pub fn unblind_share(&self, share: UnblindableShare) -> Result { +// let blinded_sig = BlindedSignature::try_from_bs58(share.blinded_share_bs58)?; +// let vk = VerificationKey::try_from_bs58(share.issuer_key_bs58)?; +// +// Ok(blinded_sig +// .unblind(&vk, &self.pedersen_commitments_openings) +// .into()) +// } +// +// #[wasm_bindgen(js_name = "unblindShares")] +// pub fn unblind_shares( +// self, +// shares: VoucherShares, +// issuers: VoucherIssuers, +// ) -> Result { +// if shares.epoch_id != issuers.epoch_id { +// console_error!( +// "the provided shares and issuers are not from the same epoch! {} and {}", +// shares.epoch_id, +// issuers.epoch_id +// ); +// return Err(ZkNymError::InconsistentEpochId { +// shares: shares.epoch_id, +// issuers: issuers.epoch_id, +// }); +// } +// +// let mut decoded_keys = HashMap::new(); +// for key in issuers.keys { +// let vk = VerificationKey::try_from_bs58(key.bs58_encoded_key)?; +// decoded_keys.insert(key.node_index, vk); +// } +// +// let mut credential_shares = Vec::new(); +// for share in shares.shares { +// let blinded_sig = BlindedSignature::try_from_bs58(share.bs58_encoded_share)?; +// let Some(vk) = decoded_keys.get(&share.node_index) else { +// console_error!("received a share from issuer {} but did not receive a corresponding verification key!", share.node_index); +// continue; +// }; +// let unblinded_sig = blinded_sig.unblind(vk, &self.pedersen_commitments_openings); +// credential_shares.push(SignatureShare::new(unblinded_sig, share.node_index)); +// } +// +// let signature = nym_coconut::aggregate_signature_shares(&credential_shares)?; +// +// let voucher_data = BandwidthCredentialIssuedDataVariant::Voucher( +// BandwidthVoucherIssuedData::new(self.credential_amount), +// ); +// +// Ok(NymIssuedBandwidthVoucher { +// inner: IssuedBandwidthCredential::new( +// self.serial_number, +// self.binding_number, +// signature, +// voucher_data, +// self.prehashed_type, +// shares.epoch_id, +// ), +// }) +// } +// } +// +// #[wasm_bindgen] +// pub struct NymIssuedBandwidthVoucher { +// inner: IssuedBandwidthCredential, +// } +// +// #[wasm_bindgen] +// impl NymIssuedBandwidthVoucher { +// #[wasm_bindgen(js_name = "ensureIsValid")] +// pub fn ensure_is_valid( +// &self, +// master_vk: String, +// parameters: Option, +// ) -> bool { +// let params = get_params(¶meters); +// let Ok(master_vk) = VerificationKey::try_from_bs58(master_vk) else { +// console_error!("malformed master verification key"); +// return false; +// }; +// +// let spending_req = match self.inner.prepare_for_spending(&master_vk) { +// Ok(req) => req, +// Err(err) => { +// console_error!("failed to prepare spending request: {err}"); +// return false; +// } +// }; +// +// spending_req.verify(params, &master_vk) +// } +// +// pub fn serialise(self) -> SerialisedNymIssuedBandwidthVoucher { +// SerialisedNymIssuedBandwidthVoucher { +// serialisation_revision: +// nym_credentials::coconut::bandwidth::issued::CURRENT_SERIALIZATION_REVISION, +// bs58_encoded_data: bs58::encode(&self.inner.pack_v1()).into_string(), +// } +// } +// } +// +// #[derive(Tsify, Serialize, Deserialize, Debug, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] +// #[tsify(into_wasm_abi, from_wasm_abi)] +// #[serde(rename_all = "camelCase")] +// pub struct SerialisedNymIssuedBandwidthVoucher { +// pub serialisation_revision: u8, +// pub bs58_encoded_data: String, +// } // #[cfg(test)] // mod tests { diff --git a/wasm/zknym-lib/src/error.rs b/wasm/zknym-lib/src/error.rs index b283908438..37c49aa672 100644 --- a/wasm/zknym-lib/src/error.rs +++ b/wasm/zknym-lib/src/error.rs @@ -7,12 +7,18 @@ use wasm_utils::wasm_error; #[derive(Debug, Error)] pub enum ZkNymError { - #[error("cryptographic failure: {source}")] + #[error("[coconut] cryptographic failure: {source}")] CoconutFailure { #[from] source: nym_coconut::CoconutError, }, + #[error("[ecash] cryptographic failure: {source}")] + EcashFailure { + #[from] + source: nym_compact_ecash::CompactEcashError, + }, + #[error("failed to contact the vpn api")] HttpClientFailure { #[from] diff --git a/wasm/zknym-lib/src/generic_scheme.rs b/wasm/zknym-lib/src/generic_scheme/coconut/mod.rs similarity index 88% rename from wasm/zknym-lib/src/generic_scheme.rs rename to wasm/zknym-lib/src/generic_scheme/coconut/mod.rs index 53f9c805f4..178f53106c 100644 --- a/wasm/zknym-lib/src/generic_scheme.rs +++ b/wasm/zknym-lib/src/generic_scheme/coconut/mod.rs @@ -1,24 +1,27 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::error::ZkNymError; -use crate::types::{ +use super::coconut::types::{ BlindSignRequestData, BlindSignRequestWrapper, BlindedCredentialWrapper, CredentialShareWrapper, CredentialWrapper, KeyPairWrapper, ParametersWrapper, ScalarsWrapper, VerificationKeyShareWrapper, VerificationKeyWrapper, VerifyCredentialRequestWrapper, }; -use crate::GLOBAL_PARAMS; +use crate::error::ZkNymError; +use crate::GLOBAL_COCONUT_PARAMS; use nym_coconut::{hash_to_scalar, Parameters, SignerIndex}; -use nym_credentials::IssuanceBandwidthCredential; use serde::{Deserialize, Serialize}; use std::sync::OnceLock; use tsify::Tsify; use wasm_bindgen::prelude::wasm_bindgen; +pub mod types; + // works under the assumption of having 4 attributes in the underlying credential(s) +const DEFAULT_ATTRIBUTES: u32 = 4; + pub fn default_bandwidth_credential_params() -> &'static Parameters { static BANDWIDTH_CREDENTIAL_PARAMS: OnceLock = OnceLock::new(); - BANDWIDTH_CREDENTIAL_PARAMS.get_or_init(IssuanceBandwidthCredential::default_parameters) + BANDWIDTH_CREDENTIAL_PARAMS.get_or_init(|| Parameters::new(DEFAULT_ATTRIBUTES).unwrap()) } // attempt to extract appropriate system parameters in the following order: @@ -30,7 +33,7 @@ pub(crate) fn get_params(explicit_params: &Option) -> &Parame return explicit; } - if let Some(global) = GLOBAL_PARAMS.get() { + if let Some(global) = GLOBAL_COCONUT_PARAMS.get() { return global; } @@ -48,16 +51,14 @@ pub struct SetupOpts { pub set_global: Option, } -#[wasm_bindgen] +#[wasm_bindgen(js_name = "coconutSetup")] pub fn setup(opts: SetupOpts) -> Result { - let num_attributes = opts - .num_attributes - .unwrap_or(IssuanceBandwidthCredential::ENCODED_ATTRIBUTES); + let num_attributes = opts.num_attributes.unwrap_or(DEFAULT_ATTRIBUTES); let params = nym_coconut::setup(num_attributes)?; if let Some(true) = opts.set_global { - GLOBAL_PARAMS + GLOBAL_COCONUT_PARAMS .set(params.clone()) .map_err(|_| ZkNymError::GlobalParamsAlreadySet)?; } @@ -65,7 +66,7 @@ pub fn setup(opts: SetupOpts) -> Result { Ok(params.into()) } -#[wasm_bindgen] +#[wasm_bindgen(js_name = "coconutKeygen")] pub fn keygen(parameters: Option) -> KeyPairWrapper { let params = get_params(¶meters); nym_coconut::keygen(params).into() @@ -80,7 +81,7 @@ pub struct TtpKeygenOpts { pub authorities: u64, } -#[wasm_bindgen(js_name = "ttpKeygen")] +#[wasm_bindgen(js_name = "coconutTtpKeygen")] pub fn ttp_keygen( opts: TtpKeygenOpts, parameters: Option, @@ -91,7 +92,7 @@ pub fn ttp_keygen( Ok(keys.into_iter().map(Into::into).collect()) } -#[wasm_bindgen(js_name = "signSimple")] +#[wasm_bindgen(js_name = "coconutSignSimple")] pub fn sign_simple( attributes: Vec, keys: &KeyPairWrapper, @@ -110,7 +111,7 @@ pub fn sign_simple( .map_err(Into::into) } -#[wasm_bindgen(js_name = "prepareBlindSign")] +#[wasm_bindgen(js_name = "coconutPrepareBlindSign")] pub fn prepare_blind_sign( private_attributes: Vec, public_attributes: Vec, @@ -138,7 +139,7 @@ pub fn prepare_blind_sign( }) } -#[wasm_bindgen(js_name = "blindSign")] +#[wasm_bindgen(js_name = "coconutBlindSign")] pub fn blind_sign( keys: &KeyPairWrapper, blind_sign_request: &BlindSignRequestWrapper, @@ -163,7 +164,7 @@ pub fn blind_sign( .map_err(Into::into) } -#[wasm_bindgen(js_name = "unblindSignatureShare")] +#[wasm_bindgen(js_name = "coconutUnblindSignatureShare")] pub fn unblind_signature_share( blinded_signature: &BlindedCredentialWrapper, partial_verification_key: &VerificationKeyWrapper, @@ -176,7 +177,7 @@ pub fn unblind_signature_share( ) } -#[wasm_bindgen(js_name = "unblindAndVerifySignatureShare")] +#[wasm_bindgen(js_name = "coconutUnblindAndVerifySignatureShare")] pub fn unblind_and_verify_signature_share( blinded_signature: &BlindedCredentialWrapper, partial_verification_key: &VerificationKeyWrapper, @@ -195,7 +196,7 @@ pub fn unblind_and_verify_signature_share( ) } -#[wasm_bindgen(js_name = "aggregateSignatureShares")] +#[wasm_bindgen(js_name = "coconutAggregateSignatureShares")] pub fn aggregate_signature_shares( shares: Vec, ) -> Result { @@ -206,7 +207,7 @@ pub fn aggregate_signature_shares( .map_err(Into::into) } -#[wasm_bindgen(js_name = "aggregateSignatureSharesAndVerify")] +#[wasm_bindgen(js_name = "coconutAggregateSignatureSharesAndVerify")] pub fn aggregate_signature_shares_and_verify( verification_key: &VerificationKeyWrapper, parameters: Option, @@ -242,7 +243,7 @@ pub fn aggregate_signature_shares_and_verify( .map_err(Into::into) } -#[wasm_bindgen(js_name = "aggregateVerificationKeyShares")] +#[wasm_bindgen(js_name = "coconutAggregateVerificationKeyShares")] pub fn aggregate_verification_key_shares( shares: Vec, ) -> Result { @@ -253,7 +254,7 @@ pub fn aggregate_verification_key_shares( .map_err(Into::into) } -#[wasm_bindgen(js_name = "aggregateVerificationKeys")] +#[wasm_bindgen(js_name = "coconutAggregateVerificationKeys")] pub fn aggregate_verification_keys( keys: Vec, indices: Vec, @@ -265,7 +266,7 @@ pub fn aggregate_verification_keys( .map_err(Into::into) } -#[wasm_bindgen(js_name = "proveBandwidthCredential")] +#[wasm_bindgen(js_name = "coconutProveBandwidthCredential")] pub fn prove_bandwidth_credential( verification_key: &VerificationKeyWrapper, credential: &CredentialWrapper, @@ -286,7 +287,7 @@ pub fn prove_bandwidth_credential( .map_err(Into::into) } -#[wasm_bindgen(js_name = "verifyCredential")] +#[wasm_bindgen(js_name = "coconutVerifyCredential")] pub fn verify_credential( verification_key: &VerificationKeyWrapper, verification_request: &VerifyCredentialRequestWrapper, @@ -309,7 +310,7 @@ pub fn verify_credential( ) } -#[wasm_bindgen(js_name = "verifySimple")] +#[wasm_bindgen(js_name = "coconutVerifySimple")] pub fn verify_simple( verification_key: &VerificationKeyWrapper, attributes: Vec, @@ -327,7 +328,7 @@ pub fn verify_simple( nym_coconut::verify(params, verification_key, &attributes_ref, credential) } -#[wasm_bindgen(js_name = "simpleRandomiseCredential")] +#[wasm_bindgen(js_name = "coconutSimpleRandomiseCredential")] pub fn simple_randomise_credential( credential: &CredentialWrapper, parameters: Option, diff --git a/wasm/zknym-lib/src/generic_scheme/coconut/types.rs b/wasm/zknym-lib/src/generic_scheme/coconut/types.rs new file mode 100644 index 0000000000..8370fcc636 --- /dev/null +++ b/wasm/zknym-lib/src/generic_scheme/coconut/types.rs @@ -0,0 +1,170 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::ZkNymError; +use crate::{data_pointer_clone, wasm_wrapper, wasm_wrapper_bs58}; +use nym_coconut::{ + hash_to_scalar, Base58, BlindSignRequest, BlindedSignature, KeyPair, Parameters, Scalar, + SecretKey, Signature, SignatureShare, SignerIndex, VerificationKey, VerificationKeyShare, + VerifyCredentialRequest, +}; +use serde::{Deserialize, Serialize}; +use std::ops::Deref; +use std::str::FromStr; +use tsify::Tsify; +use wasm_bindgen::prelude::wasm_bindgen; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +wasm_wrapper!(Parameters, ParametersWrapper); +wasm_wrapper_bs58!(Signature, CredentialWrapper); +wasm_wrapper_bs58!(BlindedSignature, BlindedCredentialWrapper); +wasm_wrapper!(SignatureShare, CredentialShareWrapper); +wasm_wrapper_bs58!(Scalar, ScalarWrapper); + +wasm_wrapper!(KeyPair, KeyPairWrapper); +wasm_wrapper!(SecretKey, SecretKeyWrapper); +wasm_wrapper!(BlindSignRequest, BlindSignRequestWrapper); +wasm_wrapper_bs58!(VerificationKey, VerificationKeyWrapper); +wasm_wrapper_bs58!(VerifyCredentialRequest, VerifyCredentialRequestWrapper); +wasm_wrapper!(VerificationKeyShare, VerificationKeyShareWrapper); + +data_pointer_clone!(VerificationKeyShareWrapper); +data_pointer_clone!(CredentialShareWrapper); +data_pointer_clone!(BlindSignRequestWrapper); + +#[wasm_bindgen] +impl BlindedCredentialWrapper { + pub fn unblind( + &self, + partial_verification_key: &VerificationKeyWrapper, + pedersen_commitments_openings: &ScalarsWrapper, + ) -> CredentialWrapper { + self.inner + .unblind(partial_verification_key, pedersen_commitments_openings) + .into() + } + + #[wasm_bindgen(js_name = "unblindAndVerify")] + pub fn unblind_and_verify( + &self, + partial_verification_key: &VerificationKeyWrapper, + request: &BlindSignRequestData, + private_attributes: Vec, + public_attributes: Vec, + parameters: Option, + ) -> Result { + let params = super::get_params(¶meters); + + let public_attributes = public_attributes + .into_iter() + .map(hash_to_scalar) + .collect::>(); + let public_attributes_ref = public_attributes.iter().collect::>(); + + let private_attributes = private_attributes + .into_iter() + .map(hash_to_scalar) + .collect::>(); + let private_attributes_ref = private_attributes.iter().collect::>(); + + let unblinded_signature = self.inner.unblind_and_verify( + params, + partial_verification_key, + &private_attributes_ref, + &public_attributes_ref, + &request.blind_sign_request.get_commitment_hash(), + &request.pedersen_commitments_openings, + )?; + + Ok(unblinded_signature.into()) + } +} + +#[wasm_bindgen] +impl CredentialWrapper { + #[wasm_bindgen(js_name = "intoShare")] + pub fn into_share(self, index: SignerIndex) -> CredentialShareWrapper { + CredentialShareWrapper { + inner: SignatureShare::new(self.inner, index), + } + } +} + +#[wasm_bindgen] +impl KeyPairWrapper { + #[wasm_bindgen(js_name = "verificationKey")] + pub fn verification_key(&self) -> VerificationKeyWrapper { + self.inner.verification_key().clone().into() + } + + pub fn index(&self) -> Option { + self.inner.index + } + + #[wasm_bindgen(js_name = "verificationKeyShare")] + pub fn verification_key_share(&self) -> Option { + self.inner.to_verification_key_share().map(Into::into) + } +} + +#[wasm_bindgen] +pub struct BlindSignRequestData { + pub(crate) blind_sign_request: BlindSignRequest, + pub(crate) pedersen_commitments_openings: Vec, +} + +#[wasm_bindgen] +impl BlindSignRequestData { + #[wasm_bindgen(js_name = "blindSignRequest")] + pub fn blind_sign_request(&self) -> BlindSignRequestWrapper { + self.blind_sign_request.clone().into() + } + + #[wasm_bindgen(js_name = "pedersenCommitmentsOpenings")] + pub fn pedersen_commitments_openings(&self) -> ScalarsWrapper { + ScalarsWrapper(self.pedersen_commitments_openings.clone()) + } +} + +#[wasm_bindgen] +#[derive(Zeroize, ZeroizeOnDrop)] +pub struct ScalarsWrapper(pub(crate) Vec); + +impl Deref for ScalarsWrapper { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive( + Tsify, Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq, Zeroize, ZeroizeOnDrop, +)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[serde(rename_all = "camelCase")] +pub struct KeypairWrapper { + pub private_key: String, + pub public_key: String, +} + +#[derive(Tsify, Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[serde(rename_all = "camelCase")] +pub struct UnblindableShare { + pub issuer_index: u64, + pub issuer_key_bs58: String, + pub blinded_share_bs58: String, +} + +#[wasm_bindgen] +impl UnblindableShare { + #[wasm_bindgen(constructor)] + pub fn new(issuer_index: u64, issuer_key_bs58: String, blinded_share_bs58: String) -> Self { + UnblindableShare { + issuer_index, + issuer_key_bs58, + blinded_share_bs58, + } + } +} diff --git a/wasm/zknym-lib/src/generic_scheme/ecash/mod.rs b/wasm/zknym-lib/src/generic_scheme/ecash/mod.rs new file mode 100644 index 0000000000..c2ddfbb025 --- /dev/null +++ b/wasm/zknym-lib/src/generic_scheme/ecash/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod types; diff --git a/wasm/zknym-lib/src/generic_scheme/ecash/types.rs b/wasm/zknym-lib/src/generic_scheme/ecash/types.rs new file mode 100644 index 0000000000..cbb465df6c --- /dev/null +++ b/wasm/zknym-lib/src/generic_scheme/ecash/types.rs @@ -0,0 +1,8 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::wasm_wrapper; +use nym_compact_ecash::GroupParameters; +use wasm_bindgen::prelude::wasm_bindgen; + +wasm_wrapper!(GroupParameters, GroupParametersWrapper); diff --git a/wasm/zknym-lib/src/generic_scheme/mod.rs b/wasm/zknym-lib/src/generic_scheme/mod.rs new file mode 100644 index 0000000000..ad786c7841 --- /dev/null +++ b/wasm/zknym-lib/src/generic_scheme/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod coconut; +pub mod ecash; diff --git a/wasm/zknym-lib/src/helpers.rs b/wasm/zknym-lib/src/helpers.rs new file mode 100644 index 0000000000..a99ec7753b --- /dev/null +++ b/wasm/zknym-lib/src/helpers.rs @@ -0,0 +1,80 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[macro_export] +macro_rules! wasm_wrapper { + ($base:ident, $wrapper:ident) => { + #[wasm_bindgen] + pub struct $wrapper { + pub(crate) inner: $base, + } + + impl std::ops::Deref for $wrapper { + type Target = $base; + + fn deref(&self) -> &Self::Target { + &self.inner + } + } + + impl From<$base> for $wrapper { + fn from(inner: $base) -> Self { + $wrapper { inner } + } + } + + impl From<$wrapper> for $base { + fn from(value: $wrapper) -> Self { + value.inner + } + } + }; +} + +#[macro_export] +macro_rules! data_pointer_clone { + ($wrapper:ident) => { + #[wasm_bindgen] + impl $wrapper { + #[wasm_bindgen(js_name = "cloneDataPointer")] + pub fn clone_data_pointer(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } + } + }; +} + +#[macro_export] +macro_rules! wasm_wrapper_bs58 { + ($base:ident, $wrapper:ident) => { + wasm_wrapper!($base, $wrapper); + + impl std::fmt::Display for $wrapper { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.inner.to_bs58().fmt(f) + } + } + + impl FromStr for $wrapper { + type Err = ZkNymError; + + fn from_str(s: &str) -> Result { + Ok($base::try_from_bs58(s)?.into()) + } + } + + #[wasm_bindgen] + impl $wrapper { + pub fn stringify(&self) -> String { + self.to_string() + } + + #[wasm_bindgen(js_name = "fromString")] + pub fn from_string(raw: String) -> Result<$wrapper, ZkNymError> { + raw.parse() + } + } + }; +} diff --git a/wasm/zknym-lib/src/lib.rs b/wasm/zknym-lib/src/lib.rs index d94f90385e..540a386ee8 100644 --- a/wasm/zknym-lib/src/lib.rs +++ b/wasm/zknym-lib/src/lib.rs @@ -1,6 +1,9 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +#![allow(unknown_lints)] +// clippy::empty_docs is not on stable as of 1.77 + // due to the code generated by Tsify #![allow(clippy::empty_docs)] @@ -11,13 +14,15 @@ use wasm_bindgen::prelude::*; pub mod bandwidth_voucher; pub mod error; pub mod generic_scheme; +pub(crate) mod helpers; +pub mod ticketbook; pub mod types; // keep in internal to the crate since I'm not sure how temporary this thing is going to be // I mostly got it, so I could test the whole thing end to end pub(crate) mod vpn_api_client; -pub(crate) static GLOBAL_PARAMS: OnceLock = OnceLock::new(); +pub(crate) static GLOBAL_COCONUT_PARAMS: OnceLock = OnceLock::new(); #[wasm_bindgen(start)] // #[cfg(target_arch = "wasm32")] diff --git a/wasm/zknym-lib/src/ticketbook.rs b/wasm/zknym-lib/src/ticketbook.rs new file mode 100644 index 0000000000..257ac1a0e7 --- /dev/null +++ b/wasm/zknym-lib/src/ticketbook.rs @@ -0,0 +1,14 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; +use tsify::Tsify; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +#[derive(Tsify, Serialize, Deserialize, Debug, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[serde(rename_all = "camelCase")] +pub struct SerialisedNymTicketBook { + pub serialisation_revision: u8, + pub bs58_encoded_data: String, +} diff --git a/wasm/zknym-lib/src/types.rs b/wasm/zknym-lib/src/types.rs index 552f44381e..755fb6cc8b 100644 --- a/wasm/zknym-lib/src/types.rs +++ b/wasm/zknym-lib/src/types.rs @@ -1,245 +1,2 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 - -use crate::error::ZkNymError; -use crate::generic_scheme::get_params; -use nym_coconut::{ - hash_to_scalar, Base58, BlindSignRequest, BlindedSignature, KeyPair, Parameters, Scalar, - SecretKey, Signature, SignatureShare, SignerIndex, VerificationKey, VerificationKeyShare, - VerifyCredentialRequest, -}; -use serde::{Deserialize, Serialize}; -use std::ops::Deref; -use std::str::FromStr; -use tsify::Tsify; -use wasm_bindgen::prelude::wasm_bindgen; -use zeroize::{Zeroize, ZeroizeOnDrop}; - -macro_rules! wasm_wrapper { - ($base:ident, $wrapper:ident) => { - #[wasm_bindgen] - pub struct $wrapper { - pub(crate) inner: $base, - } - - impl Deref for $wrapper { - type Target = $base; - - fn deref(&self) -> &Self::Target { - &self.inner - } - } - - impl From<$base> for $wrapper { - fn from(inner: $base) -> Self { - $wrapper { inner } - } - } - - impl From<$wrapper> for $base { - fn from(value: $wrapper) -> Self { - value.inner - } - } - }; -} - -macro_rules! data_pointer_clone { - ($wrapper:ident) => { - #[wasm_bindgen] - impl $wrapper { - #[wasm_bindgen(js_name = "cloneDataPointer")] - pub fn clone_data_pointer(&self) -> Self { - Self { - inner: self.inner.clone(), - } - } - } - }; -} - -macro_rules! wasm_wrapper_bs58 { - ($base:ident, $wrapper:ident) => { - wasm_wrapper!($base, $wrapper); - - impl std::fmt::Display for $wrapper { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.inner.to_bs58().fmt(f) - } - } - - impl FromStr for $wrapper { - type Err = ZkNymError; - - fn from_str(s: &str) -> Result { - Ok($base::try_from_bs58(s)?.into()) - } - } - - #[wasm_bindgen] - impl $wrapper { - pub fn stringify(&self) -> String { - self.to_string() - } - - #[wasm_bindgen(js_name = "fromString")] - pub fn from_string(raw: String) -> Result<$wrapper, ZkNymError> { - raw.parse() - } - } - }; -} - -wasm_wrapper!(Parameters, ParametersWrapper); -wasm_wrapper_bs58!(Signature, CredentialWrapper); -wasm_wrapper_bs58!(BlindedSignature, BlindedCredentialWrapper); -wasm_wrapper!(SignatureShare, CredentialShareWrapper); -wasm_wrapper_bs58!(Scalar, ScalarWrapper); - -wasm_wrapper!(KeyPair, KeyPairWrapper); -wasm_wrapper!(SecretKey, SecretKeyWrapper); -wasm_wrapper!(BlindSignRequest, BlindSignRequestWrapper); -wasm_wrapper_bs58!(VerificationKey, VerificationKeyWrapper); -wasm_wrapper_bs58!(VerifyCredentialRequest, VerifyCredentialRequestWrapper); -wasm_wrapper!(VerificationKeyShare, VerificationKeyShareWrapper); - -data_pointer_clone!(VerificationKeyShareWrapper); -data_pointer_clone!(CredentialShareWrapper); -data_pointer_clone!(BlindSignRequestWrapper); - -#[wasm_bindgen] -impl BlindedCredentialWrapper { - pub fn unblind( - &self, - partial_verification_key: &VerificationKeyWrapper, - pedersen_commitments_openings: &ScalarsWrapper, - ) -> CredentialWrapper { - self.inner - .unblind(partial_verification_key, pedersen_commitments_openings) - .into() - } - - #[wasm_bindgen(js_name = "unblindAndVerify")] - pub fn unblind_and_verify( - &self, - partial_verification_key: &VerificationKeyWrapper, - request: &BlindSignRequestData, - private_attributes: Vec, - public_attributes: Vec, - parameters: Option, - ) -> Result { - let params = get_params(¶meters); - - let public_attributes = public_attributes - .into_iter() - .map(hash_to_scalar) - .collect::>(); - let public_attributes_ref = public_attributes.iter().collect::>(); - - let private_attributes = private_attributes - .into_iter() - .map(hash_to_scalar) - .collect::>(); - let private_attributes_ref = private_attributes.iter().collect::>(); - - let unblinded_signature = self.inner.unblind_and_verify( - params, - partial_verification_key, - &private_attributes_ref, - &public_attributes_ref, - &request.blind_sign_request.get_commitment_hash(), - &request.pedersen_commitments_openings, - )?; - - Ok(unblinded_signature.into()) - } -} - -#[wasm_bindgen] -impl CredentialWrapper { - #[wasm_bindgen(js_name = "intoShare")] - pub fn into_share(self, index: SignerIndex) -> CredentialShareWrapper { - CredentialShareWrapper { - inner: SignatureShare::new(self.inner, index), - } - } -} - -#[wasm_bindgen] -impl KeyPairWrapper { - #[wasm_bindgen(js_name = "verificationKey")] - pub fn verification_key(&self) -> VerificationKeyWrapper { - self.inner.verification_key().clone().into() - } - - pub fn index(&self) -> Option { - self.inner.index - } - - #[wasm_bindgen(js_name = "verificationKeyShare")] - pub fn verification_key_share(&self) -> Option { - self.inner.to_verification_key_share().map(Into::into) - } -} - -#[wasm_bindgen] -pub struct BlindSignRequestData { - pub(crate) blind_sign_request: BlindSignRequest, - pub(crate) pedersen_commitments_openings: Vec, -} - -#[wasm_bindgen] -impl BlindSignRequestData { - #[wasm_bindgen(js_name = "blindSignRequest")] - pub fn blind_sign_request(&self) -> BlindSignRequestWrapper { - self.blind_sign_request.clone().into() - } - - #[wasm_bindgen(js_name = "pedersenCommitmentsOpenings")] - pub fn pedersen_commitments_openings(&self) -> ScalarsWrapper { - ScalarsWrapper(self.pedersen_commitments_openings.clone()) - } -} - -#[wasm_bindgen] -#[derive(Zeroize, ZeroizeOnDrop)] -pub struct ScalarsWrapper(pub(crate) Vec); - -impl Deref for ScalarsWrapper { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[derive( - Tsify, Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq, Zeroize, ZeroizeOnDrop, -)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[serde(rename_all = "camelCase")] -pub struct KeypairWrapper { - pub private_key: String, - pub public_key: String, -} - -#[derive(Tsify, Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[serde(rename_all = "camelCase")] -pub struct UnblindableShare { - pub issuer_index: u64, - pub issuer_key_bs58: String, - pub blinded_share_bs58: String, -} - -#[wasm_bindgen] -impl UnblindableShare { - #[wasm_bindgen(constructor)] - pub fn new(issuer_index: u64, issuer_key_bs58: String, blinded_share_bs58: String) -> Self { - UnblindableShare { - issuer_index, - issuer_key_bs58, - blinded_share_bs58, - } - } -} diff --git a/wasm/zknym-lib/src/vpn_api_client/types.rs b/wasm/zknym-lib/src/vpn_api_client/types.rs index 691fb99cbf..cdb8d47534 100644 --- a/wasm/zknym-lib/src/vpn_api_client/types.rs +++ b/wasm/zknym-lib/src/vpn_api_client/types.rs @@ -83,13 +83,6 @@ pub struct AttributesResponse { pub bs58_prehashed_amount: String, } -#[derive(Tsify, Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[serde(rename_all = "camelCase")] -pub struct FreepassCredentialResponse { - pub bs58_encoded_value: String, -} - #[derive(Tsify, Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)] #[tsify(into_wasm_abi, from_wasm_abi)] #[serde(rename_all = "camelCase")] diff --git a/yarn.lock b/yarn.lock index f17664b8ed..40513f47f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1212,7 +1212,7 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/runtime@^7.24.7": +"@babel/runtime@^7.24.8": version "7.24.8" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.8.tgz#5d958c3827b13cc6d05e038c07fb2e5e3420d82e" integrity sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA== @@ -2965,6 +2965,19 @@ dependencies: "@babel/runtime" "^7.23.9" +"@mui/lab@5.0.0-alpha.170": + version "5.0.0-alpha.170" + resolved "https://registry.yarnpkg.com/@mui/lab/-/lab-5.0.0-alpha.170.tgz#4519dfc8d1c51ca54fb9d8b91b95a3733d07be16" + integrity sha512-0bDVECGmrNjd3+bLdcLiwYZ0O4HP5j5WSQm5DV6iA/Z9kr8O6AnvZ1bv9ImQbbX7Gj3pX4o43EKwCutj3EQxQg== + dependencies: + "@babel/runtime" "^7.23.9" + "@mui/base" "5.0.0-beta.40" + "@mui/system" "^5.15.15" + "@mui/types" "^7.2.14" + "@mui/utils" "^5.15.14" + clsx "^2.1.0" + prop-types "^15.8.1" + "@mui/material@^5.0.1", "@mui/material@^5.2.2": version "5.15.15" resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.15.15.tgz#e3ba35f50b510aa677cec3261abddc2db7b20b59" @@ -2992,13 +3005,13 @@ "@mui/utils" "^5.15.14" prop-types "^15.8.1" -"@mui/private-theming@^5.16.1": - version "5.16.1" - resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.16.1.tgz#e7f1b0cfd9b238231fca9a0b13a5b2a9d9592b35" - integrity sha512-2EGCKnAlq9vRIFj61jNWNXlKAxXp56577OVvsts7fAqRx+G1y6F+N7Q198SBaz8jYQeGKSz8ZMXK/M3FqjdEyw== +"@mui/private-theming@^5.16.5": + version "5.16.5" + resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.16.5.tgz#b90afcfa76ea50778453c633656ff59cb1b5494d" + integrity sha512-CSLg0YkpDqg0aXOxtjo3oTMd3XWMxvNb5d0v4AYVqwOltU8q6GvnZjhWyCLjGSCrcgfwm6/VDjaKLPlR14wxIA== dependencies: "@babel/runtime" "^7.23.9" - "@mui/utils" "^5.16.1" + "@mui/utils" "^5.16.5" prop-types "^15.8.1" "@mui/styled-engine@^5.15.14": @@ -3011,10 +3024,10 @@ csstype "^3.1.3" prop-types "^15.8.1" -"@mui/styled-engine@^5.16.1": - version "5.16.1" - resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.16.1.tgz#7518f64f66edd6e09f129289cf6ece502520947f" - integrity sha512-JwWUBaYR8HHCFefSeos0z6JoTbu0MnjAuNHu4QoDgPxl2EE70XH38CsKay66Iy0QkNWmGTRXVU2sVFgUOPL/Dw== +"@mui/styled-engine@^5.16.4": + version "5.16.4" + resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.16.4.tgz#a7a8c9079c307bab91ccd65ed5dd1496ddf2a3ab" + integrity sha512-0+mnkf+UiAmTVB8PZFqOhqf729Yh0Cxq29/5cA3VAyDVTRIUUQ8FXQhiAhUIbijFmM72rY80ahFPXIm4WDbzcA== dependencies: "@babel/runtime" "^7.23.9" "@emotion/cache" "^11.11.0" @@ -3058,16 +3071,16 @@ csstype "^3.1.3" prop-types "^15.8.1" -"@mui/system@^5.16.0": - version "5.16.1" - resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.16.1.tgz#c08fddba592511d6916c6a70da292a7658551ccb" - integrity sha512-VaFcClC+uhvIEzhzcNmh9FRBvrG9IPjsOokhj6U1HPZsFnLzHV7AD7dJcT6LxWoiIZj9Ej0GK+MGh/b8+BtSlQ== +"@mui/system@^5.16.5": + version "5.16.5" + resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.16.5.tgz#a90239e1467f7fce960167939dde9f44f6869484" + integrity sha512-uzIUGdrWddUx1HPxW4+B2o4vpgKyRxGe/8BxbfXVDPNPHX75c782TseoCnR/VyfnZJfqX87GcxDmnZEE1c031g== dependencies: "@babel/runtime" "^7.23.9" - "@mui/private-theming" "^5.16.1" - "@mui/styled-engine" "^5.16.1" + "@mui/private-theming" "^5.16.5" + "@mui/styled-engine" "^5.16.4" "@mui/types" "^7.2.15" - "@mui/utils" "^5.16.1" + "@mui/utils" "^5.16.5" clsx "^2.1.0" csstype "^3.1.3" prop-types "^15.8.1" @@ -3092,13 +3105,15 @@ prop-types "^15.8.1" react-is "^18.2.0" -"@mui/utils@^5.16.0", "@mui/utils@^5.16.1": - version "5.16.1" - resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.16.1.tgz#068ddc236c10f71768133c144b3286d2cc815f5f" - integrity sha512-4UQzK46tAEYs2xZv79hRiIc3GxZScd00kGPDadNrGztAEZlmSaUY8cb9ITd2xCiTfzsx5AN6DH8aaQ8QEKJQeQ== +"@mui/utils@^5.16.5": + version "5.16.5" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.16.5.tgz#3a16877a80166a7f8b58c893d08e0993040fc49e" + integrity sha512-CwhcA9y44XwK7k2joL3Y29mRUnoBt+gOZZdGyw7YihbEwEErJYBtDwbZwVgH68zAljGe/b+Kd5bzfl63Gi3R2A== dependencies: "@babel/runtime" "^7.23.9" + "@mui/types" "^7.2.15" "@types/prop-types" "^15.7.12" + clsx "^2.1.1" prop-types "^15.8.1" react-is "^18.3.1" @@ -3139,15 +3154,24 @@ prop-types "^15.8.1" react-transition-group "^4.4.5" -"@mui/x-tree-view@^7.10.0": - version "7.10.0" - resolved "https://registry.yarnpkg.com/@mui/x-tree-view/-/x-tree-view-7.10.0.tgz#7aed69461df425e6aaebf0fd775ae5ff52cdc065" - integrity sha512-9OCAIb0wS5uuEDyjcSwSturrB4RUXBfE0UO/xpKjrMvRzCaAvxbCf2aFILP8uH9NyynYZkIGYfGnlqdAPy2OLg== +"@mui/x-internals@7.11.1": + version "7.11.1" + resolved "https://registry.yarnpkg.com/@mui/x-internals/-/x-internals-7.11.1.tgz#ab5f1155ba09665d9e53a9a461b4c52b89aaa43f" + integrity sha512-CN9HmtcyJ6/1fd8by5h1/R8WmFN4xyk6XYvYG9++oAaSF1ttX16oiE5vB+gGafl7St0epCWWjvOzl21h29k6WQ== dependencies: - "@babel/runtime" "^7.24.7" + "@babel/runtime" "^7.24.8" + "@mui/utils" "^5.16.5" + +"@mui/x-tree-view@^7.11.1": + version "7.11.1" + resolved "https://registry.yarnpkg.com/@mui/x-tree-view/-/x-tree-view-7.11.1.tgz#77748013f368a9bd5f1e5e03adf3d6a788fb0f76" + integrity sha512-BiQnx/bGnEFjPge10v9X1QTJVRvv5aAH2Q35YV8XX2iuONQO2fpam/jALyQuX9xY7LV6zoHG48oISV4VPpWo7g== + dependencies: + "@babel/runtime" "^7.24.8" "@mui/base" "^5.0.0-beta.40" - "@mui/system" "^5.16.0" - "@mui/utils" "^5.16.0" + "@mui/system" "^5.16.5" + "@mui/utils" "^5.16.5" + "@mui/x-internals" "7.11.1" "@types/react-transition-group" "^4.4.10" clsx "^2.1.1" prop-types "^15.8.1"