diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d4a639253b..0777368574 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: RUSTC_WRAPPER: /home/ubuntu/.cargo/bin/sccache steps: - name: Install Dependencies (Linux) - run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools + run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools - name: Check out repository code uses: actions/checkout@v2 diff --git a/.github/workflows/nightly_build.yml b/.github/workflows/nightly_build.yml index 087f379984..8475b83b8b 100644 --- a/.github/workflows/nightly_build.yml +++ b/.github/workflows/nightly_build.yml @@ -24,7 +24,7 @@ jobs: continue-on-error: ${{ matrix.rust == 'nightly' || matrix.rust == 'beta' || matrix.rust == 'stable' }} steps: - name: Install Dependencies (Linux) - run: sudo apt-get update && sudo apt-get install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools + run: sudo apt-get update && sudo apt-get install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools if: matrix.os == 'ubuntu-latest' - name: Check out repository code diff --git a/.github/workflows/nym-release-publish.yml b/.github/workflows/nym-release-publish.yml index 32fb811493..7fd455dd46 100644 --- a/.github/workflows/nym-release-publish.yml +++ b/.github/workflows/nym-release-publish.yml @@ -2,6 +2,12 @@ name: Publish Nym binaries on: workflow_dispatch: + inputs: + add_tokio_unstable: + description: 'True to add RUSTFLAGS="--cfg tokio_unstable"' + required: true + default: false + type: boolean release: types: [created] @@ -25,6 +31,11 @@ jobs: with: script: | core.setFailed('Release tag did not start with nym-binaries-...') + + - name: Sets env vars for tokio if set in manual dispatch inputs + run: | + echo 'RUSTFLAGS="--cfg tokio_unstable"' >> $GITHUB_ENV + if: github.event_name == 'workflow_dispatch' && inputs.add_tokio_unstable == true - name: Install Rust stable uses: actions-rs/toolchain@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b77413802..f340a152dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - nym-cli: added CLI tool for interacting with the Nyx blockchain and Nym mixnet smart contracts ([#1577]) - validator-client: added `query_contract_smart` and `query_contract_raw` on `NymdClient` ([#1558]) - network-requester: added additional Blockstream Green wallet endpoint to `example.allowed.list` ([#1611](https://github.com/nymtech/nym/pull/1611)) +- common/ledger: new library for communicating with a Ledger device ([#1640]) ### Changed @@ -21,6 +22,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// [#1577]: https://github.com/nymtech/nym/pull/1577 [#1585]: https://github.com/nymtech/nym/pull/1585 [#1591]: https://github.com/nymtech/nym/pull/1591 +[#1640]: https://github.com/nymtech/nym/pull/1640 ## [nym-binaries-1.0.2](https://github.com/nymtech/nym/tree/nym-binaries-1.0.2) diff --git a/Cargo.lock b/Cargo.lock index 62dedd2c7a..4534ee11cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,7 +20,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.5", ] [[package]] @@ -67,29 +67,20 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.6", "once_cell", "version_check", ] [[package]] name = "aho-corasick" -version = "0.7.19" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" dependencies = [ "memchr", ] -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "ansi_term" version = "0.12.1" @@ -101,9 +92,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.65" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" +checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" [[package]] name = "arrayref" @@ -146,9 +137,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.57" +version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" +checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" dependencies = [ "proc-macro2", "quote", @@ -208,57 +199,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "axum" -version = "0.5.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e3356844c4d6a6d6467b8da2cffb4a2820be256f50a3a386c9d152bab31043" -dependencies = [ - "async-trait", - "axum-core", - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "hyper", - "itoa 1.0.3", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "serde", - "sync_wrapper", - "tokio", - "tower", - "tower-http", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f0c0a60006f2a293d82d571f635042a72edf927539b7685bd62d361963839b" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "mime", - "tower-layer", - "tower-service", -] - -[[package]] -name = "az" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" - [[package]] name = "bandwidth-claim-contract" version = "0.1.0" @@ -281,9 +221,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "base64ct" -version = "1.5.2" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2b2456fd614d856680dcd9fcc660a51a820fa09daef2e49772b56a193c8474" +checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b" [[package]] name = "binascii" @@ -357,9 +297,9 @@ dependencies = [ [[package]] name = "bitvec" -version = "1.0.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +checksum = "1489fcb93a5bb47da0462ca93ad252ad6af2145cce58d10d46a83931ba9f016b" dependencies = [ "funty 2.0.0", "radium 0.7.0", @@ -395,21 +335,42 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.9.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" dependencies = [ - "block-padding", - "generic-array 0.14.6", + "block-padding 0.1.5", + "byte-tools", + "byteorder", + "generic-array 0.12.4", ] [[package]] name = "block-buffer" -version = "0.10.3" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array 0.14.6", + "block-padding 0.2.1", + "generic-array 0.14.5", +] + +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array 0.14.5", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", ] [[package]] @@ -438,7 +399,7 @@ version = "0.6.0" source = "git+https://github.com/jstuczyn/bls12_381?branch=gt-serialisation#10fb6f700bfda17c8475af3bfd31e3fec15f2278" dependencies = [ "digest 0.9.0", - "ff 0.11.1", + "ff 0.11.0", "group 0.11.0", "pairing 0.21.0", "rand_core 0.6.3", @@ -469,9 +430,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.11.0" +version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" [[package]] name = "byte-slice-cast" @@ -485,12 +446,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" -[[package]] -name = "bytemuck" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da" - [[package]] name = "byteorder" version = "1.4.3" @@ -499,15 +454,18 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.2.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cast" -version = "0.3.0" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +checksum = "4c24dab4283a142afa2fdca129b80ad2c6284e073930f964c3a1293c225ee39a" +dependencies = [ + "rustc_version 0.4.0", +] [[package]] name = "cc" @@ -542,17 +500,15 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.22" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" dependencies = [ - "iana-time-zone", - "js-sys", + "libc", "num-integer", "num-traits", "serde", "time 0.1.44", - "wasm-bindgen", "winapi", ] @@ -562,7 +518,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.5", ] [[package]] @@ -588,9 +544,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.21" +version = "3.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed5341b2301a26ab80be5cbdced622e80ed808483c52e45e3310a877d3b37d7" +checksum = "190814073e85d238f31ff738fcb0bf6910cedeb73376c87cd69291028966fd83" dependencies = [ "atty", "bitflags", @@ -605,11 +561,11 @@ dependencies = [ [[package]] name = "clap_complete" -version = "3.2.5" +version = "3.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f7a2e0a962c45ce25afce14220bc24f9dade0a1787f185cecf96bfba7847cd8" +checksum = "e4179da71abd56c26b54dd0c248cc081c1f43b0a1a7e8448e28e57a29baa993d" dependencies = [ - "clap 3.2.21", + "clap 3.2.8", ] [[package]] @@ -618,15 +574,15 @@ version = "3.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed37b4c0c1214673eba6ad8ea31666626bf72be98ffb323067d973c48b4964b9" dependencies = [ - "clap 3.2.21", + "clap 3.2.8", "clap_complete", ] [[package]] name = "clap_derive" -version = "3.2.18" +version = "3.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" +checksum = "759bf187376e1afa7b85b959e6a664a3e7a95203415dba952ad19139e798f902" dependencies = [ "heck 0.4.0", "proc-macro-error", @@ -715,9 +671,9 @@ dependencies = [ [[package]] name = "comfy-table" -version = "6.1.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85914173c2f558d61613bfbbf1911f14e630895087a7ed2fafc0f5319e1536e7" +checksum = "121d8a5b0346092c18a4b2fd6f620d7a06f0eb7ac0a45860939a0884bc579c56" dependencies = [ "crossterm", "strum 0.24.1", @@ -740,21 +696,22 @@ dependencies = [ [[package]] name = "console-api" -version = "0.4.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57ff02e8ad8e06ab9731d5dc72dc23bef9200778eae1a89d555d8c42e5d4a86" +checksum = "cc347c19eb5b940f396ac155822caee6662f850d97306890ac3773ed76c90c5a" dependencies = [ - "prost 0.11.0", - "prost-types 0.11.1", + "prost 0.9.0", + "prost-types 0.9.0", "tonic", + "tonic-build", "tracing-core", ] [[package]] name = "console-subscriber" -version = "0.1.8" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a3a81dfaf6b66bce5d159eddae701e3a002f194d378cbf7be5f053c281d9be" +checksum = "565a7dfea2d10dd0e5c57cc394d5d441b1910960d8c9211ed14135e0e6ec3a20" dependencies = [ "console-api", "crossbeam-channel", @@ -762,7 +719,7 @@ dependencies = [ "futures", "hdrhistogram", "humantime 2.1.0", - "prost-types 0.11.1", + "prost-types 0.9.0", "serde", "serde_json", "thread_local", @@ -791,6 +748,7 @@ name = "contracts-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "serde", ] [[package]] @@ -811,7 +769,7 @@ dependencies = [ "hmac 0.12.1", "percent-encoding", "rand 0.8.5", - "sha2 0.10.5", + "sha2 0.10.2", "subtle 2.4.1", "time 0.3.14", "version_check", @@ -838,7 +796,7 @@ name = "cosmos-sdk-proto" version = "0.12.3" source = "git+https://github.com/neacsu/cosmos-rust?branch=neacsu/feegrant_support#f63ded63ec13e753ebe8bdafe9dc503df265d67d" dependencies = [ - "prost 0.10.4", + "prost 0.10.3", "prost-types 0.10.1", "tendermint-proto", ] @@ -852,9 +810,9 @@ dependencies = [ "cosmos-sdk-proto", "ecdsa", "eyre", - "getrandom 0.2.7", + "getrandom 0.2.6", "k256", - "prost 0.10.4", + "prost 0.10.3", "prost-types 0.10.1", "rand_core 0.6.3", "serde", @@ -880,9 +838,9 @@ dependencies = [ [[package]] name = "cosmwasm-derive" -version = "1.1.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9053ebe2ad85831e9f9e2124fa2b22807528a78b25cc447483ce2a4aadfba394" +checksum = "4b36e527620a2a3e00e46b6e731ab6c9b68d11069c986f7d7be8eba79ef081a4" dependencies = [ "syn", ] @@ -906,9 +864,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" dependencies = [ "libc", ] @@ -959,7 +917,7 @@ dependencies = [ "async-trait", "bip39", "cfg-if 0.1.10", - "clap 3.2.21", + "clap 3.2.8", "coconut-interface", "config", "credential-storage", @@ -982,7 +940,7 @@ version = "0.1.0" dependencies = [ "async-trait", "log", - "sqlx 0.5.13", + "sqlx 0.5.11", "thiserror", "tokio", ] @@ -1004,9 +962,9 @@ dependencies = [ [[package]] name = "criterion" -version = "0.3.6" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b01d6de93b2b6c65e17c634a26653a29d107b3c98c607c765bf38d041531cd8f" +checksum = "1604dafd25fba2fe2d5895a9da139f8dc9b319a5fe5354ca137cbbce4e178d10" dependencies = [ "atty", "cast", @@ -1030,9 +988,9 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.4.5" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2673cc8207403546f45f5fd319a974b1e6983ad1a3ee7e6041650013be041876" +checksum = "d00996de9f2f7559f7f4dc286073197f83e92256a59ed395f9aac01fe717da57" dependencies = [ "cast", "itertools", @@ -1040,9 +998,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.6" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" dependencies = [ "cfg-if 1.0.0", "crossbeam-utils", @@ -1050,9 +1008,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.2" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" dependencies = [ "cfg-if 1.0.0", "crossbeam-epoch", @@ -1061,23 +1019,23 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.10" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" +checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c" dependencies = [ "autocfg 1.1.0", "cfg-if 1.0.0", "crossbeam-utils", + "lazy_static", "memoffset", - "once_cell", "scopeguard", ] [[package]] name = "crossbeam-queue" -version = "0.3.6" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd42583b04998a5363558e5f9291ee5a5ff6b49944332103f251e7479a82aa7" +checksum = "1f25d8400f4a7a5778f0e4e52384a48cbd9b5c495d110786187fc750075277a2" dependencies = [ "cfg-if 1.0.0", "crossbeam-utils", @@ -1085,25 +1043,25 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.11" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" +checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" dependencies = [ "cfg-if 1.0.0", - "once_cell", + "lazy_static", ] [[package]] name = "crossterm" -version = "0.25.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +checksum = "a2102ea4f781910f8a5b98dd061f4c2023f479ce7bb1236330099ceb5a93cf17" dependencies = [ "bitflags", "crossterm_winapi", "libc", "mio", - "parking_lot 0.12.1", + "parking_lot 0.12.0", "signal-hook", "signal-hook-mio", "winapi", @@ -1135,7 +1093,7 @@ dependencies = [ "ctr 0.9.1", "digest 0.10.3", "ed25519-dalek", - "generic-array 0.14.6", + "generic-array 0.14.5", "hkdf 0.12.3", "hmac 0.12.1", "nymsphinx-types", @@ -1154,7 +1112,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.5", "rand_core 0.6.3", "subtle 2.4.1", "zeroize", @@ -1162,11 +1120,11 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.5", "typenum", ] @@ -1186,7 +1144,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.5", "subtle 2.4.1", ] @@ -1276,9 +1234,9 @@ dependencies = [ [[package]] name = "cw-utils" -version = "0.13.4" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dbaecb78c8e8abfd6b4258c7f4fbeb5c49a5e45ee4d910d3240ee8e1d714e1b" +checksum = "babd2c090f39d07ce5bf2556962305e795daa048ce20a93709eb591476e4a29e" dependencies = [ "cosmwasm-std", "schemars", @@ -1288,9 +1246,9 @@ dependencies = [ [[package]] name = "cw3" -version = "0.13.4" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe19462a7f644ba60c19d3443cb90d00c50d9b6b3b0a3a7fca93df8261af979b" +checksum = "f871854338a54c7bb094d16ffe17212b93b146d9659dbce4c9402a9b77e240ef" dependencies = [ "cosmwasm-std", "cw-utils", @@ -1300,9 +1258,9 @@ dependencies = [ [[package]] name = "cw4" -version = "0.13.4" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0acc3549d5ce11c6901b3a676f2e2628684722197054d97cd0101ea174ed5cbd" +checksum = "c4476d6a7c13c46ed9ff260bd0e1cf648dc37b13f483822e1ff2a431f0f6ee52" dependencies = [ "cosmwasm-std", "cw-storage-plus", @@ -1425,7 +1383,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.5", ] [[package]] @@ -1434,7 +1392,7 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" dependencies = [ - "block-buffer 0.10.3", + "block-buffer 0.10.2", "crypto-common", "subtle 2.4.1", ] @@ -1463,11 +1421,11 @@ dependencies = [ name = "dkg" version = "0.1.0" dependencies = [ - "bitvec 1.0.1", + "bitvec 1.0.0", "bls12_381 0.6.0", "bs58", "criterion", - "ff 0.11.1", + "ff 0.11.0", "group 0.11.0", "lazy_static", "rand 0.8.5", @@ -1494,18 +1452,18 @@ checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" [[package]] name = "dotenvy" -version = "0.15.3" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3db6fcad7c1fc4abdd99bf5276a4db30d6a819127903a709ed41e5ff016e84" +checksum = "ed9155c8f4dc55c7470ae9da3f63c6785245093b3f6aeb0f5bf2e968efbba314" dependencies = [ "dirs", ] [[package]] name = "dyn-clone" -version = "1.0.9" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f94fa09c2aeea5b8839e414b7b841bf429fd25b9c522116ac97ee87856d88b2" +checksum = "21e50f3adc76d6a43f5ed73b698a87d0760ca74617f60f7c3b879003536fdd28" [[package]] name = "ecdsa" @@ -1521,9 +1479,9 @@ dependencies = [ [[package]] name = "ed25519" -version = "1.5.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" +checksum = "3d5c4b5e5959dc2c2b89918d8e2cc40fcdd623cef026ed09d2f0ee05199dc8e4" dependencies = [ "serde", "signature", @@ -1561,9 +1519,9 @@ dependencies = [ [[package]] name = "either" -version = "1.8.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "elliptic-curve" @@ -1574,8 +1532,8 @@ dependencies = [ "base16ct", "crypto-bigint", "der", - "ff 0.11.1", - "generic-array 0.14.6", + "ff 0.11.0", + "generic-array 0.14.5", "group 0.11.0", "rand_core 0.6.3", "sec1", @@ -1594,38 +1552,18 @@ dependencies = [ [[package]] name = "enum-iterator" -version = "0.8.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2953d1df47ac0eb70086ccabf0275aa8da8591a28bd358ee2b52bd9f9e3ff9e9" +checksum = "4eeac5c5edb79e4e39fe8439ef35207780a11f69c52cbe424ce3dfad4cb78de6" dependencies = [ - "enum-iterator-derive 0.8.1", -] - -[[package]] -name = "enum-iterator" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a0ac4aeb3a18f92eaf09c6bb9b3ac30ff61ca95514fc58cbead1c9a6bf5401" -dependencies = [ - "enum-iterator-derive 1.1.0", + "enum-iterator-derive", ] [[package]] name = "enum-iterator-derive" -version = "0.8.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8958699f9359f0b04e691a13850d48b7de329138023876d07cbd024c2c820598" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "enum-iterator-derive" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "828de45d0ca18782232dfb8f3ea9cc428e8ced380eb26a520baaacfc70de39ce" +checksum = "c134c37760b27a871ba422106eedbb8247da973a09e82558bf26d619c882b159" dependencies = [ "proc-macro2", "quote", @@ -1716,7 +1654,7 @@ name = "explorer-api" version = "1.0.1" dependencies = [ "chrono", - "clap 3.2.21", + "clap 3.2.8", "humantime-serde", "isocountry", "itertools", @@ -1749,10 +1687,16 @@ dependencies = [ ] [[package]] -name = "fastrand" -version = "1.8.0" +name = "fake-simd" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" dependencies = [ "instant", ] @@ -1769,9 +1713,9 @@ dependencies = [ [[package]] name = "ff" -version = "0.11.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "131655483be284720a17d74ff97592b8e76576dc25563148601df2d7c9080924" +checksum = "b2958d04124b9f27f175eaeb9a9f383d026098aa837eadd8ba22c11f13a05b9e" dependencies = [ "rand_core 0.6.3", "subtle 2.4.1", @@ -1779,9 +1723,9 @@ dependencies = [ [[package]] name = "figment" -version = "0.10.7" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e3bd154d9ae2f1bb0ada5b7eebd56529513efa5de7d2fc8c6adf33bc43260cf" +checksum = "790b4292c72618abbab50f787a477014fe15634f96291de45672ce46afe122df" dependencies = [ "atomic", "pear", @@ -1791,19 +1735,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "fixed" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0371cd413fb63f8ec1b9eb4dff47fa2c88b21abc681771234c84808b9920991" -dependencies = [ - "az", - "bytemuck", - "half 2.1.0", - "serde", - "typenum", -] - [[package]] name = "fixed-hash" version = "0.7.0" @@ -1817,12 +1748,20 @@ dependencies = [ ] [[package]] -name = "flate2" -version = "1.0.24" +name = "fixedbitset" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e" + +[[package]] +name = "flate2" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" dependencies = [ + "cfg-if 1.0.0", "crc32fast", + "libc", "miniz_oxide", ] @@ -1838,14 +1777,14 @@ dependencies = [ [[package]] name = "flume" -version = "0.10.14" +version = "0.10.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +checksum = "843c03199d0c0ca54bc1ea90ac0d507274c28abcc4f691ae8b4eaa375087c76a" dependencies = [ "futures-core", "futures-sink", "pin-project", - "spin 0.9.4", + "spin 0.9.2", ] [[package]] @@ -1886,10 +1825,11 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" dependencies = [ + "matches", "percent-encoding", ] @@ -2054,7 +1994,7 @@ dependencies = [ "fluvio-wasm-timer", "futures", "gateway-requests", - "getrandom 0.2.7", + "getrandom 0.2.6", "log", "network-defaults", "nymsphinx", @@ -2102,15 +2042,15 @@ checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" [[package]] name = "generator" -version = "0.7.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc184cace1cea8335047a471cc1da80f18acf8a76f3bab2028d499e328948ec7" +checksum = "c1d9279ca822891c1a4dae06d185612cf8fc6acfe5dff37781b41297811b12ee" dependencies = [ "cc", "libc", "log", "rustversion", - "windows", + "winapi", ] [[package]] @@ -2124,9 +2064,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" dependencies = [ "typenum", "version_check", @@ -2147,14 +2087,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.7" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" dependencies = [ "cfg-if 1.0.0", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.10.0+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -2182,9 +2122,9 @@ dependencies = [ [[package]] name = "git2" -version = "0.14.4" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0155506aab710a86160ddb504a480d2964d7ab5b9e62419be69e0032bc5931c" +checksum = "3826a6e0e2215d7a41c2bfc7c9244123969273f3476b939a226aac0ab56e9e3c" dependencies = [ "bitflags", "libc", @@ -2218,16 +2158,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89" dependencies = [ "byteorder", - "ff 0.11.1", + "ff 0.11.0", "rand_core 0.6.3", "subtle 2.4.1", ] [[package]] name = "h2" -version = "0.3.14" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be" +checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" dependencies = [ "bytes", "fnv", @@ -2238,7 +2178,7 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util 0.7.4", + "tokio-util 0.7.3", "tracing", ] @@ -2248,15 +2188,6 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" -[[package]] -name = "half" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad6a9459c9c30b177b925162351f97e7d967c7ea8bab3b8352805327daf45554" -dependencies = [ - "crunchy", -] - [[package]] name = "handlebars" version = "3.5.5" @@ -2300,18 +2231,18 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452c155cb93fecdfb02a73dd57b5d8e442c2063bd7aac72f1bc5e4263a43086" +checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" dependencies = [ "hashbrown 0.12.3", ] [[package]] name = "hdrhistogram" -version = "7.5.1" +version = "7.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea9fe3952d32674a14e0975009a3547af9ea364995b5ec1add2e23c2ae523ab" +checksum = "31672b7011be2c4f7456c4ddbcb40e7e9a4a9fad8efe49a6ebaf5f307d0109c0" dependencies = [ "base64", "byteorder", @@ -2322,9 +2253,9 @@ dependencies = [ [[package]] name = "headers" -version = "0.3.8" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" dependencies = [ "base64", "bitflags", @@ -2333,7 +2264,7 @@ dependencies = [ "http", "httpdate", "mime", - "sha1", + "sha-1 0.10.0", ] [[package]] @@ -2384,6 +2315,17 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" +[[package]] +name = "hidapi" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d26e1151deaab68f34fbfd16d491a2a0170cf98d69d3efa23873b567a4199e1" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "hkdf" version = "0.11.0" @@ -2424,37 +2366,31 @@ dependencies = [ [[package]] name = "http" -version = "0.2.8" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" dependencies = [ "bytes", "fnv", - "itoa 1.0.3", + "itoa 1.0.1", ] [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" dependencies = [ "bytes", "http", "pin-project-lite", ] -[[package]] -name = "http-range-header" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" - [[package]] name = "httparse" -version = "1.8.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4" [[package]] name = "httpdate" @@ -2489,9 +2425,9 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.20" +version = "0.14.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" +checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" dependencies = [ "bytes", "futures-channel", @@ -2502,7 +2438,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.3", + "itoa 1.0.1", "pin-project-lite", "socket2", "tokio", @@ -2573,20 +2509,6 @@ dependencies = [ "tokio-native-tls", ] -[[package]] -name = "iana-time-zone" -version = "0.1.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "237a0714f28b1ee39ccec0770ccb544eb02c9ef2c82bb096230eefcffa6468b0" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "js-sys", - "once_cell", - "wasm-bindgen", - "winapi", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -2659,12 +2581,12 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" -version = "1.9.1" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" dependencies = [ "autocfg 1.1.0", - "hashbrown 0.12.3", + "hashbrown 0.11.2", "serde", ] @@ -2680,7 +2602,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.5", ] [[package]] @@ -2706,9 +2628,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.5.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" +checksum = "35e70ee094dc02fd9c13fdad4940090f22dbd6ac7c9e7094a46cf0232a50bc7c" [[package]] name = "ipnetwork" @@ -2731,9 +2653,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.10.4" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8bf247779e67a9082a4790b45e71ac7cfd1321331a5c856a74a9faebdab78d0" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" dependencies = [ "either", ] @@ -2746,9 +2668,9 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" -version = "1.0.3" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" [[package]] name = "jobserver" @@ -2799,9 +2721,9 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.2" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9b7d56ba4a8344d6be9729995e6b06f928af29998cdf79fe390cbf6b1fee838" +checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" [[package]] name = "keystream" @@ -2816,16 +2738,64 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] -name = "libc" -version = "0.2.132" +name = "ledger" +version = "0.1.0" +dependencies = [ + "bip32", + "k256", + "ledger-transport", + "ledger-transport-hid", + "thiserror", +] + +[[package]] +name = "ledger-apdu" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +checksum = "fe435806c197dfeaa5efcded5e623c4b8230fd28fdf1e91e7a86e40ef2acbf90" +dependencies = [ + "arrayref", + "no-std-compat", + "snafu 0.7.1", +] + +[[package]] +name = "ledger-transport" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1117f2143d92c157197785bf57711d7b02f2cfa101e162f8ca7900fb7f976321" +dependencies = [ + "async-trait", + "ledger-apdu", +] + +[[package]] +name = "ledger-transport-hid" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ba81a1f5f24396b37211478aff7fbcd605dd4544df8dbed07b9da3c2057aee" +dependencies = [ + "byteorder", + "cfg-if 1.0.0", + "hex", + "hidapi", + "ledger-transport", + "libc", + "log", + "thiserror", +] + +[[package]] +name = "libc" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec647867e2bf0772e28c8bcde4f0d19a9216916e890543b5a03ed8ef27b8f259" [[package]] name = "libgit2-sys" -version = "0.13.4+1.4.2" +version = "0.13.2+1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0fa6563431ede25f5cc7f6d803c6afbc1c5d3ad3d4925d12c882bf2b526f5d1" +checksum = "3a42de9a51a5c12e00fc0e4ca6bc2ea43582fc6418488e8f615e905d886f258b" dependencies = [ "cc", "libc", @@ -2835,9 +2805,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.5" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "292a948cd991e376cf75541fe5b97a1081d713c618b4f1b9500f8844e49eb565" +checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db" [[package]] name = "libsqlite3-sys" @@ -2852,9 +2822,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.8" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" +checksum = "6f35facd4a5673cb5a48822be2be1d4236c1c99cb4113cab7061ac720d5bf859" dependencies = [ "cc", "libc", @@ -2864,9 +2834,9 @@ dependencies = [ [[package]] name = "linked-hash-map" -version = "0.5.6" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" [[package]] name = "lioness" @@ -2882,9 +2852,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.8" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f80bf5aacaf25cbfc8210d1cfb718f2bf3b11c4c54e5afe36c236853a8ec390" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" dependencies = [ "autocfg 1.1.0", "scopeguard", @@ -2901,9 +2871,9 @@ dependencies = [ [[package]] name = "loom" -version = "0.5.6" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +checksum = "edc5c7d328e32cc4954e8e01193d7f0ef5ab257b5090b70a964e099a36034309" dependencies = [ "cfg-if 1.0.0", "generator", @@ -2914,6 +2884,12 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + [[package]] name = "matchers" version = "0.1.0" @@ -2929,12 +2905,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" -[[package]] -name = "matchit" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" - [[package]] name = "maybe-uninit" version = "2.0.0" @@ -2943,9 +2913,9 @@ checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" [[package]] name = "memchr" -version = "2.5.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" [[package]] name = "memoffset" @@ -2970,23 +2940,35 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.5.4" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" dependencies = [ "adler", + "autocfg 1.1.0", ] [[package]] name = "mio" -version = "0.8.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" dependencies = [ "libc", "log", + "miow", + "ntapi", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", ] [[package]] @@ -2997,20 +2979,21 @@ dependencies = [ "log", "nymsphinx", "tokio", - "tokio-util 0.7.4", + "tokio-util 0.7.3", ] [[package]] name = "mixnet-contract-common" version = "0.1.0" dependencies = [ - "az", + "bs58", "contracts-common", "cosmwasm-std", - "fixed", "log", + "rand_chacha 0.3.1", "schemars", "serde", + "serde_json", "serde_repr", "thiserror", "time 0.3.14", @@ -3036,7 +3019,7 @@ dependencies = [ "serde", "task", "tokio", - "tokio-util 0.7.4", + "tokio-util 0.7.3", "url", "validator-client", "version-checker", @@ -3044,9 +3027,9 @@ dependencies = [ [[package]] name = "multer" -version = "2.0.3" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a30ba6d97eb198c5e8a35d67d5779d6680cca35652a60ee90fc23dc431d4fde8" +checksum = "5f8f35e687561d5c1667590911e6698a8cb714a134a7505718a182e7bc9d3836" dependencies = [ "bytes", "encoding_rs", @@ -3056,12 +3039,18 @@ dependencies = [ "log", "memchr", "mime", - "spin 0.9.4", + "spin 0.9.2", "tokio", - "tokio-util 0.7.4", + "tokio-util 0.6.9", "version_check", ] +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + [[package]] name = "multisig-contract-common" version = "0.1.0" @@ -3105,6 +3094,12 @@ dependencies = [ "url", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "nom" version = "7.1.1" @@ -3121,7 +3116,7 @@ version = "0.1.0" dependencies = [ "tokio", "tokio-stream", - "tokio-util 0.7.4", + "tokio-util 0.7.3", ] [[package]] @@ -3146,9 +3141,9 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.45" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" dependencies = [ "autocfg 1.1.0", "num-traits", @@ -3156,9 +3151,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ "autocfg 1.1.0", "libm", @@ -3176,9 +3171,9 @@ dependencies = [ [[package]] name = "num_threads" -version = "0.1.6" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0" dependencies = [ "libc", ] @@ -3191,7 +3186,7 @@ dependencies = [ "base64", "bip39", "bs58", - "clap 3.2.21", + "clap 3.2.8", "clap_complete", "clap_complete_fig", "dotenv", @@ -3213,7 +3208,7 @@ dependencies = [ "bip39", "bs58", "cfg-if 1.0.0", - "clap 3.2.21", + "clap 3.2.8", "comfy-table", "cosmrs", "cosmwasm-std", @@ -3238,7 +3233,7 @@ dependencies = [ name = "nym-client" version = "1.0.2" dependencies = [ - "clap 3.2.21", + "clap 3.2.8", "client-core", "coconut-interface", "config", @@ -3277,7 +3272,7 @@ dependencies = [ "async-trait", "bip39", "bs58", - "clap 3.2.21", + "clap 3.2.8", "coconut-interface", "colored", "config", @@ -3299,14 +3294,14 @@ dependencies = [ "pretty_env_logger", "rand 0.7.3", "serde", - "sqlx 0.5.13", + "sqlx 0.5.11", "statistics-common", "subtle-encoding", "thiserror", "tokio", "tokio-stream", "tokio-tungstenite 0.14.0", - "tokio-util 0.7.4", + "tokio-util 0.7.3", "url", "validator-api-requests", "validator-client", @@ -3320,7 +3315,7 @@ version = "1.0.2" dependencies = [ "anyhow", "bs58", - "clap 3.2.21", + "clap 3.2.8", "colored", "config", "crypto", @@ -3345,7 +3340,7 @@ dependencies = [ "sysinfo", "task", "tokio", - "tokio-util 0.7.4", + "tokio-util 0.7.3", "toml", "topology", "url", @@ -3359,7 +3354,7 @@ name = "nym-network-requester" version = "1.0.2" dependencies = [ "async-trait", - "clap 3.2.21", + "clap 3.2.8", "dirs", "futures", "ipnetwork", @@ -3392,7 +3387,7 @@ dependencies = [ "pretty_env_logger", "rocket", "serde", - "sqlx 0.5.13", + "sqlx 0.5.11", "statistics-common", "thiserror", "tokio", @@ -3402,7 +3397,7 @@ dependencies = [ name = "nym-socks5-client" version = "1.0.2" dependencies = [ - "clap 3.2.21", + "clap 3.2.8", "client-core", "coconut-interface", "config", @@ -3423,7 +3418,7 @@ dependencies = [ "proxy-helpers", "rand 0.7.3", "serde", - "snafu", + "snafu 0.6.10", "socks5-requests", "task", "tokio", @@ -3467,7 +3462,7 @@ dependencies = [ "anyhow", "async-trait", "cfg-if 1.0.0", - "clap 3.2.21", + "clap 3.2.8", "coconut-bandwidth-contract-common", "coconut-interface", "config", @@ -3514,7 +3509,7 @@ dependencies = [ "url", "validator-api-requests", "validator-client", - "vergen 7.4.2", + "vergen 7.2.1", "version-checker", ] @@ -3547,7 +3542,7 @@ dependencies = [ "digest 0.9.0", "doc-comment", "ff 0.10.1", - "getrandom 0.2.7", + "getrandom 0.2.6", "group 0.10.0", "itertools", "rand 0.8.5", @@ -3657,7 +3652,7 @@ dependencies = [ "bytes", "nymsphinx-params", "nymsphinx-types", - "tokio-util 0.7.4", + "tokio-util 0.7.3", ] [[package]] @@ -3689,9 +3684,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.14.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" +checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" [[package]] name = "oorandom" @@ -3713,30 +3708,18 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.41" +version = "0.10.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0" +checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" dependencies = [ "bitflags", "cfg-if 1.0.0", "foreign-types", "libc", "once_cell", - "openssl-macros", "openssl-sys", ] -[[package]] -name = "openssl-macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "openssl-probe" version = "0.1.5" @@ -3745,9 +3728,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.75" +version = "0.9.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f" +checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" dependencies = [ "autocfg 1.1.0", "cc", @@ -3765,9 +3748,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.3.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" [[package]] name = "pairing" @@ -3826,12 +3809,12 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" dependencies = [ "lock_api", - "parking_lot_core 0.9.3", + "parking_lot_core 0.9.2", ] [[package]] @@ -3844,28 +3827,28 @@ dependencies = [ "instant", "libc", "redox_syscall", - "smallvec 1.9.0", + "smallvec 1.8.0", "winapi", ] [[package]] name = "parking_lot_core" -version = "0.9.3" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +checksum = "995f667a6c822200b0433ac218e05582f0e2efa1b922a3fd2fbaadc5f87bab37" dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall", - "smallvec 1.9.0", + "smallvec 1.8.0", "windows-sys", ] [[package]] name = "paste" -version = "1.0.9" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1" +checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" [[package]] name = "pbkdf2" @@ -3946,25 +3929,24 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "pest" -version = "2.3.1" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb779fcf4bb850fbbb0edc96ff6cf34fd90c4b1a112ce042653280d9a7364048" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" dependencies = [ - "thiserror", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.3.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502b62a6d0245378b04ffe0a7fb4f4419a4815fce813bd8a0ec89a56e07d67b1" +checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" dependencies = [ "pest", "pest_generator", @@ -3972,9 +3954,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.3.1" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "451e629bf49b750254da26132f1a5a9d11fd8a95a3df51d15c4abd1ba154cb6c" +checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" dependencies = [ "pest", "pest_meta", @@ -3985,13 +3967,23 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.3.1" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcec162c71c45e269dfc3fc2916eaeb97feab22993a21bcce4721d08cd7801a6" +checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" dependencies = [ - "once_cell", + "maplit", "pest", - "sha1", + "sha-1 0.8.2", +] + +[[package]] +name = "petgraph" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a13a2fa9d0b63e5f22328828741e523766fff0ee9e779316902290dff3f824f" +dependencies = [ + "fixedbitset", + "indexmap", ] [[package]] @@ -4009,18 +4001,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.0.12" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.12" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" dependencies = [ "proc-macro2", "quote", @@ -4029,9 +4021,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" [[package]] name = "pin-utils" @@ -4058,9 +4050,9 @@ checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" [[package]] name = "plotters" -version = "0.3.4" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97" +checksum = "32a3fd9ec30b9749ce28cd91f255d569591cdf937fe280c312143e3c4bad6f2a" dependencies = [ "num-traits", "plotters-backend", @@ -4071,15 +4063,15 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.4" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142" +checksum = "d88417318da0eaf0fdcdb51a0ee6c3bed624333bff8f946733049380be67ac1c" [[package]] name = "plotters-svg" -version = "0.3.3" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f" +checksum = "521fa9638fa597e1dc53e9412a4f9cefb01187ee1f7413076f9e6749e2885ba9" dependencies = [ "plotters-backend", ] @@ -4127,11 +4119,10 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.2.1" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" +checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" dependencies = [ - "once_cell", "thiserror", "toml", ] @@ -4162,11 +4153,11 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.43" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" dependencies = [ - "unicode-ident", + "unicode-xid", ] [[package]] @@ -4184,22 +4175,55 @@ dependencies = [ [[package]] name = "prost" -version = "0.10.4" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71adf41db68aa0daaefc69bb30bcd68ded9b9abaad5d1fbb6304c4fb390e083e" +checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" +dependencies = [ + "bytes", + "prost-derive 0.9.0", +] + +[[package]] +name = "prost" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc03e116981ff7d8da8e5c220e374587b98d294af7ba7dd7fda761158f00086f" dependencies = [ "bytes", "prost-derive 0.10.1", ] [[package]] -name = "prost" -version = "0.11.0" +name = "prost-build" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "399c3c31cdec40583bb68f0b18403400d01ec4289c383aa047560439952c4dd7" +checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" dependencies = [ "bytes", - "prost-derive 0.11.0", + "heck 0.3.3", + "itertools", + "lazy_static", + "log", + "multimap", + "petgraph", + "prost 0.9.0", + "prost-types 0.9.0", + "regex", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -4216,16 +4240,13 @@ dependencies = [ ] [[package]] -name = "prost-derive" -version = "0.11.0" +name = "prost-types" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7345d5f0e08c0536d7ac7229952590239e77abf0a0100a1b1d890add6ea96364" +checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" dependencies = [ - "anyhow", - "itertools", - "proc-macro2", - "quote", - "syn", + "bytes", + "prost 0.9.0", ] [[package]] @@ -4235,17 +4256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d0a014229361011dc8e69c8a1ec6c2e8d0f2af7c91e3ea3f5b2170298461e68" dependencies = [ "bytes", - "prost 0.10.4", -] - -[[package]] -name = "prost-types" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dfaa718ad76a44b3415e6c4d53b17c8f99160dcb3a99b10470fce8ad43f6e3e" -dependencies = [ - "bytes", - "prost 0.11.0", + "prost 0.10.3", ] [[package]] @@ -4260,7 +4271,7 @@ dependencies = [ "task", "tokio", "tokio-test", - "tokio-util 0.7.4", + "tokio-util 0.7.3", ] [[package]] @@ -4291,9 +4302,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.21" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" dependencies = [ "proc-macro2", ] @@ -4413,7 +4424,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.6", ] [[package]] @@ -4499,9 +4510,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.5.3" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" +checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" dependencies = [ "autocfg 1.1.0", "crossbeam-deque", @@ -4511,13 +4522,14 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.9.3" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" +checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" dependencies = [ "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", + "lazy_static", "num_cpus", ] @@ -4532,9 +4544,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" dependencies = [ "bitflags", ] @@ -4545,25 +4557,25 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.6", "redox_syscall", "thiserror", ] [[package]] name = "ref-cast" -version = "1.0.9" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed13bcd201494ab44900a96490291651d200730904221832b9547d24a87d332b" +checksum = "300f2a835d808734ee295d45007adacb9ebb29dd3ae2424acfa17930cae541da" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.9" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5234cd6063258a5e32903b53b1b6ac043a0541c8adc1f610f67b0326c7a578fa" +checksum = "4c38e3aecd2b21cb3959637b883bb3714bc7e43f0268b9a29d3743ee3e55cdd2" dependencies = [ "proc-macro2", "quote", @@ -4572,9 +4584,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.6.0" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" dependencies = [ "aho-corasick", "memchr", @@ -4592,9 +4604,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] name = "remove_dir_all" @@ -4709,7 +4721,7 @@ dependencies = [ "memchr", "multer", "num_cpus", - "parking_lot 0.12.1", + "parking_lot 0.12.0", "pin-project-lite", "rand 0.8.5", "ref-cast", @@ -4722,7 +4734,7 @@ dependencies = [ "time 0.3.14", "tokio", "tokio-stream", - "tokio-util 0.7.4", + "tokio-util 0.7.3", "ubyte", "version_check", "yansi", @@ -4778,7 +4790,7 @@ dependencies = [ "pin-project-lite", "ref-cast", "serde", - "smallvec 1.9.0", + "smallvec 1.8.0", "stable-pattern", "state", "time 0.3.14", @@ -4836,7 +4848,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.14", + "semver 1.0.7", ] [[package]] @@ -4887,15 +4899,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.9" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" +checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" [[package]] name = "ryu" -version = "1.0.11" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" [[package]] name = "same-file" @@ -4908,12 +4920,12 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.20" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" dependencies = [ "lazy_static", - "windows-sys", + "winapi", ] [[package]] @@ -4980,7 +4992,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08da66b8b0965a5555b6bd6639e68ccba85e1e2506f5fbb089e93f8a04e1a2d1" dependencies = [ "der", - "generic-array 0.14.6", + "generic-array 0.14.5", "pkcs8", "subtle 2.4.1", "zeroize", @@ -5006,9 +5018,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.7.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" dependencies = [ "bitflags", "core-foundation", @@ -5047,9 +5059,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.14" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" +checksum = "d65bd28f48be7196d222d95b9243287f48d27aca604e08497513019ff0502cc4" [[package]] name = "semver-parser" @@ -5068,9 +5080,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.144" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" dependencies = [ "serde_derive", ] @@ -5086,9 +5098,9 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.7" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfc50e8183eeeb6178dcb167ae34a8051d63535023ae38b5d8d12beae193d37b" +checksum = "212e73464ebcde48d723aa02eb270ba62eff38a9b732df31f33f1b4e145f3a54" dependencies = [ "serde", ] @@ -5099,15 +5111,15 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" dependencies = [ - "half 1.8.2", + "half", "serde", ] [[package]] name = "serde_derive" -version = "1.0.144" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" dependencies = [ "proc-macro2", "quote", @@ -5127,20 +5139,20 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.85" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" +checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7" dependencies = [ - "itoa 1.0.3", + "itoa 1.0.1", "ryu", "serde", ] [[package]] name = "serde_repr" -version = "0.1.9" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" +checksum = "98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5" dependencies = [ "proc-macro2", "quote", @@ -5154,16 +5166,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.3", + "itoa 1.0.1", "ryu", "serde", ] [[package]] name = "serde_yaml" -version = "0.8.26" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" +checksum = "a4a521f2940385c165a24ee286aa8599633d162077a54bdcae2a6fd5a7bfa7a0" dependencies = [ "indexmap", "ryu", @@ -5171,6 +5183,18 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "sha-1" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +dependencies = [ + "block-buffer 0.7.3", + "digest 0.8.1", + "fake-simd", + "opaque-debug 0.2.3", +] + [[package]] name = "sha-1" version = "0.9.8" @@ -5195,17 +5219,6 @@ dependencies = [ "digest 0.10.3", ] -[[package]] -name = "sha1" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "006769ba83e921b3085caa8334186b00cf92b4cb1a6cf4632fbccc8eff5c7549" -dependencies = [ - "cfg-if 1.0.0", - "cpufeatures", - "digest 0.10.3", -] - [[package]] name = "sha2" version = "0.9.9" @@ -5221,9 +5234,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.5" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9db03534dff993187064c4e0c05a5708d2a9728ace9a8959b77bedf415dac5" +checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" dependencies = [ "cfg-if 1.0.0", "cpufeatures", @@ -5283,9 +5296,9 @@ dependencies = [ [[package]] name = "signature" -version = "1.4.0" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02658e48d89f2bec991f9a78e69cfa4c316f8d6a6c4ec12fae1aeb263d486788" +checksum = "f2807892cfa58e081aa1f1111391c7a0649d4fa127a4ffbe34bcbfb35a1171a4" dependencies = [ "digest 0.9.0", "rand_core 0.6.3", @@ -5293,12 +5306,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.7" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" -dependencies = [ - "autocfg 1.1.0", -] +checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" [[package]] name = "sled" @@ -5327,9 +5337,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.9.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" [[package]] name = "snafu" @@ -5338,7 +5348,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eab12d3c261b2308b0d80c26fffb58d17eba81a4be97890101f416b478c79ca7" dependencies = [ "doc-comment", - "snafu-derive", + "snafu-derive 0.6.10", +] + +[[package]] +name = "snafu" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5177903bf45656592d9eb5c0e22f408fc023aae51dbe2088889b71633ba451f2" +dependencies = [ + "doc-comment", + "snafu-derive 0.7.1", ] [[package]] @@ -5353,10 +5373,22 @@ dependencies = [ ] [[package]] -name = "socket2" -version = "0.4.7" +name = "snafu-derive" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +checksum = "410b26ed97440d90ced3e2488c868d56a86e2064f5d7d6f417909b286afe25e5" +dependencies = [ + "heck 0.4.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" dependencies = [ "libc", "winapi", @@ -5416,9 +5448,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spin" -version = "0.9.4" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09" +checksum = "511254be0c5bcf062b019a6c89c01a664aa359ded62f78aa72c6fc137c0590e5" dependencies = [ "lock_api", ] @@ -5457,12 +5489,12 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.5.13" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551873805652ba0d912fec5bbb0f8b4cdd96baf8e2ebf5970e5671092966019b" +checksum = "fc15591eb44ffb5816a4a70a7efd5dd87bfd3aa84c4c200401c4396140525826" dependencies = [ "sqlx-core 0.5.13", - "sqlx-macros 0.5.13", + "sqlx-macros 0.5.11", ] [[package]] @@ -5500,7 +5532,7 @@ dependencies = [ "hashlink 0.7.0", "hex", "indexmap", - "itoa 1.0.3", + "itoa 1.0.1", "libc", "libsqlite3-sys", "log", @@ -5509,8 +5541,8 @@ dependencies = [ "paste", "percent-encoding", "rustls 0.19.1", - "sha2 0.10.5", - "smallvec 1.9.0", + "sha2 0.10.2", + "smallvec 1.8.0", "sqlformat 0.1.8", "sqlx-rt 0.5.13", "stringprep", @@ -5544,10 +5576,10 @@ dependencies = [ "futures-executor", "futures-intrusive", "futures-util", - "hashlink 0.8.0", + "hashlink 0.8.1", "hex", "indexmap", - "itoa 1.0.3", + "itoa 1.0.1", "libc", "libsqlite3-sys", "log", @@ -5557,8 +5589,8 @@ dependencies = [ "percent-encoding", "rustls 0.20.6", "rustls-pemfile", - "sha2 0.10.5", - "smallvec 1.9.0", + "sha2 0.10.2", + "smallvec 1.8.0", "sqlformat 0.2.0", "sqlx-rt 0.6.2", "stringprep", @@ -5570,17 +5602,17 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.5.13" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0fba2b0cae21fc00fe6046f8baa4c7fcb49e379f0f592b04696607f69ed2e1" +checksum = "eee35713129561f5e55c554bba1c378e2a7e67f81257b7311183de98c50e6f94" dependencies = [ "dotenv", "either", - "heck 0.4.0", + "heck 0.3.3", "once_cell", "proc-macro2", "quote", - "sha2 0.10.5", + "sha2 0.9.9", "sqlx-core 0.5.13", "sqlx-rt 0.5.13", "syn", @@ -5599,7 +5631,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "sha2 0.10.5", + "sha2 0.10.2", "sqlx-core 0.6.2", "sqlx-rt 0.6.2", "syn", @@ -5661,7 +5693,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "sqlx 0.5.13", + "sqlx 0.5.11", "thiserror", "tokio", ] @@ -5746,21 +5778,15 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.99" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" dependencies = [ "proc-macro2", "quote", - "unicode-ident", + "unicode-xid", ] -[[package]] -name = "sync_wrapper" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" - [[package]] name = "synstructure" version = "0.12.6" @@ -5775,9 +5801,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.24.7" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54cb4ebf3d49308b99e6e9dc95e989e2fdbdc210e4f67c39db0bb89ba927001c" +checksum = "a6a8e71535da31837213ac114531d31def75d7aebd133264e420a3451fa7f703" dependencies = [ "cfg-if 1.0.0", "core-foundation-sys", @@ -5831,7 +5857,7 @@ dependencies = [ "k256", "num-traits", "once_cell", - "prost 0.10.4", + "prost 0.10.3", "prost-types 0.10.1", "ripemd160", "serde", @@ -5871,7 +5897,7 @@ dependencies = [ "flex-error", "num-derive", "num-traits", - "prost 0.10.4", + "prost 0.10.3", "prost-types 0.10.1", "serde", "serde_bytes", @@ -5889,7 +5915,7 @@ dependencies = [ "bytes", "flex-error", "futures", - "getrandom 0.2.7", + "getrandom 0.2.6", "http", "hyper", "hyper-proxy", @@ -5982,7 +6008,7 @@ version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" dependencies = [ - "itoa 1.0.3", + "itoa 1.0.1", "libc", "num_threads", "serde", @@ -6016,18 +6042,17 @@ dependencies = [ [[package]] name = "tokio" -version = "1.21.1" +version = "1.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95" +checksum = "95eec79ea28c00a365f539f1961e9278fbcaf81c0ff6aaf0e93c181352446948" dependencies = [ - "autocfg 1.1.0", "bytes", "libc", "memchr", "mio", "num_cpus", "once_cell", - "parking_lot 0.12.1", + "parking_lot 0.12.0", "pin-project-lite", "signal-hook-registry", "socket2", @@ -6048,9 +6073,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.8.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" dependencies = [ "proc-macro2", "quote", @@ -6098,7 +6123,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", - "tokio-util 0.7.4", + "tokio-util 0.7.3", ] [[package]] @@ -6141,9 +6166,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.10" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" +checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" dependencies = [ "bytes", "futures-core", @@ -6156,9 +6181,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" dependencies = [ "bytes", "futures-core", @@ -6171,22 +6196,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.9" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" dependencies = [ "serde", ] [[package]] name = "tonic" -version = "0.8.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11cd56bdb54ef93935a6a79dbd1d91f1ebd4c64150fd61654031fd6b8b775c91" +checksum = "ff08f4649d10a70ffa3522ca559031285d8e421d727ac85c60825761818f5d0a" dependencies = [ "async-stream", "async-trait", - "axum", "base64", "bytes", "futures-core", @@ -6198,11 +6222,11 @@ dependencies = [ "hyper-timeout", "percent-encoding", "pin-project", - "prost 0.11.0", - "prost-derive 0.11.0", + "prost 0.9.0", + "prost-derive 0.9.0", "tokio", "tokio-stream", - "tokio-util 0.7.4", + "tokio-util 0.6.9", "tower", "tower-layer", "tower-service", @@ -6210,6 +6234,18 @@ dependencies = [ "tracing-futures", ] +[[package]] +name = "tonic-build" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9403f1bafde247186684b230dc6f38b5cd514584e8bec1dd32514be4745fa757" +dependencies = [ + "proc-macro2", + "prost-build", + "quote", + "syn", +] + [[package]] name = "topology" version = "0.1.0" @@ -6226,9 +6262,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.4.13" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +checksum = "9a89fd63ad6adf737582df5db40d286574513c69a11dac5214dc3b5603d6713e" dependencies = [ "futures-core", "futures-util", @@ -6238,31 +6274,12 @@ dependencies = [ "rand 0.8.5", "slab", "tokio", - "tokio-util 0.7.4", + "tokio-util 0.7.3", "tower-layer", "tower-service", "tracing", ] -[[package]] -name = "tower-http" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c530c8675c1dbf98facee631536fa116b5fb6382d7dd6dc1b118d970eafe3ba" -dependencies = [ - "bitflags", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-range-header", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - [[package]] name = "tower-layer" version = "0.3.1" @@ -6271,15 +6288,15 @@ checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.36" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" +checksum = "80b9fa4360528139bc96100c160b7ae879f5567f49f1782b0b02035b0358ebf3" dependencies = [ "cfg-if 1.0.0", "log", @@ -6290,9 +6307,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.22" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" +checksum = "2e65ce065b4b5c53e73bb28912318cb8c9e9ad3921f1d669eb0e68b4c8143a2b" dependencies = [ "proc-macro2", "quote", @@ -6301,11 +6318,11 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.29" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" +checksum = "90442985ee2f57c9e1b548ee72ae842f4a9a20e3f417cc38dbc5dc684d9bb4ee" dependencies = [ - "once_cell", + "lazy_static", "valuable", ] @@ -6321,9 +6338,9 @@ dependencies = [ [[package]] name = "tracing-log" -version = "0.1.3" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3" dependencies = [ "lazy_static", "log", @@ -6332,16 +6349,16 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.15" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60db860322da191b40952ad9affe65ea23e7dd6a5c442c2c42865810c6ab8e6b" +checksum = "4bc28f93baff38037f64e6f43d34cfa1605f27a49c34e8a04c5e78b0babf2596" dependencies = [ "ansi_term", + "lazy_static", "matchers", - "once_cell", "regex", "sharded-slab", - "smallvec 1.9.0", + "smallvec 1.8.0", "thread_local", "tracing", "tracing-core", @@ -6356,9 +6373,9 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "ts-rs" -version = "6.2.0" +version = "6.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc59f479df54269b400dd95bc3b7e81623b3e4b9c70c8ca7125ab8341eafa64e" +checksum = "3d26cba30c9b3a2f537f765cf754126f11c983b7426d280ae0b4cef2374cab98" dependencies = [ "thiserror", "ts-rs-macros", @@ -6381,9 +6398,9 @@ dependencies = [ [[package]] name = "ts-rs-macros" -version = "6.2.0" +version = "6.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f807fdb3151fee75df7485b901a89624358cd07a67a8fb1a5831bf5a07681ff" +checksum = "fd8e302fbcf5b60dfa1df443535967511442c5ce4eac8b4c9fa811f2274280a4" dependencies = [ "Inflector", "proc-macro2", @@ -6439,18 +6456,18 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "ubyte" -version = "0.10.3" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c81f0dae7d286ad0d9366d7679a77934cfc3cf3a8d67e82669794412b2368fe6" +checksum = "42756bb9e708855de2f8a98195643dff31a97f0485d90d8467b39dc24be9e8fe" dependencies = [ "serde", ] [[package]] name = "ucd-trie" -version = "0.1.5" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" [[package]] name = "uint" @@ -6466,9 +6483,9 @@ dependencies = [ [[package]] name = "uncased" -version = "0.9.7" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b01702b0fd0b3fadcf98e098780badda8742d4f4a7676615cad90e8ac73622" +checksum = "5baeed7327e25054889b9bd4f975f32e5f4c5d434042d59ab6cd4142c0a76ed0" dependencies = [ "serde", "version_check", @@ -6495,15 +6512,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.8" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" - -[[package]] -name = "unicode-ident" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" [[package]] name = "unicode-normalization" @@ -6516,21 +6527,21 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.10.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" [[package]] name = "unicode-xid" -version = "0.2.3" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] name = "unicode_categories" @@ -6544,7 +6555,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.5", "subtle 2.4.1", ] @@ -6556,12 +6567,13 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.3.0" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22fe195a4f217c25b25cb5058ced57059824a678474874038dc88d211bf508d3" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" dependencies = [ "form_urlencoded", "idna", + "matches", "percent-encoding", "serde", ] @@ -6585,6 +6597,7 @@ dependencies = [ "bs58", "coconut-interface", "cosmrs", + "cosmwasm-std", "getset", "mixnet-contract-common", "schemars", @@ -6614,7 +6627,7 @@ dependencies = [ "mixnet-contract-common", "multisig-contract-common", "network-defaults", - "prost 0.10.4", + "prost 0.10.3", "reqwest", "serde", "serde_json", @@ -6649,7 +6662,7 @@ dependencies = [ "anyhow", "cfg-if 1.0.0", "chrono", - "enum-iterator 0.8.1", + "enum-iterator", "getset", "git2", "rustc_version 0.4.0", @@ -6659,13 +6672,13 @@ dependencies = [ [[package]] name = "vergen" -version = "7.4.2" +version = "7.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ba753d713ec3844652ad2cb7eb56bc71e34213a14faddac7852a10ba88f61e" +checksum = "b1f44ef1afcf5979e34748c12595f9589f3dc4e34abf156fb6d95f9b835568dc" dependencies = [ "anyhow", "cfg-if 1.0.0", - "enum-iterator 1.1.3", + "enum-iterator", "getset", "git2", "rustc_version 0.4.0", @@ -6689,7 +6702,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vesting-contract" -version = "1.0.2" +version = "1.1.0" dependencies = [ "cosmwasm-std", "cw-storage-plus", @@ -6867,7 +6880,7 @@ dependencies = [ "tiny-keccak", "tokio", "tokio-stream", - "tokio-util 0.6.10", + "tokio-util 0.6.9", "url", "web3-async-native-tls", ] @@ -6931,6 +6944,17 @@ dependencies = [ "serde_json", ] +[[package]] +name = "which" +version = "4.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c4fb54e6113b6a8772ee41c3404fb0301ac79604489467e0a9ce1f3e97c24ae" +dependencies = [ + "either", + "lazy_static", + "libc", +] + [[package]] name = "winapi" version = "0.3.9" @@ -6962,91 +6986,48 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbedf6db9096bc2364adce0ae0aa636dcd89f3c3f2cd67947062aaf0ca2a10ec" -dependencies = [ - "windows_aarch64_msvc 0.32.0", - "windows_i686_gnu 0.32.0", - "windows_i686_msvc 0.32.0", - "windows_x86_64_gnu 0.32.0", - "windows_x86_64_msvc 0.32.0", -] - [[package]] name = "windows-sys" -version = "0.36.1" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825" dependencies = [ - "windows_aarch64_msvc 0.36.1", - "windows_i686_gnu 0.36.1", - "windows_i686_msvc 0.36.1", - "windows_x86_64_gnu 0.36.1", - "windows_x86_64_msvc 0.36.1", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_msvc" -version = "0.32.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" [[package]] name = "windows_i686_gnu" -version = "0.32.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" - -[[package]] -name = "windows_i686_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" [[package]] name = "windows_i686_msvc" -version = "0.32.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" - -[[package]] -name = "windows_i686_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" [[package]] name = "windows_x86_64_gnu" -version = "0.32.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" [[package]] name = "windows_x86_64_msvc" -version = "0.32.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" [[package]] name = "winreg" @@ -7101,9 +7082,9 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "zeroize" -version = "1.5.7" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" +checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619" dependencies = [ "zeroize_derive", ] diff --git a/Cargo.toml b/Cargo.toml index 5085ab54c0..b6c45da88b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ members = [ "common/crypto/dkg", "common/execute", "common/inclusion-probability", + "common/ledger", "common/mixnode-common", "common/network-defaults", "common/nonexhaustive-delayqueue", diff --git a/clients/client-core/src/client/cover_traffic_stream.rs b/clients/client-core/src/client/cover_traffic_stream.rs index 8e25357e00..0223635b81 100644 --- a/clients/client-core/src/client/cover_traffic_stream.rs +++ b/clients/client-core/src/client/cover_traffic_stream.rs @@ -136,7 +136,7 @@ impl LoopCoverTrafficStream { let cover_message = generate_loop_cover_packet( &mut self.rng, topology_ref, - &*self.ack_key, + &self.ack_key, &self.our_full_destination, self.average_ack_delay, self.average_packet_delay, diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs index 5585044654..020a7c9e74 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -228,7 +228,7 @@ where generate_loop_cover_packet( &mut self.rng, topology_ref, - &*self.ack_key, + &self.ack_key, &self.our_full_destination, self.config.average_ack_delay, self.config.average_packet_delay, diff --git a/clients/client-core/src/client/topology_control.rs b/clients/client-core/src/client/topology_control.rs index b7bc7325fb..c276fdd49d 100644 --- a/clients/client-core/src/client/topology_control.rs +++ b/clients/client-core/src/client/topology_control.rs @@ -13,7 +13,7 @@ use std::time::Duration; use task::ShutdownListener; use tokio::sync::{RwLock, RwLockReadGuard}; use tokio::task::JoinHandle; -use topology::{nym_topology_from_bonds, NymTopology}; +use topology::{nym_topology_from_detailed, NymTopology}; use url::Url; // I'm extremely curious why compiler NEVER complained about lack of Debug here before @@ -266,8 +266,8 @@ impl TopologyRefresher { }; let mixnodes_count = mixnodes.len(); - let topology = - nym_topology_from_bonds(mixnodes, gateways).filter_system_version(&self.client_version); + let topology = nym_topology_from_detailed(mixnodes, gateways) + .filter_system_version(&self.client_version); if !self.check_layer_distribution(&topology, mixnodes_count) { warn!("The current filtered active topology has extremely skewed layer distribution. It cannot be used."); diff --git a/clients/native/src/commands/upgrade.rs b/clients/native/src/commands/upgrade.rs index c925b10f08..c84073200c 100644 --- a/clients/native/src/commands/upgrade.rs +++ b/clients/native/src/commands/upgrade.rs @@ -103,7 +103,7 @@ fn minor_0_12_upgrade( Version::new(0, 12, 0) }; - print_start_upgrade(&config_version, &to_version); + print_start_upgrade(config_version, &to_version); config .get_base_mut() @@ -111,7 +111,7 @@ fn minor_0_12_upgrade( config.save_to_file(None).unwrap_or_else(|err| { eprintln!("failed to overwrite config file! - {:?}", err); - print_failed_upgrade(&config_version, &to_version); + print_failed_upgrade(config_version, &to_version); process::exit(1); }); diff --git a/clients/socks5/src/commands/upgrade.rs b/clients/socks5/src/commands/upgrade.rs index 5e9517ac2e..ff41f3ce6e 100644 --- a/clients/socks5/src/commands/upgrade.rs +++ b/clients/socks5/src/commands/upgrade.rs @@ -102,7 +102,7 @@ fn minor_0_12_upgrade( Version::new(0, 12, 0) }; - print_start_upgrade(&config_version, &to_version); + print_start_upgrade(config_version, &to_version); config .get_base_mut() @@ -110,7 +110,7 @@ fn minor_0_12_upgrade( config.save_to_file(None).unwrap_or_else(|err| { eprintln!("failed to overwrite config file! - {:?}", err); - print_failed_upgrade(&config_version, &to_version); + print_failed_upgrade(config_version, &to_version); process::exit(1); }); diff --git a/clients/webassembly/js-example/index.js b/clients/webassembly/js-example/index.js index 7c2d3151d5..22a42a09b3 100644 --- a/clients/webassembly/js-example/index.js +++ b/clients/webassembly/js-example/index.js @@ -25,7 +25,7 @@ async function main() { set_panic_hook(); // validator server we will use to get topology from - const validator = "https://validator.nymtech.net/api"; //"http://localhost:8081"; + const validator = "https://validator.nymtech.net/api"; client = new NymClient(validator); @@ -69,7 +69,7 @@ async function sendMessageTo() { * @param {string} message */ function displaySend(message) { - let timestamp = new Date().toISOString().substr(11, 12); + let timestamp = new Date().toISOString().slice(11, 21); let sendDiv = document.createElement("div") let paragraph = document.createElement("p") @@ -90,11 +90,11 @@ function displayReceived(message) { const content = message.message const replySurb = message.replySurb - let timestamp = new Date().toISOString().substr(11, 12); + let timestamp = new Date().toISOString().slice(11, 21); let receivedDiv = document.createElement("div") let paragraph = document.createElement("p") paragraph.setAttribute('style', 'color: green') - let paragraphContent = document.createTextNode(timestamp + " received >>> " + content + ((replySurb != null) ? "Reply SURB was attached here (but we can't do anything with it yet" : " (NO REPLY-SURB AVAILABLE)")) + let paragraphContent = document.createTextNode(timestamp + " received >>> " + content) paragraph.appendChild(paragraphContent) receivedDiv.appendChild(paragraph) document.getElementById("output").appendChild(receivedDiv) diff --git a/clients/webassembly/js-example/package.json b/clients/webassembly/js-example/package.json index 361ae23c0e..b4eb7d5244 100644 --- a/clients/webassembly/js-example/package.json +++ b/clients/webassembly/js-example/package.json @@ -34,6 +34,6 @@ "webpack-dev-server": "^4.7.4" }, "dependencies": { - "@nymproject/nym-client-wasm": "file:../pkg" + "@nymproject/nym-client-wasm": "1.0.0" } -} +} \ No newline at end of file diff --git a/clients/webassembly/src/client/mod.rs b/clients/webassembly/src/client/mod.rs index 57e20931ca..848010516b 100644 --- a/clients/webassembly/src/client/mod.rs +++ b/clients/webassembly/src/client/mod.rs @@ -11,7 +11,7 @@ use rand::rngs::OsRng; use received_processor::ReceivedMessagesProcessor; use std::sync::Arc; use std::time::Duration; -use topology::{gateway, nym_topology_from_bonds, NymTopology}; +use topology::{gateway, nym_topology_from_detailed, NymTopology}; use url::Url; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; @@ -265,7 +265,7 @@ impl NymClient { Ok(gateways) => gateways, }; - let topology = nym_topology_from_bonds(mixnodes, gateways); + let topology = nym_topology_from_detailed(mixnodes, gateways); let version = env!("CARGO_PKG_VERSION"); topology.filter_system_version(version) } diff --git a/common/client-libs/mixnet-client/src/client.rs b/common/client-libs/mixnet-client/src/client.rs index 4e089a30c3..181eac65ec 100644 --- a/common/client-libs/mixnet-client/src/client.rs +++ b/common/client-libs/mixnet-client/src/client.rs @@ -186,7 +186,7 @@ impl Client { address.into(), receiver, initial_connection_timeout, - &*current_reconnection_attempt, + ¤t_reconnection_attempt, ) .await }); diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index d820611bf0..8ae389084a 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -2,35 +2,33 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{validator_api, ValidatorClientError}; -use mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixNodeBond}; +use mixnet_contract_common::mixnode::MixNodeDetails; +use mixnet_contract_common::NodeId; +use mixnet_contract_common::{GatewayBond, IdentityKeyRef}; use url::Url; use validator_api_requests::coconut::{ BlindSignRequestBody, BlindedSignatureResponse, CosmosAddressResponse, VerificationKeyResponse, VerifyCredentialBody, VerifyCredentialResponse, }; use validator_api_requests::models::{ - CoreNodeStatusResponse, MixnodeStatusResponse, RewardEstimationResponse, - StakeSaturationResponse, + GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse, + RewardEstimationResponse, StakeSaturationResponse, }; #[cfg(feature = "nymd-client")] -use validator_api_requests::models::{MixNodeBondAnnotated, UptimeResponse}; - +use crate::nymd::traits::MixnetQueryClient; #[cfg(feature = "nymd-client")] -use crate::nymd::{ - self, error::NymdError, CosmWasmClient, NymdClient, QueryNymdClient, SigningNymdClient, -}; - +use crate::nymd::{self, CosmWasmClient, NymdClient, QueryNymdClient, SigningNymdClient}; #[cfg(feature = "nymd-client")] use mixnet_contract_common::{ - mixnode::DelegationEvent, ContractStateParams, Delegation, IdentityKey, Interval, - MixnetContractVersion, MixnodeRewardingStatusResponse, RewardedSetNodeStatus, - RewardedSetUpdateDetails, + mixnode::MixNodeBond, + pending_events::{PendingEpochEvent, PendingIntervalEvent}, + Delegation, RewardedSetNodeStatus, UnbondedMixnode, }; #[cfg(feature = "nymd-client")] use network_defaults::NymNetworkDetails; #[cfg(feature = "nymd-client")] -use std::collections::{HashMap, HashSet}; +use validator_api_requests::models::MixNodeBondAnnotated; #[cfg(feature = "nymd-client")] #[must_use] @@ -191,12 +189,9 @@ impl Client { } } +// nymd wrappers #[cfg(feature = "nymd-client")] impl Client { - pub fn change_validator_api(&mut self, new_endpoint: Url) { - self.validator_api.change_url(new_endpoint) - } - // use case: somebody initialised client without a contract in order to upload and initialise one // and now they want to actually use it without making new client pub fn set_mixnet_contract_address(&mut self, mixnet_contract_address: cosmrs::AccountId) { @@ -208,203 +203,22 @@ impl Client { self.nymd.mixnet_contract_address().clone() } - pub async fn get_cached_mixnodes(&self) -> Result, ValidatorClientError> { - Ok(self.validator_api.get_mixnodes().await?) - } - - pub async fn get_cached_mixnodes_detailed( - &self, - ) -> Result, ValidatorClientError> { - Ok(self.validator_api.get_mixnodes_detailed().await?) - } - - pub async fn get_cached_rewarded_mixnodes( - &self, - ) -> Result, ValidatorClientError> { - Ok(self.validator_api.get_rewarded_mixnodes().await?) - } - - pub async fn get_cached_rewarded_mixnodes_detailed( - &self, - ) -> Result, ValidatorClientError> { - Ok(self.validator_api.get_rewarded_mixnodes_detailed().await?) - } - - pub async fn get_cached_active_mixnodes( - &self, - ) -> Result, ValidatorClientError> { - Ok(self.validator_api.get_active_mixnodes().await?) - } - - pub async fn get_cached_active_mixnodes_detailed( - &self, - ) -> Result, ValidatorClientError> { - Ok(self.validator_api.get_active_mixnodes_detailed().await?) - } - - pub async fn get_cached_gateways(&self) -> Result, ValidatorClientError> { - Ok(self.validator_api.get_gateways().await?) - } - - pub async fn get_contract_settings(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.nymd.get_contract_settings().await?) - } - - pub async fn get_operator_rewards(&self, address: String) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.nymd.get_operator_rewards(address).await?.u128()) - } - - pub async fn get_delegator_rewards( - &self, - address: String, - mix_identity: IdentityKey, - proxy: Option, - ) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self - .nymd - .get_delegator_rewards(address, mix_identity, proxy) - .await? - .u128()) - } - - pub async fn get_pending_delegation_events( - &self, - owner_address: String, - proxy_address: Option, - ) -> Result, ValidatorClientError> - where - C: CosmWasmClient + Sync, - { - Ok(self - .nymd - .get_pending_delegation_events(owner_address, proxy_address) - .await?) - } - - pub async fn get_current_epoch(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.nymd.get_current_epoch().await?) - } - - pub async fn get_current_operator_cost(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.nymd.get_current_operator_cost().await?) - } - - pub async fn get_mixnet_contract_version(&self) -> Result - where - C: CosmWasmClient + Sync, - { - self.nymd.get_mixnet_contract_version().await - } - - pub async fn get_rewarding_status( - &self, - mix_identity: mixnet_contract_common::IdentityKey, - rewarding_interval_nonce: u32, - ) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self - .nymd - .get_rewarding_status(mix_identity, rewarding_interval_nonce) - .await?) - } - - pub async fn get_reward_pool(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.nymd.get_reward_pool().await?.u128()) - } - - pub async fn get_circulating_supply(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.nymd.get_circulating_supply().await?.u128()) - } - - pub async fn get_sybil_resistance_percent(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.nymd.get_sybil_resistance_percent().await?) - } - - pub async fn get_active_set_work_factor(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.nymd.get_active_set_work_factor().await?) - } - - pub async fn get_epochs_in_interval(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.nymd.get_epochs_in_interval().await?) - } - - pub async fn get_interval_reward_percent(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.nymd.get_interval_reward_percent().await?) - } - - pub async fn get_current_rewarded_set_update_details( - &self, - ) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self - .nymd - .query_current_rewarded_set_update_details() - .await?) - } - // basically handles paging for us - pub async fn get_all_nymd_rewarded_set_mixnode_identities( + pub async fn get_all_nymd_rewarded_set_mixnodes( &self, - ) -> Result, ValidatorClientError> + ) -> Result, ValidatorClientError> where - C: CosmWasmClient + Sync, + C: CosmWasmClient + Sync + Send, { let mut identities = Vec::new(); let mut start_after = None; - let mut height = None; loop { let mut paged_response = self .nymd - .get_rewarded_set_identities_paged( - start_after.take(), - self.rewarded_set_page_limit, - height, - ) + .get_rewarded_set_paged(start_after.take(), self.rewarded_set_page_limit) .await?; - identities.append(&mut paged_response.identities); - - if height.is_none() { - // keep using the same height (the first query happened at the most recent height) - height = Some(paged_response.at_height) - } + identities.append(&mut paged_response.nodes); if let Some(start_after_res) = paged_response.start_next_after { start_after = Some(start_after_res) @@ -416,83 +230,122 @@ impl Client { Ok(identities) } - pub async fn get_nymd_rewarded_and_active_sets( - &self, - ) -> Result, ValidatorClientError> + pub async fn get_all_nymd_mixnode_bonds(&self) -> Result, ValidatorClientError> where - C: CosmWasmClient + Sync, - { - let all_mixnodes = self.get_all_nymd_mixnodes().await?; - let rewarded_set_identities = self - .get_all_nymd_rewarded_set_mixnode_identities() - .await? - .into_iter() - .collect::>(); - - Ok(all_mixnodes - .into_iter() - .filter_map(|node| { - rewarded_set_identities - .get(node.identity()) - .map(|status| (node, *status)) - }) - .collect()) - } - - /// If you need both rewarded and the active set, consider using [Self::get_nymd_rewarded_and_active_sets] instead - pub async fn get_nymd_rewarded_set(&self) -> Result, ValidatorClientError> - where - C: CosmWasmClient + Sync, - { - let all_mixnodes = self.get_all_nymd_mixnodes().await?; - let rewarded_set_identities = self - .get_all_nymd_rewarded_set_mixnode_identities() - .await? - .into_iter() - .map(|(identity, _status)| identity) - .collect::>(); - - Ok(all_mixnodes - .into_iter() - .filter(|node| rewarded_set_identities.contains(node.identity())) - .collect()) - } - - /// If you need both rewarded and the active set, consider using [Self::get_nymd_rewarded_and_active_sets] instead - pub async fn get_nymd_active_set(&self) -> Result, ValidatorClientError> - where - C: CosmWasmClient + Sync, - { - let all_mixnodes = self.get_all_nymd_mixnodes().await?; - let active_set_identities = self - .get_all_nymd_rewarded_set_mixnode_identities() - .await? - .into_iter() - .filter_map(|(identity, status)| { - if status.is_active() { - Some(identity) - } else { - None - } - }) - .collect::>(); - - Ok(all_mixnodes - .into_iter() - .filter(|node| active_set_identities.contains(node.identity())) - .collect()) - } - - pub async fn get_all_nymd_mixnodes(&self) -> Result, ValidatorClientError> - where - C: CosmWasmClient + Sync, + C: CosmWasmClient + Sync + Send, { let mut mixnodes = Vec::new(); let mut start_after = None; loop { let mut paged_response = self .nymd - .get_mixnodes_paged(start_after.take(), self.mixnode_page_limit) + .get_mixnode_bonds_paged(self.mixnode_page_limit, start_after.take()) + .await?; + mixnodes.append(&mut paged_response.nodes); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res) + } else { + break; + } + } + + Ok(mixnodes) + } + + pub async fn get_all_nymd_mixnodes_detailed( + &self, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + let mut mixnodes = Vec::new(); + let mut start_after = None; + loop { + let mut paged_response = self + .nymd + .get_mixnodes_detailed_paged(self.mixnode_page_limit, start_after.take()) + .await?; + mixnodes.append(&mut paged_response.nodes); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res) + } else { + break; + } + } + + Ok(mixnodes) + } + + pub async fn get_all_nymd_unbonded_mixnodes( + &self, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + let mut mixnodes = Vec::new(); + let mut start_after = None; + loop { + let mut paged_response = self + .nymd + .get_unbonded_paged(self.mixnode_page_limit, start_after.take()) + .await?; + mixnodes.append(&mut paged_response.nodes); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res) + } else { + break; + } + } + + Ok(mixnodes) + } + + pub async fn get_all_nymd_unbonded_mixnodes_by_owner( + &self, + owner: &cosmrs::AccountId, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + let mut mixnodes = Vec::new(); + let mut start_after = None; + loop { + let mut paged_response = self + .nymd + .get_unbonded_by_owner_paged(owner, self.mixnode_page_limit, start_after.take()) + .await?; + mixnodes.append(&mut paged_response.nodes); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res) + } else { + break; + } + } + + Ok(mixnodes) + } + + pub async fn get_all_nymd_unbonded_mixnodes_by_identity( + &self, + identity_key: String, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + let mut mixnodes = Vec::new(); + let mut start_after = None; + loop { + let mut paged_response = self + .nymd + .get_unbonded_by_identity_paged( + identity_key.clone(), + self.mixnode_page_limit, + start_after.take(), + ) .await?; mixnodes.append(&mut paged_response.nodes); @@ -508,7 +361,7 @@ impl Client { pub async fn get_all_nymd_gateways(&self) -> Result, ValidatorClientError> where - C: CosmWasmClient + Sync, + C: CosmWasmClient + Sync + Send, { let mut gateways = Vec::new(); let mut start_after = None; @@ -531,18 +384,18 @@ impl Client { pub async fn get_all_nymd_single_mixnode_delegations( &self, - identity: IdentityKey, + mix_id: NodeId, ) -> Result, ValidatorClientError> where - C: CosmWasmClient + Sync, + C: CosmWasmClient + Sync + Send, { let mut delegations = Vec::new(); let mut start_after = None; loop { let mut paged_response = self .nymd - .get_mix_delegations_paged( - identity.clone(), + .get_mixnode_delegations_paged( + mix_id, start_after.take(), self.mixnode_delegations_page_limit, ) @@ -564,7 +417,7 @@ impl Client { delegation_owner: &cosmrs::AccountId, ) -> Result, ValidatorClientError> where - C: CosmWasmClient + Sync, + C: CosmWasmClient + Sync + Send, { let mut delegations = Vec::new(); let mut start_after = None; @@ -589,10 +442,128 @@ impl Client { Ok(delegations) } - pub async fn get_mixnode_avg_uptimes( + pub async fn get_all_network_delegations(&self) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + let mut delegations = Vec::new(); + let mut start_after = None; + loop { + let mut paged_response = self + .nymd + .get_all_network_delegations_paged( + start_after.take(), + self.mixnode_delegations_page_limit, + ) + .await?; + 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; + } + } + + Ok(delegations) + } + + pub async fn get_all_nymd_pending_epoch_events( &self, - ) -> Result, ValidatorClientError> { - Ok(self.validator_api.get_mixnode_avg_uptimes().await?) + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + let mut events = Vec::new(); + let mut start_after = None; + + loop { + let mut paged_response = self + .nymd + .get_pending_epoch_events_paged(start_after.take(), self.rewarded_set_page_limit) + .await?; + events.append(&mut paged_response.events); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res) + } else { + break; + } + } + + Ok(events) + } + + pub async fn get_all_nymd_pending_interval_events( + &self, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + let mut events = Vec::new(); + let mut start_after = None; + + loop { + let mut paged_response = self + .nymd + .get_pending_interval_events_paged(start_after.take(), self.rewarded_set_page_limit) + .await?; + events.append(&mut paged_response.events); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res) + } else { + break; + } + } + + Ok(events) + } +} + +// validator-api wrappers +#[cfg(feature = "nymd-client")] +impl Client { + pub fn change_validator_api(&mut self, new_endpoint: Url) { + self.validator_api.change_url(new_endpoint) + } + + pub async fn get_cached_mixnodes(&self) -> Result, ValidatorClientError> { + Ok(self.validator_api.get_mixnodes().await?) + } + + pub async fn get_cached_mixnodes_detailed( + &self, + ) -> Result, ValidatorClientError> { + Ok(self.validator_api.get_mixnodes_detailed().await?) + } + + pub async fn get_cached_rewarded_mixnodes( + &self, + ) -> Result, ValidatorClientError> { + Ok(self.validator_api.get_rewarded_mixnodes().await?) + } + + pub async fn get_cached_rewarded_mixnodes_detailed( + &self, + ) -> Result, ValidatorClientError> { + Ok(self.validator_api.get_rewarded_mixnodes_detailed().await?) + } + + pub async fn get_cached_active_mixnodes( + &self, + ) -> Result, ValidatorClientError> { + Ok(self.validator_api.get_active_mixnodes().await?) + } + + pub async fn get_cached_active_mixnodes_detailed( + &self, + ) -> Result, ValidatorClientError> { + Ok(self.validator_api.get_active_mixnodes_detailed().await?) + } + + pub async fn get_cached_gateways(&self) -> Result, ValidatorClientError> { + Ok(self.validator_api.get_gateways().await?) } pub async fn blind_sign( @@ -630,17 +601,17 @@ impl ApiClient { pub async fn get_cached_active_mixnodes( &self, - ) -> Result, ValidatorClientError> { + ) -> Result, ValidatorClientError> { Ok(self.validator_api.get_active_mixnodes().await?) } pub async fn get_cached_rewarded_mixnodes( &self, - ) -> Result, ValidatorClientError> { + ) -> Result, ValidatorClientError> { Ok(self.validator_api.get_rewarded_mixnodes().await?) } - pub async fn get_cached_mixnodes(&self) -> Result, ValidatorClientError> { + pub async fn get_cached_mixnodes(&self) -> Result, ValidatorClientError> { Ok(self.validator_api.get_mixnodes().await?) } @@ -652,7 +623,7 @@ impl ApiClient { &self, identity: IdentityKeyRef<'_>, since: Option, - ) -> Result { + ) -> Result { Ok(self .validator_api .get_gateway_core_status_count(identity, since) @@ -661,39 +632,39 @@ impl ApiClient { pub async fn get_mixnode_core_status_count( &self, - identity: IdentityKeyRef<'_>, + mix_id: NodeId, since: Option, - ) -> Result { + ) -> Result { Ok(self .validator_api - .get_mixnode_core_status_count(identity, since) + .get_mixnode_core_status_count(mix_id, since) .await?) } pub async fn get_mixnode_status( &self, - identity: IdentityKeyRef<'_>, + mix_id: NodeId, ) -> Result { - Ok(self.validator_api.get_mixnode_status(identity).await?) + Ok(self.validator_api.get_mixnode_status(mix_id).await?) } pub async fn get_mixnode_reward_estimation( &self, - identity: IdentityKeyRef<'_>, + mix_id: NodeId, ) -> Result { Ok(self .validator_api - .get_mixnode_reward_estimation(identity) + .get_mixnode_reward_estimation(mix_id) .await?) } pub async fn get_mixnode_stake_saturation( &self, - identity: IdentityKeyRef<'_>, + mix_id: NodeId, ) -> Result { Ok(self .validator_api - .get_mixnode_stake_saturation(identity) + .get_mixnode_stake_saturation(mix_id) .await?) } diff --git a/common/client-libs/validator-client/src/connection_tester.rs b/common/client-libs/validator-client/src/connection_tester.rs index 33e698bb2c..3193fc32e0 100644 --- a/common/client-libs/validator-client/src/connection_tester.rs +++ b/common/client-libs/validator-client/src/connection_tester.rs @@ -3,6 +3,7 @@ use crate::nymd::{Config as ClientConfig, NymdClient, QueryNymdClient}; use crate::ApiClient; use network_defaults::all::Network; +use crate::nymd::traits::MixnetQueryClient; use colored::Colorize; use core::fmt; use itertools::Itertools; diff --git a/common/client-libs/validator-client/src/nymd/mod.rs b/common/client-libs/validator-client/src/nymd/mod.rs index e43db6a6a2..1a508aaa2f 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -17,15 +17,7 @@ use cosmrs::rpc::HttpClientUrl; use cosmrs::tx::Msg; use cosmwasm_std::Uint128; use execute::execute; -use mixnet_contract_common::mixnode::DelegationEvent; -use mixnet_contract_common::{ - ContractStateParams, Delegation, ExecuteMsg, Gateway, GatewayBond, GatewayBondResponse, - GatewayOwnershipResponse, IdentityKey, Interval, LayerDistribution, MixNode, MixNodeBond, - MixOwnershipResponse, MixnetContractVersion, MixnodeBondResponse, - MixnodeRewardingStatusResponse, PagedDelegatorDelegationsResponse, PagedGatewayResponse, - PagedMixDelegationsResponse, PagedMixnodeResponse, PagedRewardedSetResponse, QueryMsg, - RewardedSetUpdateDetails, -}; +use network_defaults::{ChainDetails, NymNetworkDetails}; use serde::{Deserialize, Serialize}; use std::convert::TryInto; use std::time::SystemTime; @@ -52,7 +44,7 @@ pub use cosmrs::Coin as CosmosCoin; pub use cosmrs::{bip32, AccountId, Decimal, Denom}; pub use cosmwasm_std::Coin as CosmWasmCoin; pub use fee::{gas_price::GasPrice, GasAdjustable, GasAdjustment}; -use network_defaults::{ChainDetails, NymNetworkDetails}; +use mixnet_contract_common::NodeId; pub use signing_client::Client as SigningNymdClient; pub use traits::{VestingQueryClient, VestingSigningClient}; @@ -356,6 +348,12 @@ impl NymdClient { self.simulated_gas_multiplier } + // ============= + // CHAIN RELATED + // ============= + + // CHAIN QUERIES + pub async fn account_sequence(&self) -> Result where C: SigningCosmWasmClient + Sync, @@ -484,26 +482,6 @@ impl NymdClient { self.client.get_total_supply().await } - pub async fn get_contract_settings(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::StateParams {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_operator_rewards(&self, address: String) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::QueryOperatorReward { address }; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - pub async fn vesting_get_locked_pledge_cap(&self) -> Result where C: CosmWasmClient + Sync, @@ -514,357 +492,6 @@ impl NymdClient { .await } - pub async fn get_delegator_rewards( - &self, - address: String, - mix_identity: IdentityKey, - proxy: Option, - ) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::QueryDelegatorReward { - address, - mix_identity, - proxy, - }; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_pending_delegation_events( - &self, - owner_address: String, - proxy_address: Option, - ) -> Result, NymdError> - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetPendingDelegationEvents { - owner_address, - proxy_address, - }; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_current_epoch(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetCurrentEpoch {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_current_operator_cost(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetCurrentOperatorCost {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_mixnet_contract_version(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetContractVersion {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_rewarding_status( - &self, - mix_identity: mixnet_contract_common::IdentityKey, - interval_id: u32, - ) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetRewardingStatus { - mix_identity, - interval_id, - }; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn query_current_rewarded_set_height(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetCurrentRewardedSetHeight {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn query_current_rewarded_set_update_details( - &self, - ) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetRewardedSetUpdateDetails {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_rewarded_set_identities_paged( - &self, - start_after: Option, - page_limit: Option, - height: Option, - ) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetRewardedSet { - height, - start_after, - limit: page_limit, - }; - - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_layer_distribution(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::LayerDistribution {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_reward_pool(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetRewardPool {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_circulating_supply(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetCirculatingSupply {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_sybil_resistance_percent(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetSybilResistancePercent {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_active_set_work_factor(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetActiveSetWorkFactor {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_interval_reward_percent(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetIntervalRewardPercent {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_epochs_in_interval(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetEpochsInInterval {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - /// Checks whether there is a bonded mixnode associated with the provided client's address - pub async fn owns_mixnode(&self, address: &AccountId) -> Result, NymdError> - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::OwnsMixnode { - address: address.to_string(), - }; - let response: MixOwnershipResponse = self - .client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await?; - Ok(response.mixnode) - } - - /// Checks whether there is a bonded gateway associated with the provided client's address - pub async fn owns_gateway(&self, address: &AccountId) -> Result, NymdError> - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::OwnsGateway { - address: address.to_string(), - }; - let response: GatewayOwnershipResponse = self - .client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await?; - Ok(response.gateway) - } - - /// Checks whether there is a bonded mixnode associated with the provided identity key - pub async fn get_mixnode_bond( - &self, - identity: IdentityKey, - ) -> Result, NymdError> - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetMixnodeBond { identity }; - let response: MixnodeBondResponse = self - .client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await?; - Ok(response.mixnode) - } - - /// Checks whether there is a bonded gateway associated with the provided identity key - pub async fn get_gateway_bond( - &self, - identity: IdentityKey, - ) -> Result, NymdError> - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetGatewayBond { identity }; - let response: GatewayBondResponse = self - .client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await?; - Ok(response.gateway) - } - - pub async fn get_mixnodes_paged( - &self, - start_after: Option, - page_limit: Option, - ) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetMixNodes { - start_after, - limit: page_limit, - }; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_gateways_paged( - &self, - start_after: Option, - page_limit: Option, - ) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetGateways { - start_after, - limit: page_limit, - }; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - /// Gets list of all delegations towards particular mixnode on particular page. - pub async fn get_mix_delegations_paged( - &self, - mix_identity: IdentityKey, - start_after: Option<(String, u64)>, - page_limit: Option, - ) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetMixnodeDelegations { - mix_identity: mix_identity.to_owned(), - start_after, - limit: page_limit, - }; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - /// Gets list of all the mixnodes on which a particular address delegated. - pub async fn get_delegator_delegations_paged( - &self, - delegator: String, - start_after: Option, - page_limit: Option, - ) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetDelegatorDelegations { - delegator, - start_after, - limit: page_limit, - }; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - /// Checks value of delegation of given client towards particular mixnode. - pub async fn get_delegation_details( - &self, - mix_identity: IdentityKey, - delegator: &AccountId, - proxy: Option, - ) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetDelegationDetails { - mix_identity, - delegator: delegator.to_string(), - proxy, - }; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - pub async fn simulate(&self, messages: I) -> Result where C: SigningCosmWasmClient + Sync, @@ -1081,86 +708,14 @@ impl NymdClient { .await } - #[execute("mixnet")] - fn _compound_reward( - &self, - operator: Option, - delegator: Option, - mix_identity: Option, - proxy: Option, - fee: Option, - ) -> (ExecuteMsg, Option) - where - C: SigningCosmWasmClient + Sync, - { - ( - ExecuteMsg::CompoundReward { - operator, - delegator, - mix_identity, - proxy, - }, - fee, - ) - } - - #[execute("mixnet")] - fn _compound_operator_reward(&self, fee: Option) -> (ExecuteMsg, Option) - where - C: SigningCosmWasmClient + Sync, - { - (ExecuteMsg::CompoundOperatorReward {}, fee) - } - - #[execute("mixnet")] - fn _claim_operator_reward(&self, fee: Option) -> (ExecuteMsg, Option) - where - C: SigningCosmWasmClient + Sync, - { - (ExecuteMsg::ClaimOperatorReward {}, fee) - } - - #[execute("mixnet")] - fn _compound_delegator_reward( - &self, - mix_identity: IdentityKey, - fee: Option, - ) -> (ExecuteMsg, Option) - where - C: SigningCosmWasmClient + Sync, - { - (ExecuteMsg::CompoundDelegatorReward { mix_identity }, fee) - } - - #[execute("mixnet")] - fn _claim_delegator_reward( - &self, - mix_identity: IdentityKey, - fee: Option, - ) -> (ExecuteMsg, Option) - where - C: SigningCosmWasmClient + Sync, - { - (ExecuteMsg::ClaimDelegatorReward { mix_identity }, fee) - } + // @DU: I don't want to touch them now, but for consistency sake those should be moved to + // `VestingSigningClient` #[execute("vesting")] - fn _vesting_compound_delegator_reward( + fn _vesting_withdraw_operator_reward( &self, - mix_identity: IdentityKey, fee: Option, ) -> (VestingExecuteMsg, Option) - where - C: SigningCosmWasmClient + Sync, - { - ( - VestingExecuteMsg::CompoundDelegatorReward { mix_identity }, - fee, - ) - } - - #[execute("vesting")] - fn _vesting_claim_operator_reward(&self, fee: Option) -> (VestingExecuteMsg, Option) where C: SigningCosmWasmClient + Sync, { @@ -1168,29 +723,15 @@ impl NymdClient { } #[execute("vesting")] - fn _vesting_compound_operator_reward( + fn _vesting_withdraw_delegator_reward( &self, + mix_id: NodeId, fee: Option, ) -> (VestingExecuteMsg, Option) where C: SigningCosmWasmClient + Sync, { - (VestingExecuteMsg::CompoundOperatorReward {}, fee) - } - - #[execute("vesting")] - fn _vesting_claim_delegator_reward( - &self, - mix_identity: IdentityKey, - fee: Option, - ) -> (VestingExecuteMsg, Option) - where - C: SigningCosmWasmClient + Sync, - { - ( - VestingExecuteMsg::ClaimDelegatorReward { mix_identity }, - fee, - ) + (VestingExecuteMsg::ClaimDelegatorReward { mix_id }, fee) } #[execute("vesting")] @@ -1204,564 +745,4 @@ impl NymdClient { { (VestingExecuteMsg::UpdateLockedPledgeCap { amount }, fee) } - - /// Announce a mixnode, paying a fee. - pub async fn bond_mixnode( - &self, - mixnode: MixNode, - owner_signature: String, - pledge: Coin, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::BondMixnode { - mix_node: mixnode, - owner_signature, - }; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Bonding mixnode from rust!", - vec![pledge], - ) - .await - } - - /// Announce a mixnode on behalf of the owner, paying a fee. - pub async fn bond_mixnode_on_behalf( - &self, - mixnode: MixNode, - owner: String, - owner_signature: String, - pledge: Coin, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::BondMixnodeOnBehalf { - mix_node: mixnode, - owner, - owner_signature, - }; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Bonding mixnode on behalf from rust!", - vec![pledge], - ) - .await - } - - /// Announce multiple mixnodes on behalf of other owners, paying a fee. - pub async fn bond_multiple_mixnodes_on_behalf( - &self, - mixnode_bonds_with_sigs: Vec<(MixNodeBond, String)>, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let reqs: Vec<(ExecuteMsg, Vec)> = mixnode_bonds_with_sigs - .into_iter() - .map(|(bond, owner_signature)| { - ( - ExecuteMsg::BondMixnodeOnBehalf { - mix_node: bond.mix_node, - owner: bond.owner.to_string(), - owner_signature, - }, - vec![bond.pledge_amount.into()], - ) - }) - .collect(); - - self.client - .execute_multiple( - self.address(), - self.mixnet_contract_address(), - reqs, - fee, - "Bonding multiple mixnodes on behalf from rust!", - ) - .await - } - - /// Unbond a mixnode, removing it from the network and reclaiming staked coins - pub async fn unbond_mixnode(&self, fee: Option) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::UnbondMixnode {}; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Unbonding mixnode from rust!", - vec![], - ) - .await - } - - /// Unbond a mixnode on behalf of the owner, removing it from the network and reclaiming staked coins - pub async fn unbond_mixnode_on_behalf( - &self, - owner: String, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::UnbondMixnodeOnBehalf { owner }; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Unbonding mixnode on behalf from rust!", - vec![], - ) - .await - } - - /// Update the configuration of a mixnode. Right now, only possible for profit margin. - pub async fn update_mixnode_config( - &self, - profit_margin_percent: u8, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::UpdateMixnodeConfig { - profit_margin_percent, - }; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Updating mixnode configuration from rust!", - vec![], - ) - .await - } - - /// Delegates specified amount of stake to particular mixnode. - pub async fn delegate_to_mixnode( - &self, - mix_identity: &str, - amount: Coin, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::DelegateToMixnode { - mix_identity: mix_identity.to_string(), - }; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Delegating to mixnode from rust!", - vec![amount], - ) - .await - } - - /// Delegates specified amount of stake to particular mixnode on - /// behalf of a particular delegator. - pub async fn delegate_to_mixnode_on_behalf( - &self, - mix_identity: &str, - delegate: &str, - amount: Coin, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::DelegateToMixnodeOnBehalf { - mix_identity: mix_identity.to_string(), - delegate: delegate.to_string(), - }; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Delegating to mixnode on behalf from rust!", - vec![amount], - ) - .await - } - - /// Delegates specified amount of stake to multiple mixnodes on behalf of multiple delegators. - pub async fn delegate_to_multiple_mixnodes_on_behalf( - &self, - mixnode_delegations: Vec, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let reqs: Vec<(ExecuteMsg, Vec)> = mixnode_delegations - .into_iter() - .map(|delegation| { - ( - ExecuteMsg::DelegateToMixnodeOnBehalf { - mix_identity: delegation.node_identity(), - delegate: delegation.owner().to_string(), - }, - vec![delegation.amount().clone().into()], - ) - }) - .collect(); - - self.client - .execute_multiple( - self.address(), - self.mixnet_contract_address(), - reqs, - fee, - "Delegating to multiple mixnodes on behalf from rust!", - ) - .await - } - - /// Removes stake delegation from a particular mixnode. - pub async fn remove_mixnode_delegation( - &self, - mix_identity: &str, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::UndelegateFromMixnode { - mix_identity: mix_identity.to_string(), - }; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Removing mixnode delegation from rust!", - vec![], - ) - .await - } - - /// Removes stake delegation from a particular mixnode on behalf of a particular delegator. - pub async fn remove_mixnode_delegation_on_behalf( - &self, - mix_identity: &str, - delegate: &str, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::UndelegateFromMixnodeOnBehalf { - mix_identity: mix_identity.to_string(), - delegate: delegate.to_string(), - }; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Removing mixnode delegation on behalf from rust!", - vec![], - ) - .await - } - - /// Announce a gateway, paying a fee. - pub async fn bond_gateway( - &self, - gateway: Gateway, - owner_signature: String, - pledge: Coin, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::BondGateway { - gateway, - owner_signature, - }; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Bonding gateway from rust!", - vec![pledge], - ) - .await - } - - /// Announce a gateway on behalf of the owner, paying a fee. - pub async fn bond_gateway_on_behalf( - &self, - gateway: Gateway, - owner: String, - owner_signature: String, - pledge: Coin, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::BondGatewayOnBehalf { - gateway, - owner, - owner_signature, - }; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Bonding gateway on behalf from rust!", - vec![pledge], - ) - .await - } - - /// Announce multiple gateways on behalf of other owners, paying a fee. - pub async fn bond_multiple_gateways_on_behalf( - &self, - gateway_bonds_with_sigs: Vec<(GatewayBond, String)>, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let reqs: Vec<(ExecuteMsg, Vec)> = gateway_bonds_with_sigs - .into_iter() - .map(|(bond, owner_signature)| { - ( - ExecuteMsg::BondGatewayOnBehalf { - gateway: bond.gateway, - owner: bond.owner.to_string(), - owner_signature, - }, - vec![bond.pledge_amount.into()], - ) - }) - .collect(); - - self.client - .execute_multiple( - self.address(), - self.mixnet_contract_address(), - reqs, - fee, - "Bonding multiple gateways on behalf from rust!", - ) - .await - } - - /// Unbond a gateway, removing it from the network and reclaiming staked coins - pub async fn unbond_gateway(&self, fee: Option) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::UnbondGateway {}; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Unbonding gateway from rust!", - vec![], - ) - .await - } - - /// Unbond a gateway on behalf of the owner, removing it from the - /// network and reclaiming staked coins - pub async fn unbond_gateway_on_behalf( - &self, - owner: String, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::UnbondGatewayOnBehalf { owner }; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Unbonding gateway on behalf from rust!", - vec![], - ) - .await - } - - pub async fn update_contract_settings( - &self, - new_params: ContractStateParams, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::UpdateContractStateParams(new_params); - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Updating contract state from rust!", - vec![], - ) - .await - } - - pub async fn advance_current_epoch(&self, fee: Option) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::AdvanceCurrentEpoch {}; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Advance current epoch", - vec![], - ) - .await - } - - pub async fn reconcile_delegations(&self, fee: Option) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::ReconcileDelegations {}; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Reconciling delegation events", - vec![], - ) - .await - } - - pub async fn checkpoint_mixnodes(&self, fee: Option) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::CheckpointMixnodes {}; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Snapshotting mixnodes", - vec![], - ) - .await - } - - pub async fn write_rewarded_set( - &self, - rewarded_set: Vec, - expected_active_set_size: u32, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::WriteRewardedSet { - rewarded_set, - expected_active_set_size, - }; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Writing rewarded set", - vec![], - ) - .await - } } diff --git a/common/client-libs/validator-client/src/nymd/traits/mixnet_query_client.rs b/common/client-libs/validator-client/src/nymd/traits/mixnet_query_client.rs new file mode 100644 index 0000000000..4622f2d638 --- /dev/null +++ b/common/client-libs/validator-client/src/nymd/traits/mixnet_query_client.rs @@ -0,0 +1,387 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub use crate::nymd::cosmwasm_client::client::CosmWasmClient; +use crate::nymd::error::NymdError; +use crate::nymd::NymdClient; +use async_trait::async_trait; +use cosmrs::AccountId; +use mixnet_contract_common::delegation::{MixNodeDelegationResponse, OwnerProxySubKey}; +use mixnet_contract_common::mixnode::{ + MixNodeDetails, MixnodeRewardingDetailsResponse, PagedMixnodesDetailsResponse, + PagedUnbondedMixnodesResponse, StakeSaturationResponse, UnbondedMixnodeResponse, +}; +use mixnet_contract_common::reward_params::{Performance, RewardingParams}; +use mixnet_contract_common::rewarding::{ + EstimatedCurrentEpochRewardResponse, PendingRewardResponse, +}; +use mixnet_contract_common::{ + delegation, ContractBuildInformation, ContractState, ContractStateParams, + CurrentIntervalResponse, EpochEventId, GatewayBondResponse, GatewayOwnershipResponse, + IdentityKey, IntervalEventId, LayerDistribution, MixOwnershipResponse, MixnodeDetailsResponse, + NodeId, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, PagedGatewayResponse, + PagedMixNodeDelegationsResponse, PagedMixnodeBondsResponse, PagedRewardedSetResponse, + PendingEpochEventsResponse, PendingIntervalEventsResponse, QueryMsg as MixnetQueryMsg, +}; +use serde::Deserialize; + +#[async_trait] +pub trait MixnetQueryClient { + async fn query_mixnet_contract(&self, query: MixnetQueryMsg) -> Result + where + for<'a> T: Deserialize<'a>; + + // state/sys-params-related + + async fn get_mixnet_contract_version(&self) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetContractVersion {}) + .await + } + + async fn get_rewarding_validator_address(&self) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetRewardingValidatorAddress {}) + .await + } + + async fn get_mixnet_contract_settings(&self) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetStateParams {}) + .await + } + + async fn get_mixnet_contract_state(&self) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetState {}) + .await + } + + async fn get_rewarding_parameters(&self) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetRewardingParams {}) + .await + } + + async fn get_current_interval_details(&self) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetCurrentIntervalDetails {}) + .await + } + + async fn get_rewarded_set_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetRewardedSet { limit, start_after }) + .await + } + + // mixnode-related: + + async fn get_mixnode_bonds_paged( + &self, + limit: Option, + start_after: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetMixNodeBonds { limit, start_after }) + .await + } + + async fn get_mixnodes_detailed_paged( + &self, + limit: Option, + start_after: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetMixNodesDetailed { limit, start_after }) + .await + } + + async fn get_unbonded_paged( + &self, + limit: Option, + start_after: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedMixNodes { limit, start_after }) + .await + } + + async fn get_unbonded_by_owner_paged( + &self, + owner: &AccountId, + limit: Option, + start_after: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedMixNodesByOwner { + owner: owner.to_string(), + limit, + start_after, + }) + .await + } + + async fn get_unbonded_by_identity_paged( + &self, + identity_key: String, + limit: Option, + start_after: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedMixNodesByIdentityKey { + identity_key, + limit, + start_after, + }) + .await + } + + async fn get_owned_mixnode( + &self, + address: &AccountId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetOwnedMixnode { + address: address.to_string(), + }) + .await + } + + async fn get_mixnode_details( + &self, + mix_id: NodeId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetMixnodeDetails { mix_id }) + .await + } + + async fn get_mixnode_rewarding_details( + &self, + mix_id: NodeId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetMixnodeRewardingDetails { mix_id }) + .await + } + + async fn get_mixnode_stake_saturation( + &self, + mix_id: NodeId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetStakeSaturation { mix_id }) + .await + } + + async fn get_unbonded_mixnode_information( + &self, + mix_id: NodeId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedMixNodeInformation { mix_id }) + .await + } + + async fn get_layer_distribution(&self) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetLayerDistribution {}) + .await + } + + // gateway-related: + + async fn get_gateways_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetGateways { start_after, limit }) + .await + } + + /// Checks whether there is a bonded gateway associated with the provided identity key + async fn get_gateway_bond( + &self, + identity: IdentityKey, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetGatewayBond { identity }) + .await + } + + /// Checks whether there is a bonded gateway associated with the provided client's address + async fn get_owned_gateway( + &self, + address: &AccountId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetOwnedGateway { + address: address.to_string(), + }) + .await + } + + // delegation-related: + + /// Gets list of all delegations towards particular mixnode on particular page. + async fn get_mixnode_delegations_paged( + &self, + mix_id: NodeId, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetMixnodeDelegations { + mix_id, + start_after, + limit, + }) + .await + } + + /// Gets list of all the mixnodes to which a particular address delegated. + async fn get_delegator_delegations_paged( + &self, + delegator: String, + start_after: Option<(NodeId, OwnerProxySubKey)>, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetDelegatorDelegations { + delegator, + start_after, + limit, + }) + .await + } + + /// Checks value of delegation of given client towards particular mixnode. + async fn get_delegation_details( + &self, + mix_id: NodeId, + delegator: &AccountId, + proxy: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetDelegationDetails { + mix_id, + delegator: delegator.to_string(), + proxy, + }) + .await + } + + /// Gets all the delegations on the entire network + async fn get_all_network_delegations_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetAllDelegations { start_after, limit }) + .await + } + + // rewards related + async fn get_pending_operator_reward( + &self, + operator: &AccountId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetPendingOperatorReward { + address: operator.to_string(), + }) + .await + } + + async fn get_pending_mixnode_operator_reward( + &self, + mix_id: NodeId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetPendingMixNodeOperatorReward { mix_id }) + .await + } + + async fn get_pending_delegator_reward( + &self, + delegator: &AccountId, + mix_id: NodeId, + proxy: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetPendingDelegatorReward { + address: delegator.to_string(), + mix_id, + proxy, + }) + .await + } + + // given the provided performance, estimate the reward at the end of the current epoch + async fn get_estimated_current_epoch_operator_reward( + &self, + mix_id: NodeId, + estimated_performance: Performance, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetEstimatedCurrentEpochOperatorReward { + mix_id, + estimated_performance, + }) + .await + } + + // given the provided performance, estimate the reward at the end of the current epoch + async fn get_estimated_current_epoch_delegator_reward( + &self, + delegator: &AccountId, + mix_id: NodeId, + proxy: Option, + estimated_performance: Performance, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetEstimatedCurrentEpochDelegatorReward { + address: delegator.to_string(), + mix_id, + proxy, + estimated_performance, + }) + .await + } + + // interval-related + + async fn get_pending_epoch_events_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetPendingEpochEvents { start_after, limit }) + .await + } + + async fn get_pending_interval_events_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetPendingIntervalEvents { start_after, limit }) + .await + } + + async fn get_mixnode_details_by_identity( + &self, + mix_identity: IdentityKey, + ) -> Result, NymdError> { + self.query_mixnet_contract(MixnetQueryMsg::GetBondedMixnodeDetailsByIdentity { + mix_identity, + }) + .await + } +} + +#[async_trait] +impl MixnetQueryClient for NymdClient +where + C: CosmWasmClient + Sync + Send, +{ + async fn query_mixnet_contract(&self, query: MixnetQueryMsg) -> Result + where + for<'a> T: Deserialize<'a>, + { + self.client + .query_contract_smart(self.mixnet_contract_address(), &query) + .await + } +} + +#[async_trait] +impl MixnetQueryClient for crate::Client +where + C: CosmWasmClient + Sync + Send, +{ + async fn query_mixnet_contract(&self, query: MixnetQueryMsg) -> Result + where + for<'a> T: Deserialize<'a>, + { + self.nymd.query_mixnet_contract(query).await + } +} diff --git a/common/client-libs/validator-client/src/nymd/traits/mixnet_signing_client.rs b/common/client-libs/validator-client/src/nymd/traits/mixnet_signing_client.rs new file mode 100644 index 0000000000..c1e8c8cce6 --- /dev/null +++ b/common/client-libs/validator-client/src/nymd/traits/mixnet_signing_client.rs @@ -0,0 +1,496 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::nymd::coin::Coin; +pub use crate::nymd::cosmwasm_client::client::CosmWasmClient; +use crate::nymd::cosmwasm_client::types::ExecuteResult; +use crate::nymd::error::NymdError; +use crate::nymd::{Fee, NymdClient, SigningCosmWasmClient}; +use async_trait::async_trait; +use cosmrs::AccountId; +use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; +use mixnet_contract_common::reward_params::{IntervalRewardingParamsUpdate, Performance}; +use mixnet_contract_common::{ + ContractStateParams, ExecuteMsg as MixnetExecuteMsg, Gateway, MixNode, NodeId, +}; + +#[async_trait] +pub trait MixnetSigningClient { + async fn execute_mixnet_contract( + &self, + fee: Option, + msg: MixnetExecuteMsg, + funds: Vec, + ) -> Result; + + // state/sys-params-related + + async fn update_rewarding_validator_address( + &self, + address: AccountId, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UpdateRewardingValidatorAddress { + address: address.to_string(), + }, + vec![], + ) + .await + } + + async fn update_contract_state_params( + &self, + updated_parameters: ContractStateParams, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UpdateContractStateParams { updated_parameters }, + vec![], + ) + .await + } + + async fn update_active_set_size( + &self, + active_set_size: u32, + force_immediately: bool, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UpdateActiveSetSize { + active_set_size, + force_immediately, + }, + vec![], + ) + .await + } + + async fn update_rewarding_parameters( + &self, + updated_params: IntervalRewardingParamsUpdate, + force_immediately: bool, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UpdateRewardingParams { + updated_params, + force_immediately, + }, + vec![], + ) + .await + } + + async fn update_interval_config( + &self, + epochs_in_interval: u32, + epoch_duration_secs: u64, + force_immediately: bool, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UpdateIntervalConfig { + epochs_in_interval, + epoch_duration_secs, + force_immediately, + }, + vec![], + ) + .await + } + + async fn advance_current_epoch( + &self, + new_rewarded_set: Vec, + expected_active_set_size: u32, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::AdvanceCurrentEpoch { + new_rewarded_set, + expected_active_set_size, + }, + vec![], + ) + .await + } + + async fn reconcile_epoch_events( + &self, + limit: Option, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::ReconcileEpochEvents { limit }, + vec![], + ) + .await + } + + // mixnode-related: + + async fn bond_mixnode( + &self, + mix_node: MixNode, + cost_params: MixNodeCostParams, + owner_signature: String, + pledge: Coin, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::BondMixnode { + mix_node, + cost_params, + owner_signature, + }, + vec![pledge], + ) + .await + } + + async fn bond_mixnode_on_behalf( + &self, + owner: AccountId, + mix_node: MixNode, + cost_params: MixNodeCostParams, + owner_signature: String, + pledge: Coin, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::BondMixnodeOnBehalf { + mix_node, + cost_params, + owner_signature, + owner: owner.to_string(), + }, + vec![pledge], + ) + .await + } + + async fn unbond_mixnode(&self, fee: Option) -> Result { + self.execute_mixnet_contract(fee, MixnetExecuteMsg::UnbondMixnode {}, vec![]) + .await + } + + async fn unbond_mixnode_on_behalf( + &self, + owner: AccountId, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UnbondMixnodeOnBehalf { + owner: owner.to_string(), + }, + vec![], + ) + .await + } + + async fn update_mixnode_cost_params( + &self, + new_costs: MixNodeCostParams, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UpdateMixnodeCostParams { new_costs }, + vec![], + ) + .await + } + + async fn update_mixnode_cost_params_on_behalf( + &self, + owner: AccountId, + new_costs: MixNodeCostParams, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UpdateMixnodeCostParamsOnBehalf { + new_costs, + owner: owner.to_string(), + }, + vec![], + ) + .await + } + + async fn update_mixnode_config( + &self, + new_config: MixNodeConfigUpdate, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UpdateMixnodeConfig { new_config }, + vec![], + ) + .await + } + + async fn update_mixnode_config_on_behalf( + &self, + owner: AccountId, + new_config: MixNodeConfigUpdate, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UpdateMixnodeConfigOnBehalf { + new_config, + owner: owner.to_string(), + }, + vec![], + ) + .await + } + + // gateway-related: + + async fn bond_gateway( + &self, + gateway: Gateway, + owner_signature: String, + pledge: Coin, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::BondGateway { + gateway, + owner_signature, + }, + vec![pledge], + ) + .await + } + + async fn bond_gateway_on_behalf( + &self, + owner: AccountId, + gateway: Gateway, + owner_signature: String, + pledge: Coin, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::BondGatewayOnBehalf { + gateway, + owner_signature, + owner: owner.to_string(), + }, + vec![pledge], + ) + .await + } + + async fn unbond_gateway(&self, fee: Option) -> Result { + self.execute_mixnet_contract(fee, MixnetExecuteMsg::UnbondGateway {}, vec![]) + .await + } + + async fn unbond_gateway_on_behalf( + &self, + owner: AccountId, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UnbondGatewayOnBehalf { + owner: owner.to_string(), + }, + vec![], + ) + .await + } + + // delegation-related: + + async fn delegate_to_mixnode( + &self, + mix_id: NodeId, + amount: Coin, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::DelegateToMixnode { mix_id }, + vec![amount], + ) + .await + } + + async fn delegate_to_mixnode_on_behalf( + &self, + delegate: AccountId, + mix_id: NodeId, + amount: Coin, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::DelegateToMixnodeOnBehalf { + mix_id, + delegate: delegate.to_string(), + }, + vec![amount], + ) + .await + } + + async fn undelegate_from_mixnode( + &self, + mix_id: NodeId, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UndelegateFromMixnode { mix_id }, + vec![], + ) + .await + } + + async fn undelegate_to_mixnode_on_behalf( + &self, + delegate: AccountId, + mix_id: NodeId, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UndelegateFromMixnodeOnBehalf { + mix_id, + delegate: delegate.to_string(), + }, + vec![], + ) + .await + } + + // reward-related + + async fn reward_mixnode( + &self, + mix_id: NodeId, + performance: Performance, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::RewardMixnode { + mix_id, + performance, + }, + vec![], + ) + .await + } + + async fn withdraw_operator_reward(&self, fee: Option) -> Result { + self.execute_mixnet_contract(fee, MixnetExecuteMsg::WithdrawOperatorReward {}, vec![]) + .await + } + + async fn withdraw_operator_reward_on_behalf( + &self, + owner: AccountId, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::WithdrawOperatorRewardOnBehalf { + owner: owner.to_string(), + }, + vec![], + ) + .await + } + + async fn withdraw_delegator_reward( + &self, + mix_id: NodeId, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::WithdrawDelegatorReward { mix_id }, + vec![], + ) + .await + } + + async fn withdraw_delegator_reward_on_behalf( + &self, + owner: AccountId, + mix_id: NodeId, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::WithdrawDelegatorRewardOnBehalf { + mix_id, + owner: owner.to_string(), + }, + vec![], + ) + .await + } +} + +#[async_trait] +impl MixnetSigningClient for NymdClient +where + C: SigningCosmWasmClient + Sync + Send, +{ + async fn execute_mixnet_contract( + &self, + fee: Option, + msg: MixnetExecuteMsg, + funds: Vec, + ) -> Result { + let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); + let memo = msg.default_memo(); + self.client + .execute( + self.address(), + self.mixnet_contract_address(), + &msg, + fee, + memo, + funds, + ) + .await + } +} + +#[async_trait] +impl MixnetSigningClient for crate::Client +where + C: SigningCosmWasmClient + Sync + Send, +{ + async fn execute_mixnet_contract( + &self, + fee: Option, + msg: MixnetExecuteMsg, + funds: Vec, + ) -> Result { + self.nymd.execute_mixnet_contract(fee, msg, funds).await + } +} diff --git a/common/client-libs/validator-client/src/nymd/traits/mod.rs b/common/client-libs/validator-client/src/nymd/traits/mod.rs index f1cda7066b..61a562a5b3 100644 --- a/common/client-libs/validator-client/src/nymd/traits/mod.rs +++ b/common/client-libs/validator-client/src/nymd/traits/mod.rs @@ -1,8 +1,10 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 mod coconut_bandwidth_query_client; mod coconut_bandwidth_signing_client; +mod mixnet_query_client; +mod mixnet_signing_client; mod multisig_query_client; mod multisig_signing_client; mod vesting_query_client; @@ -10,6 +12,8 @@ mod vesting_signing_client; pub use coconut_bandwidth_query_client::CoconutBandwidthQueryClient; pub use coconut_bandwidth_signing_client::CoconutBandwidthSigningClient; +pub use mixnet_query_client::MixnetQueryClient; +pub use mixnet_signing_client::MixnetSigningClient; pub use multisig_query_client::MultisigQueryClient; pub use multisig_signing_client::MultisigSigningClient; pub use vesting_query_client::VestingQueryClient; diff --git a/common/client-libs/validator-client/src/nymd/traits/vesting_query_client.rs b/common/client-libs/validator-client/src/nymd/traits/vesting_query_client.rs index f23bf357eb..66ceaaa41b 100644 --- a/common/client-libs/validator-client/src/nymd/traits/vesting_query_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/vesting_query_client.rs @@ -7,7 +7,7 @@ use crate::nymd::error::NymdError; use crate::nymd::NymdClient; use async_trait::async_trait; use cosmwasm_std::{Coin as CosmWasmCoin, Timestamp}; -use mixnet_contract_common::IdentityKey; +use mixnet_contract_common::NodeId; use vesting_contract::vesting::Account; use vesting_contract_common::{ messages::QueryMsg as VestingQueryMsg, AllDelegationsResponse, DelegationTimesResponse, @@ -76,12 +76,12 @@ pub trait VestingQueryClient { async fn get_delegation_timestamps( &self, address: &str, - mix_identity: String, + mix_id: NodeId, ) -> Result; async fn get_all_vesting_delegations_paged( &self, - start_after: Option<(u32, IdentityKey, u64)>, + start_after: Option<(u32, NodeId, u64)>, limit: Option, ) -> Result; @@ -269,11 +269,11 @@ impl VestingQueryClient for NymdClient { async fn get_delegation_timestamps( &self, address: &str, - mix_identity: String, + mix_id: NodeId, ) -> Result { let request = VestingQueryMsg::GetDelegationTimes { address: address.to_string(), - mix_identity, + mix_id, }; self.client .query_contract_smart(self.vesting_contract_address(), &request) @@ -282,7 +282,7 @@ impl VestingQueryClient for NymdClient { async fn get_all_vesting_delegations_paged( &self, - start_after: Option<(u32, IdentityKey, u64)>, + start_after: Option<(u32, NodeId, u64)>, limit: Option, ) -> Result { let request = VestingQueryMsg::GetAllDelegations { start_after, limit }; diff --git a/common/client-libs/validator-client/src/nymd/traits/vesting_signing_client.rs b/common/client-libs/validator-client/src/nymd/traits/vesting_signing_client.rs index 957a02e3e1..7325b17bc8 100644 --- a/common/client-libs/validator-client/src/nymd/traits/vesting_signing_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/vesting_signing_client.rs @@ -6,14 +6,28 @@ use crate::nymd::cosmwasm_client::types::ExecuteResult; use crate::nymd::error::NymdError; use crate::nymd::{Coin, Fee, NymdClient}; use async_trait::async_trait; -use mixnet_contract_common::{Gateway, IdentityKey, IdentityKeyRef, MixNode}; +use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; +use mixnet_contract_common::{Gateway, MixNode, NodeId}; use vesting_contract_common::messages::{ExecuteMsg as VestingExecuteMsg, VestingSpecification}; #[async_trait] pub trait VestingSigningClient { + async fn execute_vesting_contract( + &self, + fee: Option, + msg: VestingExecuteMsg, + funds: Vec, + ) -> Result; + + async fn vesting_update_mixnode_cost_params( + &self, + new_costs: MixNodeCostParams, + fee: Option, + ) -> Result; + async fn vesting_update_mixnode_config( &self, - profix_margin_percent: u8, + new_config: MixNodeConfigUpdate, fee: Option, ) -> Result; @@ -43,10 +57,12 @@ pub trait VestingSigningClient { async fn vesting_bond_mixnode( &self, mix_node: MixNode, + cost_params: MixNodeCostParams, owner_signature: &str, pledge: Coin, fee: Option, ) -> Result; + async fn vesting_unbond_mixnode(&self, fee: Option) -> Result; async fn vesting_track_unbond_mixnode( @@ -65,21 +81,21 @@ pub trait VestingSigningClient { async fn vesting_track_undelegation( &self, address: &str, - mix_identity: IdentityKey, + mix_id: NodeId, amount: Coin, fee: Option, ) -> Result; - async fn vesting_delegate_to_mixnode<'a>( + async fn vesting_delegate_to_mixnode( &self, - mix_identity: IdentityKeyRef<'a>, + mix_id: NodeId, amount: Coin, fee: Option, ) -> Result; - async fn vesting_undelegate_from_mixnode<'a>( + async fn vesting_undelegate_from_mixnode( &self, - mix_identity: IdentityKeyRef<'a>, + mix_id: NodeId, fee: Option, ) -> Result; @@ -95,15 +111,46 @@ pub trait VestingSigningClient { #[async_trait] impl VestingSigningClient for NymdClient { + async fn execute_vesting_contract( + &self, + fee: Option, + msg: VestingExecuteMsg, + funds: Vec, + ) -> Result { + let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); + let memo = msg.name().to_string(); + self.client + .execute( + self.address(), + self.vesting_contract_address(), + &msg, + fee, + memo, + funds, + ) + .await + } + + async fn vesting_update_mixnode_cost_params( + &self, + new_costs: MixNodeCostParams, + fee: Option, + ) -> Result { + self.execute_vesting_contract( + fee, + VestingExecuteMsg::UpdateMixnodeCostParams { new_costs }, + vec![], + ) + .await + } + async fn vesting_update_mixnode_config( &self, - profit_margin_percent: u8, + new_config: MixNodeConfigUpdate, fee: Option, ) -> Result { let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - let req = VestingExecuteMsg::UpdateMixnodeConfig { - profit_margin_percent, - }; + let req = VestingExecuteMsg::UpdateMixnodeConfig { new_config }; self.client .execute( self.address(), @@ -203,26 +250,22 @@ impl VestingSigningClient for NymdClient async fn vesting_bond_mixnode( &self, mix_node: MixNode, + cost_params: MixNodeCostParams, owner_signature: &str, pledge: Coin, fee: Option, ) -> Result { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - let req = VestingExecuteMsg::BondMixnode { - mix_node, - owner_signature: owner_signature.to_string(), - amount: pledge.into(), - }; - self.client - .execute( - self.address(), - self.vesting_contract_address(), - &req, - fee, - "VestingContract::BondMixnode", - vec![], - ) - .await + self.execute_vesting_contract( + fee, + VestingExecuteMsg::BondMixnode { + mix_node, + cost_params, + owner_signature: owner_signature.to_string(), + amount: pledge.into(), + }, + vec![], + ) + .await } async fn vesting_unbond_mixnode(&self, fee: Option) -> Result { @@ -262,6 +305,7 @@ impl VestingSigningClient for NymdClient ) .await } + async fn withdraw_vested_coins( &self, amount: Coin, @@ -282,72 +326,54 @@ impl VestingSigningClient for NymdClient ) .await } + async fn vesting_track_undelegation( &self, address: &str, - mix_identity: IdentityKey, + mix_id: NodeId, amount: Coin, fee: Option, ) -> Result { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - let req = VestingExecuteMsg::TrackUndelegation { - owner: address.to_string(), - mix_identity, - amount: amount.into(), - }; - self.client - .execute( - self.address(), - self.vesting_contract_address(), - &req, - fee, - "VestingContract::TrackUndelegation", - vec![], - ) - .await - } - async fn vesting_delegate_to_mixnode<'a>( - &self, - mix_identity: IdentityKeyRef<'a>, - amount: Coin, - fee: Option, - ) -> Result { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - let req = VestingExecuteMsg::DelegateToMixnode { - mix_identity: mix_identity.into(), - amount: amount.into(), - }; - self.client - .execute( - self.address(), - self.vesting_contract_address(), - &req, - fee, - "VestingContract::DelegateToMixnode", - vec![], - ) - .await + self.execute_vesting_contract( + fee, + VestingExecuteMsg::TrackUndelegation { + owner: address.to_string(), + mix_id, + amount: amount.into(), + }, + vec![], + ) + .await } - async fn vesting_undelegate_from_mixnode<'a>( + async fn vesting_delegate_to_mixnode( &self, - mix_identity: IdentityKeyRef<'a>, + mix_id: NodeId, + amount: Coin, fee: Option, ) -> Result { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - let req = VestingExecuteMsg::UndelegateFromMixnode { - mix_identity: mix_identity.into(), - }; - self.client - .execute( - self.address(), - self.vesting_contract_address(), - &req, - fee, - "VestingContract::UndelegateFromMixnode", - vec![], - ) - .await + self.execute_vesting_contract( + fee, + VestingExecuteMsg::DelegateToMixnode { + mix_id, + amount: amount.into(), + }, + vec![], + ) + .await + } + + async fn vesting_undelegate_from_mixnode( + &self, + mix_id: NodeId, + fee: Option, + ) -> Result { + self.execute_vesting_contract( + fee, + VestingExecuteMsg::UndelegateFromMixnode { mix_id }, + vec![], + ) + .await } async fn create_periodic_vesting_account( diff --git a/common/client-libs/validator-client/src/nymd/wallet/mod.rs b/common/client-libs/validator-client/src/nymd/wallet/mod.rs index e071d48853..b22dfdd6f4 100644 --- a/common/client-libs/validator-client/src/nymd/wallet/mod.rs +++ b/common/client-libs/validator-client/src/nymd/wallet/mod.rs @@ -72,7 +72,7 @@ impl DirectSecp256k1HdWallet { } fn derive_keypair(&self, hd_path: &DerivationPath) -> Result { - let extended_private_key = XPrv::derive_from_path(&self.seed, hd_path)?; + let extended_private_key = XPrv::derive_from_path(self.seed, hd_path)?; let private_key: SigningKey = extended_private_key.into(); let public_key = private_key.public_key(); diff --git a/common/client-libs/validator-client/src/validator_api/mod.rs b/common/client-libs/validator-client/src/validator_api/mod.rs index 1fa3a50862..2dcb50c62f 100644 --- a/common/client-libs/validator-client/src/validator_api/mod.rs +++ b/common/client-libs/validator-client/src/validator_api/mod.rs @@ -3,17 +3,18 @@ use crate::validator_api::error::ValidatorAPIError; use crate::validator_api::routes::{CORE_STATUS_COUNT, SINCE_ARG}; -use mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixNodeBond}; +use mixnet_contract_common::mixnode::MixNodeDetails; +use mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId}; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; use url::Url; use validator_api_requests::coconut::{ BlindSignRequestBody, BlindedSignatureResponse, CosmosAddressResponse, VerificationKeyResponse, VerifyCredentialBody, VerifyCredentialResponse, }; use validator_api_requests::models::{ - CoreNodeStatusResponse, InclusionProbabilityResponse, MixNodeBondAnnotated, - MixnodeStatusResponse, RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, + GatewayCoreStatusResponse, InclusionProbabilityResponse, MixNodeBondAnnotated, + MixnodeCoreStatusResponse, MixnodeStatusResponse, RewardEstimationResponse, + StakeSaturationResponse, UptimeResponse, }; pub mod error; @@ -57,6 +58,7 @@ impl Client { V: AsRef, { let url = create_api_url(&self.url, path, params); + log::trace!("url: {:?}", url.as_str()); Ok(self.reqwest_client.get(url).send().await?.json().await?) } @@ -83,7 +85,7 @@ impl Client { } } - pub async fn get_mixnodes(&self) -> Result, ValidatorAPIError> { + pub async fn get_mixnodes(&self) -> Result, ValidatorAPIError> { self.query_validator_api(&[routes::API_VERSION, routes::MIXNODES], NO_PARAMS) .await } @@ -103,7 +105,7 @@ impl Client { .await } - pub async fn get_active_mixnodes(&self) -> Result, ValidatorAPIError> { + pub async fn get_active_mixnodes(&self) -> Result, ValidatorAPIError> { self.query_validator_api( &[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE], NO_PARAMS, @@ -126,7 +128,7 @@ impl Client { .await } - pub async fn get_rewarded_mixnodes(&self) -> Result, ValidatorAPIError> { + pub async fn get_rewarded_mixnodes(&self) -> Result, ValidatorAPIError> { self.query_validator_api( &[routes::API_VERSION, routes::MIXNODES, routes::REWARDED], NO_PARAMS, @@ -149,28 +151,11 @@ impl Client { .await } - pub async fn get_probs_mixnode_rewarded( - &self, - mixnode_id: &str, - ) -> Result, ValidatorAPIError> { - self.query_validator_api( - &[ - routes::API_VERSION, - routes::MIXNODES, - routes::REWARDED, - routes::INCLUSION_CHANCE, - mixnode_id, - ], - NO_PARAMS, - ) - .await - } - pub async fn get_gateway_core_status_count( &self, identity: IdentityKeyRef<'_>, since: Option, - ) -> Result { + ) -> Result { if let Some(since) = since { self.query_validator_api( &[ @@ -199,16 +184,16 @@ impl Client { pub async fn get_mixnode_core_status_count( &self, - identity: IdentityKeyRef<'_>, + mix_id: NodeId, since: Option, - ) -> Result { + ) -> Result { if let Some(since) = since { self.query_validator_api( &[ routes::API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, - identity, + &mix_id.to_string(), CORE_STATUS_COUNT, ], &[(SINCE_ARG, since.to_string())], @@ -220,7 +205,8 @@ impl Client { routes::API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, - identity, + &mix_id.to_string(), + CORE_STATUS_COUNT, ], NO_PARAMS, ) @@ -230,14 +216,14 @@ impl Client { pub async fn get_mixnode_status( &self, - identity: IdentityKeyRef<'_>, + mix_id: NodeId, ) -> Result { self.query_validator_api( &[ routes::API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, - identity, + &mix_id.to_string(), routes::STATUS, ], NO_PARAMS, @@ -247,14 +233,14 @@ impl Client { pub async fn get_mixnode_reward_estimation( &self, - identity: IdentityKeyRef<'_>, + mix_id: NodeId, ) -> Result { self.query_validator_api( &[ routes::API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, - identity, + &mix_id.to_string(), routes::REWARD_ESTIMATION, ], NO_PARAMS, @@ -264,14 +250,14 @@ impl Client { pub async fn get_mixnode_stake_saturation( &self, - identity: IdentityKeyRef<'_>, + mix_id: NodeId, ) -> Result { self.query_validator_api( &[ routes::API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, - identity, + &mix_id.to_string(), routes::STAKE_SATURATION, ], NO_PARAMS, @@ -281,14 +267,14 @@ impl Client { pub async fn get_mixnode_inclusion_probability( &self, - identity: IdentityKeyRef<'_>, + mix_id: NodeId, ) -> Result { self.query_validator_api( &[ routes::API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, - identity, + &mix_id.to_string(), routes::INCLUSION_CHANCE, ], NO_PARAMS, @@ -298,27 +284,14 @@ impl Client { pub async fn get_mixnode_avg_uptime( &self, - identity: IdentityKeyRef<'_>, + mix_id: NodeId, ) -> Result { self.query_validator_api( &[ routes::API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, - identity, - routes::AVG_UPTIME, - ], - NO_PARAMS, - ) - .await - } - - pub async fn get_mixnode_avg_uptimes(&self) -> Result, ValidatorAPIError> { - self.query_validator_api( - &[ - routes::API_VERSION, - routes::STATUS_ROUTES, - routes::MIXNODES, + &mix_id.to_string(), routes::AVG_UPTIME, ], NO_PARAMS, diff --git a/common/commands/src/utils.rs b/common/commands/src/utils.rs index d6419fa20b..2e042da300 100644 --- a/common/commands/src/utils.rs +++ b/common/commands/src/utils.rs @@ -28,6 +28,11 @@ pub fn pretty_cosmwasm_coin(coin: &CosmWasmCoin) -> String { format!("{} {}", amount, denom) } +pub fn pretty_decimal_with_denom(value: Decimal, denom: &str) -> String { + // TODO: we might have to truncate the value here (that's why I moved it to separate function) + format!("{} {}", value, denom) +} + pub fn show_error(e: E) where E: Display, diff --git a/common/commands/src/validator/account/balance.rs b/common/commands/src/validator/account/balance.rs index 8ac24b273f..eb8e2dfa8f 100644 --- a/common/commands/src/validator/account/balance.rs +++ b/common/commands/src/validator/account/balance.rs @@ -51,7 +51,7 @@ pub async fn query_balance( return; } - let denom = args.denom.unwrap_or_else(|| "".to_string()); + let denom = args.denom.unwrap_or_default(); for coin in coins { if denom.is_empty() || denom.eq_ignore_ascii_case(&coin.denom) { diff --git a/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs b/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs index d69d5e0028..c2bd86c7ee 100644 --- a/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs @@ -4,12 +4,16 @@ use crate::context::SigningClient; use clap::Parser; use log::info; -use mixnet_contract_common::Coin; +use mixnet_contract_common::{Coin, NodeId}; +use validator_client::nymd::traits::{MixnetQueryClient, MixnetSigningClient}; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub identity_key: String, + pub mix_id: Option, + + #[clap(long)] + pub identity_key: Option, #[clap(long)] pub amount: u128, @@ -20,10 +24,25 @@ pub async fn delegate_to_mixnode(args: Args, client: SigningClient) { info!("Starting delegation to mixnode"); + 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") + .expect("mixnode with the specified identity doesnt exist"); + node_details.mix_id() + } + }; + let coin = Coin::new(args.amount, denom); let res = client - .delegate_to_mixnode(&*args.identity_key, coin.into(), None) + .delegate_to_mixnode(mix_id, coin.into(), None) .await .expect("failed to delegate to mixnode!"); 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 efb685e085..cdf0ef4140 100644 --- a/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs +++ b/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs @@ -8,8 +8,8 @@ use crate::context::SigningClientWithValidatorAPI; use crate::utils::{pretty_cosmwasm_coin, show_error_passthrough}; use comfy_table::Table; -use mixnet_contract_common::mixnode::DelegationEvent; -use mixnet_contract_common::Delegation; +use cosmwasm_std::Addr; +use mixnet_contract_common::{Delegation, PendingEpochEvent, PendingEpochEventData}; #[derive(Debug, Parser)] pub struct Args {} @@ -26,19 +26,7 @@ pub async fn execute(_args: Args, client: SigningClientWithValidatorAPI) { .map_err(show_error_passthrough); let mixnet_contract_events = client - .nymd - .get_pending_delegation_events(client.nymd.address().to_string(), None) - .await - .map_err(show_error_passthrough); - - let vesting_contract = client.nymd.vesting_contract_address(); - - let vesting_contract_events = client - .nymd - .get_pending_delegation_events( - client.nymd.address().to_string(), - Some(vesting_contract.to_string()), - ) + .get_all_nymd_pending_epoch_events() .await .map_err(show_error_passthrough); @@ -58,13 +46,6 @@ pub async fn execute(_args: Args, client: SigningClientWithValidatorAPI) { print_delegation_events(res, &client).await; } } - if let Ok(res) = vesting_contract_events { - if !res.is_empty() { - println!(); - println!("Pending delegations (locked tokens):"); - print_delegation_events(res, &client).await; - } - } } async fn to_iso_timestamp(block_height: u32, client: &SigningClientWithValidatorAPI) -> String { @@ -77,14 +58,17 @@ async fn to_iso_timestamp(block_height: u32, client: &SigningClientWithValidator async fn print_delegations(delegations: Vec, client: &SigningClientWithValidatorAPI) { let mut table = Table::new(); - table.set_header(vec!["Timestamp", "Identity Key", "Delegation", "Proxy"]); + table.set_header(vec!["Timestamp", "Mix Id", "Delegation", "Proxy"]); for delegation in delegations { table.add_row(vec![ - to_iso_timestamp(delegation.block_height as u32, client).await, - delegation.node_identity.to_string(), + to_iso_timestamp(delegation.height as u32, client).await, + delegation.node_id.to_string(), pretty_cosmwasm_coin(&delegation.amount), - format!("{:?}", delegation.proxy), + delegation + .proxy + .map(Addr::into_string) + .unwrap_or_else(|| "-".into()), ]); } @@ -92,36 +76,53 @@ async fn print_delegations(delegations: Vec, client: &SigningClientW } async fn print_delegation_events( - events: Vec, + events: Vec, client: &SigningClientWithValidatorAPI, ) { let mut table = Table::new(); table.set_header(vec![ "Timestamp", - "Identity Key", + "Mix id", "Delegation", "Event Type", + "Proxy", ]); for event in events { - match event { - DelegationEvent::Delegate(delegation) => { - table.add_row(vec![ - to_iso_timestamp(delegation.block_height as u32, client).await, - delegation.node_identity.to_string(), - pretty_cosmwasm_coin(&delegation.amount), - "Delegate".to_string(), - ]); + match event.event { + PendingEpochEventData::Delegate { + owner, + mix_id, + amount, + proxy, + } => { + if owner.as_str() == client.nymd.address().as_ref() { + table.add_row(vec![ + "not-sure-if-applicable".into(), + mix_id.to_string(), + pretty_cosmwasm_coin(&amount), + "Delegate".to_string(), + proxy.map(Addr::into_string).unwrap_or_else(|| "-".into()), + ]); + } } - DelegationEvent::Undelegate(undelegate) => { - table.add_row(vec![ - to_iso_timestamp(undelegate.block_height() as u32, client).await, - undelegate.mix_identity().to_string(), - "-".to_string(), - "Undelegate".to_string(), - ]); + PendingEpochEventData::Undelegate { + owner, + mix_id, + proxy, + } => { + if owner.as_str() == client.nymd.address().as_ref() { + table.add_row(vec![ + "not-sure-if-applicable".into(), + mix_id.to_string(), + "-".to_string(), + "Undelegate".to_string(), + proxy.map(Addr::into_string).unwrap_or_else(|| "-".into()), + ]); + } } + _ => {} } } diff --git a/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs b/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs index 850ac076d6..ab4497b18d 100644 --- a/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs +++ b/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs @@ -4,18 +4,38 @@ use crate::context::SigningClient; use clap::Parser; use log::info; +use mixnet_contract_common::NodeId; +use validator_client::nymd::traits::{MixnetQueryClient, MixnetSigningClient}; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub identity_key: String, + pub mix_id: Option, + + #[clap(long)] + pub identity_key: Option, } pub async fn claim_delegator_reward(args: Args, client: SigningClient) { info!("Claim delegator reward"); + 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") + .expect("mixnode with the specified identity doesnt exist"); + node_details.mix_id() + } + }; + let res = client - .execute_claim_delegator_reward(args.identity_key, None) + .withdraw_delegator_reward(mix_id, None) .await .expect("failed to claim delegator-reward"); diff --git a/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs b/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs index 7e0e4d23ee..23f6d7ecc2 100644 --- a/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs +++ b/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs @@ -4,18 +4,38 @@ use crate::context::SigningClient; use clap::Parser; use log::info; +use mixnet_contract_common::NodeId; +use validator_client::nymd::traits::{MixnetQueryClient, MixnetSigningClient}; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub identity: String, + pub mix_id: Option, + + #[clap(long)] + pub identity_key: Option, } pub async fn vesting_claim_delegator_reward(args: Args, client: SigningClient) { info!("Claim vesting delegator reward"); + 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") + .expect("mixnode with the specified identity doesnt exist"); + node_details.mix_id() + } + }; + let res = client - .execute_vesting_claim_delegator_reward(args.identity, None) + .withdraw_delegator_reward_on_behalf(client.address().clone(), mix_id, None) .await .expect("failed to claim vesting delegator-reward"); diff --git a/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs b/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs index 8cd28d0629..f97e949d8c 100644 --- a/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs @@ -4,18 +4,38 @@ use crate::context::SigningClient; use clap::Parser; use log::info; +use mixnet_contract_common::NodeId; +use validator_client::nymd::traits::{MixnetQueryClient, MixnetSigningClient}; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub identity_key: String, + pub mix_id: Option, + + #[clap(long)] + pub identity_key: Option, } pub async fn undelegate_from_mixnode(args: Args, client: SigningClient) { info!("removing stake from mix-node"); + 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") + .expect("mixnode with the specified identity doesnt exist"); + node_details.mix_id() + } + }; + let res = client - .remove_mixnode_delegation(&*args.identity_key, None) + .undelegate_from_mixnode(mix_id, None) .await .expect("failed to remove stake from mixnode!"); diff --git a/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs b/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs index d87c334fa8..3c6ce441e3 100644 --- a/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs @@ -4,7 +4,8 @@ use clap::Parser; use log::info; -use mixnet_contract_common::Coin; +use mixnet_contract_common::{Coin, NodeId}; +use validator_client::nymd::traits::MixnetQueryClient; use validator_client::nymd::VestingSigningClient; use crate::context::SigningClient; @@ -12,7 +13,10 @@ use crate::context::SigningClient; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub identity_key: String, + pub mix_id: Option, + + #[clap(long)] + pub identity_key: Option, #[clap(long)] pub amount: u128, @@ -23,10 +27,25 @@ pub async fn vesting_delegate_to_mixnode(args: Args, client: SigningClient) { info!("Starting vesting delegation to mixnode"); + 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") + .expect("mixnode with the specified identity doesnt exist"); + node_details.mix_id() + } + }; + let coin = Coin::new(args.amount, denom); let res = client - .vesting_delegate_to_mixnode(&*args.identity_key, coin.into(), None) + .vesting_delegate_to_mixnode(mix_id, coin.into(), None) .await .expect("failed to delegate to mixnode!"); diff --git a/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs b/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs index 9dd802f1e0..1149639583 100644 --- a/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs @@ -3,7 +3,8 @@ use clap::Parser; use log::info; - +use mixnet_contract_common::NodeId; +use validator_client::nymd::traits::MixnetQueryClient; use validator_client::nymd::VestingSigningClient; use crate::context::SigningClient; @@ -11,14 +12,32 @@ use crate::context::SigningClient; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub identity_key: String, + pub mix_id: Option, + + #[clap(long)] + pub identity_key: Option, } pub async fn vesting_undelegate_from_mixnode(args: Args, client: SigningClient) { info!("removing stake from vesting mix-node"); + 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") + .expect("mixnode with the specified identity doesnt exist"); + node_details.mix_id() + } + }; + let res = client - .vesting_undelegate_from_mixnode(&*args.identity_key, None) + .vesting_undelegate_from_mixnode(mix_id, None) .await .expect("failed to remove stake from vesting account on mixnode!"); diff --git a/common/commands/src/validator/mixnet/operators/gateway/bond_gateway.rs b/common/commands/src/validator/mixnet/operators/gateway/bond_gateway.rs index 254ae650da..e9482dde34 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/bond_gateway.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/bond_gateway.rs @@ -6,6 +6,7 @@ use clap::Parser; use log::{info, warn}; use mixnet_contract_common::Coin; use network_defaults::{DEFAULT_CLIENT_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT}; +use validator_client::nymd::traits::MixnetSigningClient; #[derive(Debug, Parser)] pub struct Args { diff --git a/common/commands/src/validator/mixnet/operators/gateway/unbond_gateway.rs b/common/commands/src/validator/mixnet/operators/gateway/unbond_gateway.rs index 75a695dfa1..90cae1623c 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/unbond_gateway.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/unbond_gateway.rs @@ -4,6 +4,7 @@ use crate::context::SigningClient; use clap::Parser; use log::info; +use validator_client::nymd::traits::MixnetSigningClient; #[derive(Debug, Parser)] pub struct Args {} diff --git a/common/commands/src/validator/mixnet/operators/gateway/vesting_bond_gateway.rs b/common/commands/src/validator/mixnet/operators/gateway/vesting_bond_gateway.rs index d82a01c0c5..6c26b09eeb 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/vesting_bond_gateway.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/vesting_bond_gateway.rs @@ -68,7 +68,7 @@ pub async fn vesting_bond_gateway(client: SigningClient, args: Args, denom: &str let coin = Coin::new(args.amount, denom); let res = client - .vesting_bond_gateway(gateway, &*args.signature, coin.into(), None) + .vesting_bond_gateway(gateway, &args.signature, coin.into(), None) .await .expect("failed to bond gateway!"); diff --git a/common/commands/src/validator/mixnet/operators/gateway/vesting_unbond_gateway.rs b/common/commands/src/validator/mixnet/operators/gateway/vesting_unbond_gateway.rs index 563c4bdff6..0c0eae2fe6 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/vesting_unbond_gateway.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/vesting_unbond_gateway.rs @@ -4,6 +4,7 @@ use crate::context::SigningClient; use clap::Parser; use log::info; +use validator_client::nymd::traits::MixnetSigningClient; #[derive(Debug, Parser)] pub struct Args {} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs index 26d95fe93f..4d672a5322 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs @@ -2,12 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 use clap::Parser; +use cosmwasm_std::Uint128; use log::{info, warn}; -use mixnet_contract_common::Coin; +use mixnet_contract_common::{Coin, MixNodeCostParams, Percent}; use network_defaults::{ DEFAULT_HTTP_API_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT, }; +use validator_client::nymd::traits::MixnetSigningClient; +use validator_client::nymd::CosmWasmCoin; use crate::context::SigningClient; @@ -40,6 +43,12 @@ pub struct Args { #[clap(long)] pub profit_margin_percent: Option, + #[clap( + long, + help = "operating cost in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub interval_operating_cost: Option, + #[clap( long, help = "bonding amount in current DENOMINATION (so it would be 'unym', rather than 'nym')" @@ -71,13 +80,23 @@ pub async fn bond_mixnode(args: Args, client: SigningClient) { sphinx_key: args.sphinx_key, identity_key: args.identity_key, version: args.version, - profit_margin_percent: args.profit_margin_percent.unwrap_or(10), }; let coin = Coin::new(args.amount, denom); + let cost_params = MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value( + args.profit_margin_percent.unwrap_or(10) as u64, + ) + .unwrap(), + interval_operating_cost: CosmWasmCoin { + denom: denom.into(), + amount: Uint128::new(args.interval_operating_cost.unwrap_or(40_000_000)), + }, + }; + let res = client - .bond_mixnode(mixnode, args.signature, coin.into(), None) + .bond_mixnode(mixnode, cost_params, args.signature, coin.into(), None) .await .expect("failed to bond mixnode!"); diff --git a/common/commands/src/validator/mixnet/operators/mixnode/rewards/claim_operator_reward.rs b/common/commands/src/validator/mixnet/operators/mixnode/rewards/claim_operator_reward.rs index 2f3f8dd3c4..773f4cb3d7 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/rewards/claim_operator_reward.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/rewards/claim_operator_reward.rs @@ -4,6 +4,7 @@ use crate::context::SigningClient; use clap::Parser; use log::info; +use validator_client::nymd::traits::MixnetSigningClient; #[derive(Debug, Parser)] pub struct Args {} @@ -12,7 +13,7 @@ pub async fn claim_operator_reward(_args: Args, client: SigningClient) { info!("Claim operator reward"); let res = client - .execute_claim_operator_reward(None) + .withdraw_operator_reward(None) .await .expect("failed to claim operator reward"); diff --git a/common/commands/src/validator/mixnet/operators/mixnode/rewards/vesting_claim_operator_reward.rs b/common/commands/src/validator/mixnet/operators/mixnode/rewards/vesting_claim_operator_reward.rs index ffcd4670e1..d4895e83f2 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/rewards/vesting_claim_operator_reward.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/rewards/vesting_claim_operator_reward.rs @@ -4,6 +4,7 @@ use crate::context::SigningClient; use clap::Parser; use log::info; +use validator_client::nymd::traits::MixnetSigningClient; #[derive(Debug, Parser)] pub struct Args { @@ -15,7 +16,7 @@ pub async fn vesting_claim_operator_reward(client: SigningClient) { info!("Claim vesting operator reward"); let res = client - .execute_vesting_claim_operator_reward(None) + .withdraw_operator_reward_on_behalf(client.address().clone(), None) .await .expect("failed to claim vesting operator reward"); diff --git a/common/commands/src/validator/mixnet/operators/mixnode/settings/mod.rs b/common/commands/src/validator/mixnet/operators/mixnode/settings/mod.rs index 10bbd6ba8b..9d0c018cf9 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/settings/mod.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/settings/mod.rs @@ -3,8 +3,8 @@ use clap::{Args, Subcommand}; -pub mod update_profit_percent; -pub mod vesting_update_profit_percent; +pub mod update_config; +pub mod vesting_update_config; #[derive(Debug, Args)] #[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] @@ -15,8 +15,12 @@ pub struct MixnetOperatorsMixnodeSettings { #[derive(Debug, Subcommand)] pub enum MixnetOperatorsMixnodeSettingsCommands { - /// Update profit percentage - UpdateProfitPercentage(update_profit_percent::Args), - /// Update profit percentage for a mixnode bonded with locked tokens - VestingUpdateProfitPercentage(vesting_update_profit_percent::Args), + /// Update mixnode configuration + UpdateConfig(update_config::Args), + /// Update mixnode configuration for a mixnode bonded with locked tokens + VestingUpdateConfig(vesting_update_config::Args), + /// Update mixnode cost parameters + UpdateCostParameters, + /// Update mixnode cost parameters for a mixnode bonded with locked tokens + VestingUpdateCostParameters, } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/settings/update_config.rs b/common/commands/src/validator/mixnet/operators/mixnode/settings/update_config.rs new file mode 100644 index 0000000000..84cf9f2f4e --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/settings/update_config.rs @@ -0,0 +1,68 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use mixnet_contract_common::MixNodeConfigUpdate; +use validator_client::nymd::traits::{MixnetQueryClient, MixnetSigningClient}; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub host: Option, + + #[clap(long)] + pub mix_port: Option, + + #[clap(long)] + pub verloc_port: Option, + + #[clap(long)] + pub http_api_port: Option, + + #[clap(long)] + pub version: Option, +} + +pub async fn update_config(args: Args, client: SigningClient) { + info!("Update mix node config!"); + + let current_details = match client + .get_owned_mixnode(client.address()) + .await + .expect("failed to query the chain for mixnode details") + .mixnode_details + { + Some(details) => details, + None => { + log::warn!("this operator does not own a mixnode to update"); + return; + } + }; + + let update = MixNodeConfigUpdate { + host: args + .host + .unwrap_or(current_details.bond_information.mix_node.host), + mix_port: args + .mix_port + .unwrap_or(current_details.bond_information.mix_node.mix_port), + verloc_port: args + .verloc_port + .unwrap_or(current_details.bond_information.mix_node.verloc_port), + http_api_port: args + .http_api_port + .unwrap_or(current_details.bond_information.mix_node.http_api_port), + version: args + .version + .unwrap_or(current_details.bond_information.mix_node.version), + }; + + let res = client + .update_mixnode_config(update, None) + .await + .expect("updating mix-node config"); + + info!("mixnode config updated: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/settings/update_profit_percent.rs b/common/commands/src/validator/mixnet/operators/mixnode/settings/update_profit_percent.rs deleted file mode 100644 index 3c874c007c..0000000000 --- a/common/commands/src/validator/mixnet/operators/mixnode/settings/update_profit_percent.rs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::context::SigningClient; -use clap::Parser; -use log::info; - -#[derive(Debug, Parser)] -pub struct Args { - #[clap(long)] - pub profit_percent: u8, -} - -pub async fn update_profit_percent(args: Args, client: SigningClient) { - info!("Update mix node profit percent - get those rewards!"); - - //profit percent between 1-100 - let res = client - .update_mixnode_config(args.profit_percent, None) - .await - .expect("updating mix-node profit percent"); - - info!("profit percentage updated: {:?}", res) -} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/settings/vesting_update_config.rs b/common/commands/src/validator/mixnet/operators/mixnode/settings/vesting_update_config.rs new file mode 100644 index 0000000000..bca3642c97 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/settings/vesting_update_config.rs @@ -0,0 +1,69 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use mixnet_contract_common::MixNodeConfigUpdate; +use validator_client::nymd::traits::MixnetQueryClient; +use validator_client::nymd::VestingSigningClient; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub host: Option, + + #[clap(long)] + pub mix_port: Option, + + #[clap(long)] + pub verloc_port: Option, + + #[clap(long)] + pub http_api_port: Option, + + #[clap(long)] + pub version: Option, +} + +pub async fn vesting_update_config(client: SigningClient, args: Args) { + info!("Update vesting mix node config!"); + + let current_details = match client + .get_owned_mixnode(client.address()) + .await + .expect("failed to query the chain for mixnode details") + .mixnode_details + { + Some(details) => details, + None => { + log::warn!("this operator does not own a mixnode to update"); + return; + } + }; + + let update = MixNodeConfigUpdate { + host: args + .host + .unwrap_or(current_details.bond_information.mix_node.host), + mix_port: args + .mix_port + .unwrap_or(current_details.bond_information.mix_node.mix_port), + verloc_port: args + .verloc_port + .unwrap_or(current_details.bond_information.mix_node.verloc_port), + http_api_port: args + .http_api_port + .unwrap_or(current_details.bond_information.mix_node.http_api_port), + version: args + .version + .unwrap_or(current_details.bond_information.mix_node.version), + }; + + let res = client + .vesting_update_mixnode_config(update, None) + .await + .expect("updating vesting mix-node config"); + + info!("mixnode config updated: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/settings/vesting_update_profit_percent.rs b/common/commands/src/validator/mixnet/operators/mixnode/settings/vesting_update_profit_percent.rs deleted file mode 100644 index cc47d99c1e..0000000000 --- a/common/commands/src/validator/mixnet/operators/mixnode/settings/vesting_update_profit_percent.rs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::context::SigningClient; -use clap::Parser; -use log::info; -use validator_client::nymd::VestingSigningClient; - -#[derive(Debug, Parser)] -pub struct Args { - #[clap(long)] - pub profit_percent: u8, - - #[clap(long)] - pub gas: Option, -} - -pub async fn vesting_update_profit_percent(client: SigningClient, args: Args) { - info!("Update vesting mix node profit percent - get those rewards!"); - - //profit percent between 1-100 - let res = client - .vesting_update_mixnode_config(args.profit_percent, None) - .await - .expect("updating vesting mix-node profit percent"); - - info!("profit percentage updated: {:?}", res) -} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/unbond_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/unbond_mixnode.rs index d4f2bb478c..04fa9a55cd 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/unbond_mixnode.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/unbond_mixnode.rs @@ -3,6 +3,7 @@ use clap::Parser; use log::info; +use validator_client::nymd::traits::MixnetSigningClient; use crate::context::SigningClient; diff --git a/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs index 2e90a83f7e..6f20996460 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs @@ -3,13 +3,14 @@ use crate::context::SigningClient; use clap::Parser; +use cosmwasm_std::Uint128; use log::{info, warn}; -use mixnet_contract_common::Coin; -use mixnet_contract_common::MixNode; +use mixnet_contract_common::{Coin, MixNodeCostParams}; +use mixnet_contract_common::{MixNode, Percent}; use network_defaults::{ DEFAULT_HTTP_API_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT, }; -use validator_client::nymd::VestingSigningClient; +use validator_client::nymd::{CosmWasmCoin, VestingSigningClient}; #[derive(Debug, Parser)] pub struct Args { @@ -40,6 +41,12 @@ pub struct Args { #[clap(long)] pub profit_margin_percent: Option, + #[clap( + long, + help = "operating cost in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub interval_operating_cost: Option, + #[clap( long, help = "bonding amount in current DENOMINATION (so it would be 'unym', rather than 'nym')" @@ -72,13 +79,23 @@ pub async fn vesting_bond_mixnode(client: SigningClient, args: Args, denom: &str sphinx_key: args.sphinx_key, identity_key: args.identity_key, version: args.version, - profit_margin_percent: args.profit_margin_percent.unwrap_or(10), }; let coin = Coin::new(args.amount, denom); + let cost_params = MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value( + args.profit_margin_percent.unwrap_or(10) as u64, + ) + .unwrap(), + interval_operating_cost: CosmWasmCoin { + denom: denom.into(), + amount: Uint128::new(args.interval_operating_cost.unwrap_or(40_000_000)), + }, + }; + let res = client - .vesting_bond_mixnode(mixnode, &*args.signature, coin.into(), None) + .vesting_bond_mixnode(mixnode, cost_params, &args.signature, coin.into(), None) .await .expect("failed to bond vesting mixnode!"); diff --git a/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs b/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs index 91cc344806..4ec6b25b7b 100644 --- a/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs +++ b/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs @@ -5,7 +5,7 @@ use clap::Parser; use comfy_table::Table; use crate::context::QueryClientWithValidatorAPI; -use crate::utils::{pretty_cosmwasm_coin, show_error}; +use crate::utils::{pretty_decimal_with_denom, show_error}; #[derive(Debug, Parser)] pub struct Args { @@ -19,7 +19,8 @@ pub async fn query(args: Args, client: &QueryClientWithValidatorAPI) { Ok(res) => match args.identity_key { Some(identity_key) => { let node = res.iter().find(|node| { - node.mix_node + node.bond_information + .mix_node .identity_key .to_string() .eq_ignore_ascii_case(&identity_key) @@ -33,6 +34,7 @@ pub async fn query(args: Args, client: &QueryClientWithValidatorAPI) { let mut table = Table::new(); table.set_header(vec![ + "Mix id", "Identity Key", "Owner", "Host", @@ -41,13 +43,15 @@ pub async fn query(args: Args, client: &QueryClientWithValidatorAPI) { "Version", ]); for node in res { + let denom = &node.bond_information.original_pledge().denom; table.add_row(vec![ - node.mix_node.identity_key.to_string(), - node.owner.to_string(), - node.mix_node.host.to_string(), - pretty_cosmwasm_coin(&node.pledge_amount), - pretty_cosmwasm_coin(&node.total_delegation()), - node.mix_node.version, + node.mix_id().to_string(), + node.bond_information.mix_node.identity_key.clone(), + node.bond_information.owner.clone().into_string(), + node.bond_information.mix_node.host.clone(), + pretty_decimal_with_denom(node.rewarding_details.operator, denom), + pretty_decimal_with_denom(node.rewarding_details.delegates, denom), + node.bond_information.mix_node.version, ]); } diff --git a/common/commands/src/validator/vesting/create_vesting_schedule.rs b/common/commands/src/validator/vesting/create_vesting_schedule.rs index 60f69e6f2b..65268d27ed 100644 --- a/common/commands/src/validator/vesting/create_vesting_schedule.rs +++ b/common/commands/src/validator/vesting/create_vesting_schedule.rs @@ -51,7 +51,7 @@ pub async fn create(args: Args, client: SigningClient, network_details: &NymNetw let res = client .create_periodic_vesting_account( - &*args.address, + &args.address, args.staking_address, Some(vesting), coin.into(), @@ -70,7 +70,7 @@ pub async fn create(args: Args, client: SigningClient, network_details: &NymNetw let send_coin_response = client .send( - &AccountId::from_str(&*args.address).unwrap(), + &AccountId::from_str(&args.address).unwrap(), vec![coin.into()], "payment made :)", None, diff --git a/common/config/src/lib.rs b/common/config/src/lib.rs index a1ec8ff1ac..1f72896d9c 100644 --- a/common/config/src/lib.rs +++ b/common/config/src/lib.rs @@ -88,6 +88,7 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned { let location = custom_location .unwrap_or_else(|| self.config_directory().join(Self::config_file_name())); + log::info!("Configuration file will be saved to {:?}", location); cfg_if::cfg_if! { if #[cfg(unix)] { diff --git a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml index a7e383db64..2145acc513 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml +++ b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml @@ -8,3 +8,4 @@ edition = "2021" [dependencies] cosmwasm-std = "1.0.0" +serde = { version = "1.0", features = ["derive"] } diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/events.rs b/common/cosmwasm-smart-contracts/contracts-common/src/events.rs index d9b5b363ca..06cea03f22 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/events.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/events.rs @@ -28,3 +28,26 @@ pub fn may_find_attribute(event: &Event, key: &str) -> Option { } None } + +pub trait OptionallyAddAttribute { + fn add_optional_attribute( + self, + key: impl Into, + value: Option>, + ) -> Self; +} + +impl OptionallyAddAttribute for Event { + fn add_optional_attribute( + self, + key: impl Into, + value: Option>, + ) -> Self { + if let Some(value) = value { + self.add_attribute(key, value) + } else { + // TODO: perhaps if value doesn't exist, we should emit explicit 'null'? + self + } + } +} diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs b/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs index 58d367a2f5..53bf334611 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs @@ -2,3 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 pub mod events; +pub mod types; + +pub use types::*; diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs new file mode 100644 index 0000000000..6c25d95552 --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs @@ -0,0 +1,33 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +// TODO: there's no reason this couldn't be used for proper binaries, but in that case +// perhaps the struct should get renamed and moved to a "more" common crate +#[derive(Debug, Serialize, Deserialize)] +pub struct ContractBuildInformation { + // VERGEN_BUILD_TIMESTAMP + /// Provides the build timestamp, for example `2021-02-23T20:14:46.558472672+00:00`. + pub build_timestamp: String, + + // VERGEN_BUILD_SEMVER + /// Provides the build version, for example `0.1.0-9-g46f83e1`. + pub build_version: String, + + // VERGEN_GIT_SHA + /// Provides the hash of the commit that was used for the build, for example `46f83e112520533338245862d366f6a02cef07d4`. + pub commit_sha: String, + + // VERGEN_GIT_COMMIT_TIMESTAMP + /// Provides the timestamp of the commit that was used for the build, for example `2021-02-23T08:08:02-05:00`. + pub commit_timestamp: String, + + // VERGEN_GIT_BRANCH + /// Provides the name of the git branch that was used for the build, for example `master`. + pub commit_branch: String, + + // VERGEN_RUSTC_SEMVER + /// Provides the rustc version that was used for the build, for example `1.52.0-nightly`. + pub rustc_version: String, +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml index 1a8cbe55cc..bd5a6c2085 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml @@ -8,21 +8,22 @@ rust-version = "1.62" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +bs58 = "0.4.0" cosmwasm-std = "1.0.0" - serde = { version = "1.0", features = ["derive"] } serde_repr = "0.1" schemars = "0.8" thiserror = "1.0" -fixed = { version = "1.1", features = ["serde"] } -az = "1.1" +contracts-common = { path = "../contracts-common" } +serde_json = "1.0.0" + +# TO CHECK WHETHER STILL NEEDED: log = "0.4.14" time = { version = "0.3.6", features = ["parsing", "formatting"] } -ts-rs = {version = "6.1.2", optional = true} - -contracts-common = { path = "../contracts-common" } +ts-rs = { version = "6.1.2", optional = true } [dev-dependencies] +rand_chacha = "0.3" time = { version = "0.3.5", features = ["serde", "macros"] } [features] diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/constants.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/constants.rs new file mode 100644 index 0000000000..2c46abf8ce --- /dev/null +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/constants.rs @@ -0,0 +1,25 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::Decimal; + +// I'm still not 100% sure how to feel about existence of this file +// This is equivalent of representing our display coin with 6 decimal places. +// I'm using this one as opposed to "Decimal::one()", as this provides us with higher accuracy +// whilst providing no noticable drawbacks. +pub const UNIT_DELEGATION_BASE: Decimal = + Decimal::raw(1_000_000_000 * 1_000_000_000_000_000_000u128); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn unit_delegation_didnt_change() { + // a sanity check test to make sure Decimal's `DECIMAL_FRACTIONAL` internal implementation hasn't changed + assert_eq!( + UNIT_DELEGATION_BASE, + Decimal::one() * Decimal::from_atomics(1_000_000_000u32, 0).unwrap() + ) + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs index d0d4372fa4..7fbff39f98 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs @@ -1,151 +1,134 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 // due to code generated by JsonSchema #![allow(clippy::field_reassign_with_default)] -use crate::{Addr, IdentityKey}; -use cosmwasm_std::{Coin, Uint128}; +use crate::{Addr, NodeId}; +use cosmwasm_std::{Coin, Decimal}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::fmt::Display; -use std::hash::{Hash, Hasher}; -type OwnerAddressBytes = Vec; -type BlockHeight = u64; +// just use a string representation of those so that we wouldn't need to bother with decoding bytes +// and trying to figure out whether they're valid, etc +pub type OwnerProxySubKey = String; +pub type StorageKey = (NodeId, OwnerProxySubKey); -pub fn generate_storage_key(address: &Addr, proxy: Option<&Addr>) -> Vec { +pub fn generate_owner_storage_subkey(address: &Addr, proxy: Option<&Addr>) -> String { if let Some(proxy) = &proxy { - address + let key_bytes = address .as_bytes() .iter() .zip(proxy.as_bytes()) .map(|(x, y)| x ^ y) - .collect() + .collect::>(); + bs58::encode(key_bytes).into_string() } else { - address.as_bytes().to_vec() + address.clone().into_string() } } #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] pub struct Delegation { + /// Address of the owner of this delegation. pub owner: Addr, - pub node_identity: IdentityKey, + + /// Id of the MixNode that this delegation was performed against. + pub node_id: NodeId, + + // Note to UI/UX devs: there's absolutely no point in displaying this value to the users, + // it would serve them no purpose. It's only used for calculating rewards + /// Value of the "unit delegation" associated with the mixnode at the time of delegation. + #[serde(rename = "crr")] + pub cumulative_reward_ratio: Decimal, + + /// Original delegation amount. Note that it is never mutated as delegation accumulates rewards. pub amount: Coin, - pub block_height: u64, - pub proxy: Option, // proxy address used to delegate the funds on behalf of another address -} -impl Eq for Delegation {} + /// Block height where this delegation occurred. + pub height: u64, -#[allow(clippy::derive_hash_xor_eq)] -impl Hash for Delegation { - fn hash(&self, state: &mut H) { - self.owner.hash(state); - self.node_identity.hash(state); - self.block_height.hash(state); - self.proxy.hash(state); - } + /// Proxy address used to delegate the funds on behalf of another address + pub proxy: Option, } impl Delegation { pub fn new( owner: Addr, - node_identity: IdentityKey, + node_id: NodeId, + cumulative_reward_ratio: Decimal, amount: Coin, - block_height: BlockHeight, + height: u64, proxy: Option, ) -> Self { Delegation { owner, - node_identity, + node_id, + cumulative_reward_ratio, amount, - block_height, + height, proxy, } } - pub fn storage_key(&self) -> (IdentityKey, OwnerAddressBytes, BlockHeight) { - ( - self.node_identity(), - self.proxy_storage_key(), - self.block_height(), - ) + pub fn generate_storage_key( + node_id: NodeId, + owner_address: &Addr, + proxy: Option<&Addr>, + ) -> StorageKey { + (node_id, generate_owner_storage_subkey(owner_address, proxy)) } - pub fn event_storage_key(&self) -> (OwnerAddressBytes, BlockHeight, IdentityKey) { - ( - self.proxy_storage_key(), - self.block_height(), - self.node_identity(), - ) + // this function might seem a bit redundant, but I'd rather explicitly keep it around in case + // some types change in the future + pub fn generate_storage_key_with_subkey( + node_id: NodeId, + owner_proxy_subkey: OwnerProxySubKey, + ) -> StorageKey { + (node_id, owner_proxy_subkey) } - pub fn proxy_storage_key(&self) -> OwnerAddressBytes { - generate_storage_key(&self.owner, self.proxy.as_ref()) + pub fn dec_amount(&self) -> Decimal { + // the unwrap here is fine as we're guaranteed our base coin amount is going to fit in a Decimal + // with 0 decimal places + Decimal::from_atomics(self.amount.amount, 0).unwrap() } - pub fn proxy(&self) -> Option<&Addr> { - self.proxy.as_ref() + pub fn proxy_storage_key(&self) -> OwnerProxySubKey { + generate_owner_storage_subkey(&self.owner, self.proxy.as_ref()) } - pub fn increment_amount(&mut self, amount: Uint128, at_height: Option) { - self.amount.amount += amount; - if let Some(at_height) = at_height { - self.block_height = at_height; - } - } - - pub fn amount(&self) -> &Coin { - &self.amount - } - - pub fn node_identity(&self) -> IdentityKey { - self.node_identity.clone() - } - - pub fn owner(&self) -> Addr { - self.owner.clone() - } - - pub fn block_height(&self) -> u64 { - self.block_height + pub fn storage_key(&self) -> StorageKey { + Self::generate_storage_key(self.node_id, &self.owner, self.proxy.as_ref()) } } -impl Display for Delegation { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{} delegated towards {} by {} at block {}", - self.amount, self.node_identity, self.owner, self.block_height - ) - } -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] -pub struct PagedMixDelegationsResponse { +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct PagedMixNodeDelegationsResponse { pub delegations: Vec, - pub start_next_after: Option<(String, u64)>, + pub start_next_after: Option, } -impl PagedMixDelegationsResponse { - pub fn new(delegations: Vec, start_next_after: Option<(String, u64)>) -> Self { - PagedMixDelegationsResponse { +impl PagedMixNodeDelegationsResponse { + pub fn new(delegations: Vec, start_next_after: Option) -> Self { + PagedMixNodeDelegationsResponse { delegations, start_next_after, } } } -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct PagedDelegatorDelegationsResponse { pub delegations: Vec, - pub start_next_after: Option, + pub start_next_after: Option<(NodeId, OwnerProxySubKey)>, } impl PagedDelegatorDelegationsResponse { - pub fn new(delegations: Vec, start_next_after: Option) -> Self { + pub fn new( + delegations: Vec, + start_next_after: Option<(NodeId, OwnerProxySubKey)>, + ) -> Self { PagedDelegatorDelegationsResponse { delegations, start_next_after, @@ -153,20 +136,32 @@ impl PagedDelegatorDelegationsResponse { } } -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] -pub struct PagedAllDelegationsResponse { - pub delegations: Vec, - pub start_next_after: Option<(IdentityKey, Vec, u64)>, +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct MixNodeDelegationResponse { + pub delegation: Option, + pub mixnode_still_bonded: bool, } -impl PagedAllDelegationsResponse { - pub fn new( - delegations: Vec, - start_next_after: Option<(IdentityKey, Vec, u64)>, - ) -> Self { - PagedAllDelegationsResponse { - delegations, - start_next_after: start_next_after.map(|(id, addr, height)| (id, addr, height)), +impl MixNodeDelegationResponse { + pub fn new(delegation: Option, mixnode_still_bonded: bool) -> Self { + MixNodeDelegationResponse { + delegation, + mixnode_still_bonded, + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct PagedAllDelegationsResponse { + pub delegations: Vec, + pub start_next_after: Option, +} + +impl PagedAllDelegationsResponse { + pub fn new(delegations: Vec, start_next_after: Option) -> Self { + PagedAllDelegationsResponse { + delegations, + start_next_after, } } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs index 1cc501a3a0..3808df55d8 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs @@ -1,23 +1,139 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::NodeId; +use cosmwasm_std::{Addr, Coin, Decimal}; use thiserror::Error; #[derive(Error, Debug, PartialEq)] pub enum MixnetContractError { - #[error("Overflow Error")] - OverflowError(#[from] cosmwasm_std::OverflowError), - #[error("reward_blockstamp field not set, set_reward_blockstamp must be called before attempting to issue rewards")] - BlockstampNotSet, - #[error("{source}")] - TryFromIntError { - #[from] - source: std::num::TryFromIntError, - }, - #[error("Error casting from U128")] - CastError, #[error("{source}")] StdErr { #[from] source: cosmwasm_std::StdError, }, - #[error("Division by zero at {}", line!())] - DivisionByZero, + + #[error("Provided percent value is greater than 100%")] + InvalidPercent, + + #[error("Attempted to subtract decimals with overflow ({minuend}.sub({subtrahend}))")] + OverflowDecimalSubtraction { + minuend: Decimal, + subtrahend: Decimal, + }, + + #[error("Attempted to subtract with overflow ({minuend}.sub({subtrahend}))")] + OverflowSubtraction { minuend: u64, subtrahend: u64 }, + + #[error("Not enough funds sent for node pledge. (received {received}, minimum {minimum})")] + InsufficientPledge { received: Coin, minimum: Coin }, + + #[error("Not enough funds sent for node delegation. (received {received}, minimum {minimum})")] + InsufficientDelegation { received: Coin, minimum: Coin }, + + #[error("Mixnode ({id}) does not exist")] + MixNodeBondNotFound { id: NodeId }, + + #[error("{owner} does not seem to own any mixnodes")] + NoAssociatedMixNodeBond { owner: Addr }, + + #[error("{owner} does not seem to own any gateways")] + NoAssociatedGatewayBond { owner: Addr }, + + #[error("This address has already bonded a mixnode")] + AlreadyOwnsMixnode, + + #[error("This address has already bonded a gateway")] + AlreadyOwnsGateway, + + #[error("Gateway with this identity already exists. Its owner is {owner}")] + DuplicateGateway { owner: Addr }, + + #[error("Unauthorized")] + Unauthorized, + + #[error("No tokens were sent for the bonding")] + NoBondFound, + + #[error("No funds were provided for the delegation")] + EmptyDelegation, + + #[error("Wrong coin denomination. Received: {received}, expected: {expected}")] + WrongDenom { received: String, expected: String }, + + #[error("Received multiple coin types during staking")] + MultipleDenoms, + + #[error("Proxy address mismatch, expected {existing}, got {incoming}")] + ProxyMismatch { existing: String, incoming: String }, + + #[error("Failed to recover ed25519 public key from its base58 representation - {0}")] + MalformedEd25519IdentityKey(String), + + #[error("Failed to recover ed25519 signature from its base58 representation - {0}")] + MalformedEd25519Signature(String), + + #[error("Provided ed25519 signature did not verify correctly")] + InvalidEd25519Signature, + + #[error("Can't perform the specified action as the current epoch is still progress. It started at {epoch_start} and finishes at {epoch_end}, while the current block time is {current_block_time}")] + EpochInProgress { + current_block_time: u64, + epoch_start: i64, + epoch_end: i64, + }, + + #[error("Mixnode {node_id} has already been rewarded during the current rewarding epoch ({absolute_epoch_id})")] + MixnodeAlreadyRewarded { + node_id: NodeId, + absolute_epoch_id: u32, + }, + + #[error("Mixnode {node_id} hasn't been selected to the rewarding set in this epoch ({absolute_epoch_id})")] + MixnodeNotInRewardedSet { + node_id: NodeId, + absolute_epoch_id: u32, + }, + + #[error("Mixnode {node_id} is currently in the process of unbonding")] + MixnodeIsUnbonding { node_id: NodeId }, + + #[error("Mixnode {node_id} has already unbonded")] + MixnodeHasUnbonded { node_id: NodeId }, + + #[error("The contract has ended up in a state that was deemed impossible: {comment}")] + InconsistentState { comment: String }, + + #[error( + "Could not find any delegation information associated with mixnode {mix_id} for {address} (proxy: {proxy:?})" + )] + NoMixnodeDelegationFound { + mix_id: NodeId, + address: String, + proxy: Option, + }, + + #[error("Provided message to update rewarding params did not contain any updates")] + EmptyParamsChangeMsg, + + #[error("Provided active set size is bigger than the rewarded set")] + InvalidActiveSetSize, + + #[error("Provided rewarded set size is smaller than the active set")] + InvalidRewardedSetSize, + + #[error("Provided active set size is zero")] + ZeroActiveSet, + + #[error("Provided rewarded set size is zero")] + ZeroRewardedSet, + + #[error("Received unexpected value for the active set. Got: {received}, expected: {expected}")] + UnexpectedActiveSetSize { received: u32, expected: u32 }, + + #[error("Received unexpected value for the rewarded set. Got: {received}, expected at most: {expected}")] + UnexpectedRewardedSetSize { received: u32, expected: u32 }, + + #[error("Mixnode {node_id} appears multiple times in the provided rewarded set update!")] + DuplicateRewardedSetNode { node_id: NodeId }, } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs index a2ed8ffe35..2804f8660f 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs @@ -1,32 +1,87 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::mixnode::NodeRewardResult; -use crate::{ContractStateParams, IdentityKeyRef, Interval, Layer}; -use cosmwasm_std::{Addr, Coin, Event, Uint128}; +use crate::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; +use crate::reward_params::{IntervalRewardParams, IntervalRewardingParamsUpdate}; +use crate::rewarding::RewardDistribution; +use crate::{ContractStateParams, IdentityKeyRef, Interval, Layer, NodeId}; pub use contracts_common::events::*; -// FIXME: This should becoma an Enum -// event types -pub const DELEGATION_EVENT_TYPE: &str = "delegation"; -pub const PENDING_DELEGATION_EVENT_TYPE: &str = "pending_delegation"; -pub const RECONCILE_DELEGATION_EVENT_TYPE: &str = "reconcile_delegation"; -pub const UNDELEGATION_EVENT_TYPE: &str = "undelegation"; -pub const PENDING_UNDELEGATION_EVENT_TYPE: &str = "pending_undelegation"; -pub const GATEWAY_BONDING_EVENT_TYPE: &str = "gateway_bonding"; -pub const GATEWAY_UNBONDING_EVENT_TYPE: &str = "gateway_unbonding"; -pub const MIXNODE_BONDING_EVENT_TYPE: &str = "mixnode_bonding"; -pub const MIXNODE_UNBONDING_EVENT_TYPE: &str = "mixnode_unbonding"; -pub const SETTINGS_UPDATE_EVENT_TYPE: &str = "settings_update"; -pub const OPERATOR_REWARDING_EVENT_TYPE: &str = "mix_rewarding"; -pub const MIX_DELEGATORS_REWARDING_EVENT_TYPE: &str = "mix_delegators_rewarding"; -pub const CHANGE_REWARDED_SET_EVENT_TYPE: &str = "change_rewarded_set"; -pub const ADVANCE_INTERVAL_EVENT_TYPE: &str = "advance_interval"; -pub const ADVANCE_EPOCH_EVENT_TYPE: &str = "advance_epoch"; -pub const COMPOUND_DELEGATOR_REWARD_EVENT_TYPE: &str = "compound_delegator_reward"; -pub const CLAIM_DELEGATOR_REWARD_EVENT_TYPE: &str = "claim_delegator_reward"; -pub const COMPOUND_OPERATOR_REWARD_EVENT_TYPE: &str = "compound_operator_reward"; -pub const CLAIM_OPERATOR_REWARD_EVENT_TYPE: &str = "claim_operator_reward"; -pub const SNAPSHOT_MIXNODES_EVENT: &str = "snapshot_mixnodes"; +use cosmwasm_std::{Addr, Coin, Event}; + +pub enum MixnetEventType { + MixnodeBonding, + GatewayBonding, + GatewayUnbonding, + PendingMixnodeUnbonding, + MixnodeUnbonding, + MixnodeConfigUpdate, + PendingMixnodeCostParamsUpdate, + MixnodeCostParamsUpdate, + MixnodeRewarding, + WithdrawDelegatorReward, + WithdrawOperatorReward, + PendingActiveSetUpdate, + ActiveSetUpdate, + PendingIntervalRewardingParamsUpdate, + IntervalRewardingParamsUpdate, + PendingDelegation, + PendingUndelegation, + Delegation, + DelegationOnUnbonding, + Undelegation, + ContractSettingsUpdate, + RewardingValidatorUpdate, + AdvanceEpoch, + ExecutePendingEpochEvents, + ExecutePendingIntervalEvents, + ReconcilePendingEvents, + PendingIntervalConfigUpdate, + IntervalConfigUpdate, +} + +impl From for String { + fn from(typ: MixnetEventType) -> Self { + typ.to_string() + } +} + +impl ToString for MixnetEventType { + fn to_string(&self) -> String { + match self { + MixnetEventType::MixnodeBonding => "mixnode_bonding", + MixnetEventType::GatewayBonding => "gateway_bonding", + MixnetEventType::GatewayUnbonding => "gateway_unbonding", + MixnetEventType::PendingMixnodeUnbonding => "pending_mixnode_unbonding", + MixnetEventType::MixnodeConfigUpdate => "mixnode_config_update", + MixnetEventType::MixnodeUnbonding => "mixnode_unbonding", + MixnetEventType::PendingMixnodeCostParamsUpdate => "pending_mixnode_cost_params_update", + MixnetEventType::MixnodeCostParamsUpdate => "mixnode_cost_params_update", + MixnetEventType::MixnodeRewarding => "mix_rewarding", + MixnetEventType::WithdrawDelegatorReward => "withdraw_delegator_reward", + MixnetEventType::WithdrawOperatorReward => "withdraw_operator_reward", + MixnetEventType::PendingActiveSetUpdate => "pending_active_set_update", + MixnetEventType::ActiveSetUpdate => "active_set_update", + MixnetEventType::PendingIntervalRewardingParamsUpdate => { + "pending_interval_rewarding_params_update" + } + MixnetEventType::IntervalRewardingParamsUpdate => "interval_rewarding_params_update", + MixnetEventType::PendingDelegation => "pending_delegation", + MixnetEventType::PendingUndelegation => "pending_undelegation", + MixnetEventType::Delegation => "delegation", + MixnetEventType::Undelegation => "undelegation", + MixnetEventType::ContractSettingsUpdate => "settings_update", + MixnetEventType::RewardingValidatorUpdate => "rewarding_validator_address_update", + MixnetEventType::AdvanceEpoch => "advance_epoch", + MixnetEventType::ExecutePendingEpochEvents => "execute_pending_epoch_events", + MixnetEventType::ExecutePendingIntervalEvents => "execute_pending_interval_events", + MixnetEventType::ReconcilePendingEvents => "reconcile_pending_events", + MixnetEventType::PendingIntervalConfigUpdate => "pending_interval_config_update", + MixnetEventType::IntervalConfigUpdate => "interval_config_update", + MixnetEventType::DelegationOnUnbonding => "delegation_on_unbonding_node", + } + .into() + } +} // attributes that are used in multiple places pub const OWNER_KEY: &str = "owner"; @@ -41,35 +96,42 @@ pub const DELEGATION_TARGET_KEY: &str = "delegation_target"; pub const DELEGATION_HEIGHT_KEY: &str = "delegation_latest_block_height"; // bonding/unbonding +pub const NODE_ID_KEY: &str = "node_id"; pub const NODE_IDENTITY_KEY: &str = "identity"; pub const ASSIGNED_LAYER_KEY: &str = "assigned_layer"; // settings change pub const OLD_MINIMUM_MIXNODE_PLEDGE_KEY: &str = "old_minimum_mixnode_pledge"; pub const OLD_MINIMUM_GATEWAY_PLEDGE_KEY: &str = "old_minimum_gateway_pledge"; -pub const OLD_MIXNODE_REWARDED_SET_SIZE_KEY: &str = "old_mixnode_rewarded_set_size"; -pub const OLD_MIXNODE_ACTIVE_SET_SIZE_KEY: &str = "old_mixnode_active_set_size"; -pub const OLD_ACTIVE_SET_WORK_FACTOR_KEY: &str = "old_active_set_work_factor"; +pub const OLD_MINIMUM_DELEGATION_KEY: &str = "old_minimum_delegation"; pub const NEW_MINIMUM_MIXNODE_PLEDGE_KEY: &str = "new_minimum_mixnode_pledge"; pub const NEW_MINIMUM_GATEWAY_PLEDGE_KEY: &str = "new_minimum_gateway_pledge"; -pub const NEW_MIXNODE_REWARDED_SET_SIZE_KEY: &str = "new_mixnode_rewarded_set_size"; -pub const NEW_MIXNODE_ACTIVE_SET_SIZE_KEY: &str = "new_mixnode_active_set_size"; +pub const NEW_MINIMUM_DELEGATION_KEY: &str = "new_minimum_delegation"; + +pub const OLD_REWARDING_VALIDATOR_ADDRESS_KEY: &str = "old_rewarding_validator_address"; +pub const NEW_REWARDING_VALIDATOR_ADDRESS_KEY: &str = "new_rewarding_validator_address"; + +pub const UPDATED_MIXNODE_CONFIG_KEY: &str = "updated_mixnode_config"; +pub const UPDATED_MIXNODE_COST_PARAMS_KEY: &str = "updated_mixnode_cost_params"; // rewarding -pub const INTERVAL_ID_KEY: &str = "interval_id"; +pub const INTERVAL_KEY: &str = "interval_details"; pub const TOTAL_MIXNODE_REWARD_KEY: &str = "total_node_reward"; -pub const OPERATOR_REWARD_KEY: &str = "operator_reward"; pub const TOTAL_PLEDGE_KEY: &str = "pledge"; pub const TOTAL_DELEGATIONS_KEY: &str = "delegated"; -pub const LAMBDA_KEY: &str = "lambda"; -pub const SIGMA_KEY: &str = "sigma"; +pub const OPERATOR_REWARD_KEY: &str = "operator_reward"; +pub const DELEGATES_REWARD_KEY: &str = "delegates_reward"; +pub const APPROXIMATE_TIME_LEFT_SECS_KEY: &str = "approximate_time_left_secs"; +pub const INTERVAL_REWARDING_PARAMS_UPDATE_KEY: &str = "interval_rewarding_params_update"; +pub const UPDATED_INTERVAL_REWARDING_PARAMS_KEY: &str = "updated_interval_rewarding_params"; + pub const DISTRIBUTED_DELEGATION_REWARDS_KEY: &str = "distributed_delegation_rewards"; pub const FURTHER_DELEGATIONS_TO_REWARD_KEY: &str = "further_delegations"; pub const NO_REWARD_REASON_KEY: &str = "no_reward_reason"; pub const BOND_NOT_FOUND_VALUE: &str = "bond_not_found"; pub const BOND_TOO_FRESH_VALUE: &str = "bond_too_fresh"; -pub const ZERO_UPTIME_VALUE: &str = "zero_uptime"; +pub const ZERO_PERFORMANCE_VALUE: &str = "zero_performance"; // rewarded set update pub const ACTIVE_SET_SIZE_KEY: &str = "active_set_size"; @@ -80,156 +142,140 @@ pub const CURRENT_INTERVAL_ID_KEY: &str = "current_interval"; pub const NEW_CURRENT_INTERVAL_KEY: &str = "new_current_interval"; pub const NEW_CURRENT_EPOCH_KEY: &str = "new_current_epoch"; pub const BLOCK_HEIGHT_KEY: &str = "block_height"; -pub const CHECKPOINT_MIXNODES_EVENT: &str = "checkpoint_mixnodes"; pub const RECONCILIATION_ERROR_EVENT: &str = "reconciliation_error"; -pub fn new_checkpoint_mixnodes_event(block_height: u64) -> Event { - Event::new(CHECKPOINT_MIXNODES_EVENT) - .add_attribute(BLOCK_HEIGHT_KEY, format!("{}", block_height)) -} - -pub fn new_error_event(err: String) -> Event { - Event::new(RECONCILIATION_ERROR_EVENT).add_attribute("error", err) -} +// interval +pub const EVENTS_EXECUTED_KEY: &str = "number_of_events_executed"; +pub const REWARDED_SET_NODES_KEY: &str = "rewarded_set_nodes"; +pub const NEW_EPOCHS_DURATION_SECS_KEY: &str = "new_epoch_durations_secs"; +pub const NEW_EPOCHS_IN_INTERVAL: &str = "new_epochs_in_interval"; pub fn new_delegation_event( delegator: &Addr, proxy: &Option, amount: &Coin, - mix_identity: IdentityKeyRef<'_>, + mix_id: NodeId, ) -> Event { - let mut event = Event::new(DELEGATION_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator); - - if let Some(proxy) = proxy { - event = event.add_attribute(PROXY_KEY, proxy) - } - - // coin implements Display trait and we use that implementation here - event + Event::new(MixnetEventType::Delegation) + .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_identity) + .add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string()) +} + +pub fn new_delegation_on_unbonded_node_event( + delegator: &Addr, + proxy: &Option, + mix_id: NodeId, +) -> 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_identity: IdentityKeyRef<'_>, + mix_id: NodeId, ) -> Event { - let mut event = - Event::new(PENDING_DELEGATION_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator); - - if let Some(proxy) = proxy { - event = event.add_attribute(PROXY_KEY, proxy) - } - - // coin implements Display trait and we use that implementation here - event - .add_attribute(AMOUNT_KEY, amount.to_string()) - .add_attribute(DELEGATION_TARGET_KEY, mix_identity) -} - -pub fn new_reconcile_delegation_event( - delegator: &Addr, - proxy: &Option, - amount: &Coin, - mix_identity: IdentityKeyRef<'_>, -) -> Event { - let mut event = - Event::new(RECONCILE_DELEGATION_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator); - - if let Some(proxy) = proxy { - event = event.add_attribute(PROXY_KEY, proxy) - } - - // coin implements Display trait and we use that implementation here - event - .add_attribute(AMOUNT_KEY, amount.to_string()) - .add_attribute(DELEGATION_TARGET_KEY, mix_identity) -} - -pub fn new_compound_operator_reward_event(owner: &Addr, amount: Uint128) -> Event { - let event = Event::new(COMPOUND_OPERATOR_REWARD_EVENT_TYPE).add_attribute(OWNER_KEY, owner); - event.add_attribute(AMOUNT_KEY, amount.to_string()) -} - -pub fn new_claim_operator_reward_event(owner: &Addr, amount: Uint128) -> Event { - let event = Event::new(CLAIM_OPERATOR_REWARD_EVENT_TYPE).add_attribute(OWNER_KEY, owner); - event.add_attribute(AMOUNT_KEY, amount.to_string()) -} - -pub fn new_compound_delegator_reward_event( - delegator: &Addr, - proxy: &Option, - amount: Uint128, - mix_identity: IdentityKeyRef<'_>, -) -> Event { - let mut event = - Event::new(COMPOUND_DELEGATOR_REWARD_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator); - - if let Some(proxy) = proxy { - event = event.add_attribute(PROXY_KEY, proxy) - } - - // coin implements Display trait and we use that implementation here - event - .add_attribute(AMOUNT_KEY, amount.to_string()) - .add_attribute(DELEGATION_TARGET_KEY, mix_identity) + 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_claim_delegator_reward_event( +pub fn new_withdraw_operator_reward_event( + owner: &Addr, + proxy: &Option, + amount: Coin, + mix_id: NodeId, +) -> 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(NODE_ID_KEY, mix_id.to_string()) +} + +pub fn new_withdraw_delegator_reward_event( delegator: &Addr, proxy: &Option, - amount: Uint128, - mix_identity: IdentityKeyRef<'_>, + amount: Coin, + mix_id: NodeId, ) -> Event { - let mut event = - Event::new(CLAIM_DELEGATOR_REWARD_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator); - - if let Some(proxy) = proxy { - event = event.add_attribute(PROXY_KEY, proxy) - } - - // coin implements Display trait and we use that implementation here - event - .add_attribute(AMOUNT_KEY, amount.to_string()) - .add_attribute(DELEGATION_TARGET_KEY, mix_identity) + 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()) } -pub fn new_undelegation_event( - delegator: &Addr, - proxy: &Option, - mix_identity: IdentityKeyRef<'_>, - amount: Uint128, +pub fn new_active_set_update_event(new_size: u32) -> Event { + Event::new(MixnetEventType::ActiveSetUpdate) + .add_attribute(ACTIVE_SET_SIZE_KEY, new_size.to_string()) +} + +pub fn new_pending_active_set_update_event( + new_size: u32, + approximate_time_remaining_secs: i64, ) -> Event { - let mut event = Event::new(UNDELEGATION_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator); + Event::new(MixnetEventType::PendingActiveSetUpdate) + .add_attribute(ACTIVE_SET_SIZE_KEY, new_size.to_string()) + .add_attribute( + APPROXIMATE_TIME_LEFT_SECS_KEY, + approximate_time_remaining_secs.to_string(), + ) +} - if let Some(proxy) = proxy { - event = event.add_attribute(PROXY_KEY, proxy) - } +pub fn new_rewarding_params_update_event( + update: IntervalRewardingParamsUpdate, + updated: IntervalRewardParams, +) -> Event { + Event::new(MixnetEventType::IntervalRewardingParamsUpdate) + .add_attribute( + INTERVAL_REWARDING_PARAMS_UPDATE_KEY, + update.to_inline_json(), + ) + .add_attribute( + UPDATED_INTERVAL_REWARDING_PARAMS_KEY, + updated.to_inline_json(), + ) +} - // coin implements Display trait and we use that implementation here - event - .add_attribute(AMOUNT_KEY, amount.to_string()) - .add_attribute(DELEGATION_TARGET_KEY, mix_identity) +pub fn new_pending_rewarding_params_update_event( + update: IntervalRewardingParamsUpdate, + approximate_time_remaining_secs: i64, +) -> Event { + Event::new(MixnetEventType::PendingIntervalRewardingParamsUpdate) + .add_attribute( + INTERVAL_REWARDING_PARAMS_UPDATE_KEY, + update.to_inline_json(), + ) + .add_attribute( + APPROXIMATE_TIME_LEFT_SECS_KEY, + approximate_time_remaining_secs.to_string(), + ) +} + +pub fn new_undelegation_event(delegator: &Addr, proxy: &Option, mix_id: NodeId) -> Event { + Event::new(MixnetEventType::Undelegation) + .add_attribute(DELEGATOR_KEY, delegator) + .add_optional_attribute(PROXY_KEY, proxy.as_ref()) + .add_attribute(NODE_ID_KEY, mix_id.to_string()) } pub fn new_pending_undelegation_event( delegator: &Addr, proxy: &Option, - mix_identity: IdentityKeyRef<'_>, + mix_id: NodeId, ) -> Event { - let mut event = - Event::new(PENDING_UNDELEGATION_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator); - - if let Some(proxy) = proxy { - event = event.add_attribute(PROXY_KEY, proxy) - } - - // coin implements Display trait and we use that implementation here - event.add_attribute(DELEGATION_TARGET_KEY, mix_identity) + Event::new(MixnetEventType::PendingUndelegation) + .add_attribute(DELEGATOR_KEY, delegator) + .add_optional_attribute(PROXY_KEY, proxy.as_ref()) + .add_attribute(NODE_ID_KEY, mix_id.to_string()) } pub fn new_gateway_bonding_event( @@ -238,16 +284,11 @@ pub fn new_gateway_bonding_event( amount: &Coin, identity: IdentityKeyRef<'_>, ) -> Event { - let mut event = Event::new(GATEWAY_BONDING_EVENT_TYPE) + Event::new(MixnetEventType::GatewayBonding) .add_attribute(OWNER_KEY, owner) - .add_attribute(NODE_IDENTITY_KEY, identity); - - if let Some(proxy) = proxy { - event = event.add_attribute(PROXY_KEY, proxy) - } - - // coin implements Display trait and we use that implementation here - event.add_attribute(AMOUNT_KEY, amount.to_string()) + .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( @@ -256,16 +297,11 @@ pub fn new_gateway_unbonding_event( amount: &Coin, identity: IdentityKeyRef<'_>, ) -> Event { - let mut event = Event::new(GATEWAY_UNBONDING_EVENT_TYPE) + Event::new(MixnetEventType::GatewayUnbonding) .add_attribute(OWNER_KEY, owner) - .add_attribute(NODE_IDENTITY_KEY, identity); - - if let Some(proxy) = proxy { - event = event.add_attribute(PROXY_KEY, proxy) - } - - // coin implements Display trait and we use that implementation here - event.add_attribute(AMOUNT_KEY, amount.to_string()) + .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( @@ -273,55 +309,92 @@ pub fn new_mixnode_bonding_event( proxy: &Option, amount: &Coin, identity: IdentityKeyRef<'_>, + node_id: NodeId, assigned_layer: Layer, ) -> Event { - let mut event = Event::new(MIXNODE_BONDING_EVENT_TYPE) - .add_attribute(OWNER_KEY, owner) - .add_attribute(NODE_IDENTITY_KEY, identity); - - if let Some(proxy) = proxy { - event = event.add_attribute(PROXY_KEY, proxy) - } - // coin implements Display trait and we use that implementation here - event + Event::new(MixnetEventType::MixnodeBonding) + .add_attribute(NODE_ID_KEY, node_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()) } -pub fn new_mixnode_unbonding_event( +pub fn new_mixnode_unbonding_event(node_id: NodeId) -> Event { + Event::new(MixnetEventType::MixnodeUnbonding).add_attribute(NODE_ID_KEY, node_id.to_string()) +} + +pub fn new_pending_mixnode_unbonding_event( owner: &Addr, proxy: &Option, - amount: &Coin, identity: IdentityKeyRef<'_>, + node_id: NodeId, ) -> Event { - let mut event = Event::new(MIXNODE_UNBONDING_EVENT_TYPE) + Event::new(MixnetEventType::PendingMixnodeUnbonding) + .add_attribute(NODE_ID_KEY, node_id.to_string()) + .add_attribute(NODE_IDENTITY_KEY, identity) .add_attribute(OWNER_KEY, owner) - .add_attribute(NODE_IDENTITY_KEY, identity); + .add_optional_attribute(PROXY_KEY, proxy.as_ref()) +} - if let Some(proxy) = proxy { - event = event.add_attribute(PROXY_KEY, proxy) - } +pub fn new_mixnode_config_update_event( + node_id: NodeId, + owner: &Addr, + proxy: &Option, + update: &MixNodeConfigUpdate, +) -> Event { + Event::new(MixnetEventType::MixnodeConfigUpdate) + .add_attribute(NODE_ID_KEY, node_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()) +} - // coin implements Display trait and we use that implementation here - event.add_attribute(AMOUNT_KEY, amount.to_string()) +pub fn new_mixnode_pending_cost_params_update_event( + node_id: NodeId, + owner: &Addr, + proxy: &Option, + new_costs: &MixNodeCostParams, +) -> Event { + Event::new(MixnetEventType::PendingMixnodeCostParamsUpdate) + .add_attribute(NODE_ID_KEY, node_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()) +} + +pub fn new_mixnode_cost_params_update_event( + node_id: NodeId, + new_costs: &MixNodeCostParams, +) -> Event { + Event::new(MixnetEventType::MixnodeCostParamsUpdate) + .add_attribute(NODE_ID_KEY, node_id.to_string()) + .add_attribute(UPDATED_MIXNODE_COST_PARAMS_KEY, new_costs.to_inline_json()) +} + +pub fn new_rewarding_validator_address_update_event(old: Addr, new: Addr) -> Event { + Event::new(MixnetEventType::RewardingValidatorUpdate) + .add_attribute(OLD_REWARDING_VALIDATOR_ADDRESS_KEY, old) + .add_attribute(NEW_REWARDING_VALIDATOR_ADDRESS_KEY, new) } pub fn new_settings_update_event( old_params: &ContractStateParams, new_params: &ContractStateParams, ) -> Event { - let mut event = Event::new(SETTINGS_UPDATE_EVENT_TYPE); + let mut event = Event::new(MixnetEventType::ContractSettingsUpdate); if old_params.minimum_mixnode_pledge != new_params.minimum_mixnode_pledge { event = event .add_attribute( OLD_MINIMUM_MIXNODE_PLEDGE_KEY, - old_params.minimum_mixnode_pledge, + old_params.minimum_mixnode_pledge.to_string(), ) .add_attribute( NEW_MINIMUM_MIXNODE_PLEDGE_KEY, - new_params.minimum_mixnode_pledge, + new_params.minimum_mixnode_pledge.to_string(), ) } @@ -329,128 +402,128 @@ pub fn new_settings_update_event( event = event .add_attribute( OLD_MINIMUM_GATEWAY_PLEDGE_KEY, - old_params.minimum_gateway_pledge, + old_params.minimum_gateway_pledge.to_string(), ) .add_attribute( NEW_MINIMUM_GATEWAY_PLEDGE_KEY, - new_params.minimum_gateway_pledge, + new_params.minimum_gateway_pledge.to_string(), ) } - if old_params.mixnode_rewarded_set_size != new_params.mixnode_rewarded_set_size { - event = event - .add_attribute( - OLD_MIXNODE_REWARDED_SET_SIZE_KEY, - old_params.mixnode_rewarded_set_size.to_string(), - ) - .add_attribute( - NEW_MIXNODE_REWARDED_SET_SIZE_KEY, - new_params.mixnode_rewarded_set_size.to_string(), - ) - } - - if old_params.mixnode_active_set_size != new_params.mixnode_active_set_size { - event = event - .add_attribute( - OLD_MIXNODE_ACTIVE_SET_SIZE_KEY, - old_params.mixnode_active_set_size.to_string(), - ) - .add_attribute( - NEW_MIXNODE_ACTIVE_SET_SIZE_KEY, - new_params.mixnode_active_set_size.to_string(), - ) + if old_params.minimum_mixnode_delegation != new_params.minimum_mixnode_delegation { + if let Some(ref old) = old_params.minimum_mixnode_delegation { + event = event.add_attribute(OLD_MINIMUM_DELEGATION_KEY, old.to_string()) + } else { + event = event.add_attribute(OLD_MINIMUM_DELEGATION_KEY, "None") + } + if let Some(ref new) = new_params.minimum_mixnode_delegation { + event = event.add_attribute(NEW_MINIMUM_DELEGATION_KEY, new.to_string()) + } else { + event = event.add_attribute(NEW_MINIMUM_DELEGATION_KEY, "None") + } } event } -pub fn new_not_found_mix_operator_rewarding_event( - interval_id: u32, - identity: IdentityKeyRef<'_>, -) -> Event { - Event::new(OPERATOR_REWARDING_EVENT_TYPE) - .add_attribute(INTERVAL_ID_KEY, interval_id.to_string()) - .add_attribute(NODE_IDENTITY_KEY, identity) +pub fn new_not_found_mix_operator_rewarding_event(interval: Interval, node_id: NodeId) -> Event { + Event::new(MixnetEventType::MixnodeRewarding) + .add_attribute( + INTERVAL_KEY, + interval.current_epoch_absolute_id().to_string(), + ) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute(NO_REWARD_REASON_KEY, BOND_NOT_FOUND_VALUE) } -pub fn new_too_fresh_bond_mix_operator_rewarding_event( - interval_id: u32, - identity: IdentityKeyRef<'_>, -) -> Event { - Event::new(OPERATOR_REWARDING_EVENT_TYPE) - .add_attribute(INTERVAL_ID_KEY, interval_id.to_string()) - .add_attribute(NODE_IDENTITY_KEY, identity) - .add_attribute(NO_REWARD_REASON_KEY, BOND_TOO_FRESH_VALUE) -} - -pub fn new_zero_uptime_mix_operator_rewarding_event( - interval_id: u32, - identity: IdentityKeyRef<'_>, -) -> Event { - Event::new(OPERATOR_REWARDING_EVENT_TYPE) - .add_attribute(INTERVAL_ID_KEY, interval_id.to_string()) - .add_attribute(NODE_IDENTITY_KEY, identity) - .add_attribute(NO_REWARD_REASON_KEY, ZERO_UPTIME_VALUE) -} - -#[allow(clippy::too_many_arguments)] -pub fn new_mix_operator_rewarding_event( - interval_id: u32, - identity: IdentityKeyRef<'_>, - node_reward_result: NodeRewardResult, - node_pledge: Uint128, - node_delegation: Uint128, -) -> Event { - Event::new(OPERATOR_REWARDING_EVENT_TYPE) - .add_attribute(INTERVAL_ID_KEY, interval_id.to_string()) - .add_attribute(NODE_IDENTITY_KEY, identity) - .add_attribute(TOTAL_PLEDGE_KEY, node_pledge) - .add_attribute(TOTAL_DELEGATIONS_KEY, node_delegation) +pub fn new_zero_uptime_mix_operator_rewarding_event(interval: Interval, node_id: NodeId) -> Event { + Event::new(MixnetEventType::MixnodeRewarding) .add_attribute( - TOTAL_MIXNODE_REWARD_KEY, - node_reward_result.reward().to_string(), + INTERVAL_KEY, + interval.current_epoch_absolute_id().to_string(), ) - .add_attribute(LAMBDA_KEY, node_reward_result.lambda().to_string()) - .add_attribute(SIGMA_KEY, node_reward_result.sigma().to_string()) + .add_attribute(NODE_ID_KEY, node_id.to_string()) + .add_attribute(NO_REWARD_REASON_KEY, ZERO_PERFORMANCE_VALUE) } -pub fn new_mix_delegators_rewarding_event( - interval_id: u32, - identity: IdentityKeyRef<'_>, - delegation_rewards_distributed: Uint128, - further_delegations: bool, +pub fn new_mix_rewarding_event( + interval: Interval, + node_id: NodeId, + reward_distribution: RewardDistribution, ) -> Event { - Event::new(MIX_DELEGATORS_REWARDING_EVENT_TYPE) - .add_attribute(INTERVAL_ID_KEY, interval_id.to_string()) - .add_attribute(NODE_IDENTITY_KEY, identity) + Event::new(MixnetEventType::MixnodeRewarding) + // TODO: to calculate Timmy's (delegator) reward at this time + // emit: + // - unit delegation BEFORE rewarding (to determine Timmy's state before rewarding happened) + // - total delegation BEFORE rewarding .add_attribute( - DISTRIBUTED_DELEGATION_REWARDS_KEY, - delegation_rewards_distributed, + INTERVAL_KEY, + interval.current_epoch_absolute_id().to_string(), + ) + .add_attribute(NODE_ID_KEY, node_id.to_string()) + .add_attribute( + OPERATOR_REWARD_KEY, + reward_distribution.operator.to_string(), ) .add_attribute( - FURTHER_DELEGATIONS_TO_REWARD_KEY, - further_delegations.to_string(), + DELEGATES_REWARD_KEY, + reward_distribution.delegates.to_string(), ) } -// note that when this event is emitted, we'll know the current block height -pub fn new_change_rewarded_set_event( - active_set_size: u32, - rewarded_set_size: u32, - nodes_in_rewarded_set: u32, +pub fn new_advance_epoch_event(interval: Interval, rewarded_nodes: u32) -> Event { + Event::new(MixnetEventType::AdvanceEpoch) + .add_attribute( + NEW_CURRENT_EPOCH_KEY, + interval.current_epoch_absolute_id().to_string(), + ) + .add_attribute(REWARDED_SET_NODES_KEY, rewarded_nodes.to_string()) +} + +pub fn new_pending_epoch_events_execution_event(executed: u32) -> Event { + Event::new(MixnetEventType::ExecutePendingEpochEvents) + .add_attribute(EVENTS_EXECUTED_KEY, executed.to_string()) +} + +pub fn new_pending_interval_events_execution_event(executed: u32) -> Event { + Event::new(MixnetEventType::ExecutePendingIntervalEvents) + .add_attribute(EVENTS_EXECUTED_KEY, executed.to_string()) +} + +pub fn new_reconcile_pending_events() -> Event { + Event::new(MixnetEventType::ReconcilePendingEvents) +} + +pub fn new_interval_config_update_event( + epochs_in_interval: u32, + epoch_duration_secs: u64, + updated_rewarding_params: IntervalRewardParams, ) -> Event { - Event::new(CHANGE_REWARDED_SET_EVENT_TYPE) - .add_attribute(ACTIVE_SET_SIZE_KEY, active_set_size.to_string()) - .add_attribute(REWARDED_SET_SIZE_KEY, rewarded_set_size.to_string()) - .add_attribute(NODES_IN_REWARDED_SET_KEY, nodes_in_rewarded_set.to_string()) + Event::new(MixnetEventType::IntervalConfigUpdate) + .add_attribute( + NEW_EPOCHS_DURATION_SECS_KEY, + epoch_duration_secs.to_string(), + ) + .add_attribute(NEW_EPOCHS_IN_INTERVAL, epochs_in_interval.to_string()) + .add_attribute( + UPDATED_INTERVAL_REWARDING_PARAMS_KEY, + updated_rewarding_params.to_inline_json(), + ) } -pub fn new_advance_interval_event(interval: Interval) -> Event { - Event::new(ADVANCE_INTERVAL_EVENT_TYPE) - .add_attribute(NEW_CURRENT_INTERVAL_KEY, interval.to_string()) -} - -pub fn new_advance_epoch_event(interval: Interval) -> Event { - Event::new(ADVANCE_EPOCH_EVENT_TYPE).add_attribute(NEW_CURRENT_EPOCH_KEY, interval.to_string()) +pub fn new_pending_interval_config_update_event( + epochs_in_interval: u32, + epoch_duration_secs: u64, + approximate_time_remaining_secs: i64, +) -> Event { + Event::new(MixnetEventType::PendingIntervalConfigUpdate) + .add_attribute( + NEW_EPOCHS_DURATION_SECS_KEY, + epoch_duration_secs.to_string(), + ) + .add_attribute(NEW_EPOCHS_IN_INTERVAL, epochs_in_interval.to_string()) + .add_attribute( + APPROXIMATE_TIME_LEFT_SECS_KEY, + approximate_time_remaining_secs.to_string(), + ) } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs index 502d43dc13..ebb5464064 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs @@ -1,3 +1,6 @@ +// Copyright 2021-2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + // due to code generated by JsonSchema #![allow(clippy::field_reassign_with_default)] diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs index 862851ca6c..515c120439 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs @@ -1,12 +1,13 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::pending_events::{PendingEpochEvent, PendingIntervalEvent}; +use crate::{EpochId, IntervalId}; use cosmwasm_std::Env; use schemars::gen::SchemaGenerator; use schemars::schema::{InstanceType, Schema, SchemaObject}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::convert::TryInto; use std::fmt::{Display, Formatter}; use std::time::Duration; use time::OffsetDateTime; @@ -59,12 +60,16 @@ pub(crate) mod string_rfc3339_offset_date_time { } } -#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Serialize)] +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)] pub struct Interval { - id: u32, + id: IntervalId, + epochs_in_interval: u32, + #[serde(with = "string_rfc3339_offset_date_time")] - start: OffsetDateTime, - length: Duration, + current_epoch_start: OffsetDateTime, + current_epoch_id: EpochId, + epoch_length: Duration, + total_elapsed_epochs: EpochId, } impl JsonSchema for Interval { @@ -81,20 +86,46 @@ impl JsonSchema for Interval { let object_validation = schema_object.object(); object_validation .properties - .insert("id".to_owned(), gen.subschema_for::()); + .insert("id".to_owned(), gen.subschema_for::()); object_validation.required.insert("id".to_owned()); + object_validation + .properties + .insert("epochs_in_interval".to_owned(), gen.subschema_for::()); + object_validation + .required + .insert("epochs_in_interval".to_owned()); + // PrimitiveDateTime does not implement JsonSchema. However it has a custom // serialization to string, so we just specify the schema to be String. + object_validation.properties.insert( + "current_epoch_start".to_owned(), + gen.subschema_for::(), + ); object_validation - .properties - .insert("start".to_owned(), gen.subschema_for::()); - object_validation.required.insert("start".to_owned()); + .required + .insert("current_epoch_start".to_owned()); + + object_validation.properties.insert( + "current_epoch_id".to_owned(), + gen.subschema_for::(), + ); + object_validation + .required + .insert("current_epoch_id".to_owned()); object_validation .properties - .insert("length".to_owned(), gen.subschema_for::()); - object_validation.required.insert("length".to_owned()); + .insert("epoch_length".to_owned(), gen.subschema_for::()); + object_validation.required.insert("epoch_length".to_owned()); + + object_validation.properties.insert( + "total_elapsed_epochs".to_owned(), + gen.subschema_for::(), + ); + object_validation + .required + .insert("total_elapsed_epochs".to_owned()); Schema::Object(schema_object) } @@ -102,328 +133,451 @@ impl JsonSchema for Interval { impl Interval { /// Initialize epoch in the contract with default values. - pub fn init_epoch(env: Env) -> Self { + pub fn init_interval(epochs_in_interval: u32, epoch_length: Duration, env: &Env) -> Self { Interval { id: 0, + epochs_in_interval, // I really don't see a way for this to fail, unless the blockchain is lying to us - start: OffsetDateTime::from_unix_timestamp(env.block.time.seconds() as i64) - .expect("Invalid timestamp from env.block.time"), - length: Duration::from_secs(3600), + current_epoch_start: OffsetDateTime::from_unix_timestamp( + env.block.time.seconds() as i64 + ) + .expect("Invalid timestamp from env.block.time"), + current_epoch_id: 0, + epoch_length, + total_elapsed_epochs: 0, } } - pub fn is_over(&self, env: Env) -> bool { - self.end_unix_timestamp() <= env.block.time.seconds() as i64 + pub const fn current_epoch_id(&self) -> EpochId { + self.current_epoch_id } - pub fn in_progress(&self, env: Env) -> bool { - let block_time = env.block.time.seconds() as i64; - self.start_unix_timestamp() <= block_time && block_time < self.end_unix_timestamp() - } - - pub fn update_duration(&mut self, secs: u64) { - self.length = Duration::from_secs(secs); - } - - pub const fn length_secs(&self) -> u64 { - self.length.as_secs() - } - - /// Returns the next interval. - #[must_use] - pub fn next(&self) -> Self { - Interval { - id: self.id + 1, - start: self.end(), - length: self.length, - } - } - - pub fn next_on_chain(&self, env: Env) -> Self { - let start = self - .end() - .max(OffsetDateTime::from_unix_timestamp(env.block.time.seconds() as i64).unwrap()); - Interval { - id: self.id + 1, - start, - length: self.length, - } - } - - /// Returns the last interval. - pub fn previous(&self) -> Option { - if self.id > 0 { - Some(Interval { - id: self.id - 1, - start: self.start - self.length, - length: self.length, - }) - } else { - None - } - } - - /// Determines whether the provided datetime is contained within the interval - /// - /// # Arguments - /// - /// * `datetime`: specified datetime - pub fn contains(&self, datetime: OffsetDateTime) -> bool { - self.start <= datetime && datetime <= self.end() - } - - /// Determines whether the provided unix timestamp is contained within the interval - /// - /// # Arguments - /// - /// * `timestamp`: specified timestamp - pub fn contains_timestamp(&self, timestamp: i64) -> bool { - self.start_unix_timestamp() <= timestamp && timestamp <= self.end_unix_timestamp() - } - - /// Returns new instance of [Interval] such that the provided datetime would be within - /// its duration. - /// - /// # Arguments - /// - /// * `now`: current datetime - pub fn current(&self, now: OffsetDateTime) -> Option { - let mut candidate = *self; - - if now > self.start { - loop { - if candidate.contains(now) { - return Some(candidate); - } - candidate = candidate.next(); - } - } else { - loop { - if candidate.contains(now) { - return Some(candidate); - } - candidate = candidate.previous()?; - } - } - } - - /// Returns new instance of [Interval] such that the provided unix timestamp would be within - /// its duration. - /// - /// # Arguments - /// - /// * `now_unix`: current unix time - pub fn current_with_timestamp(&self, now_unix: i64) -> Option { - let mut candidate = *self; - - if now_unix > self.start_unix_timestamp() { - loop { - if candidate.contains_timestamp(now_unix) { - return Some(candidate); - } - candidate = candidate.next(); - } - } else { - loop { - if candidate.contains_timestamp(now_unix) { - return Some(candidate); - } - candidate = candidate.previous()?; - } - } - } - - /// Checks whether this interval has already finished - /// - /// # Arguments - /// - /// * `now`: current datetime - pub fn has_elapsed(&self, now: OffsetDateTime) -> bool { - self.end() < now - } - - /// Returns id of this interval - pub const fn id(&self) -> u32 { + pub const fn current_interval_id(&self) -> IntervalId { self.id } - /// Determines amount of time left until this interval finishes. - /// - /// # Arguments - /// - /// * `now`: current datetime - pub fn until_end(&self, now: OffsetDateTime) -> Option { - let remaining = self.end() - now; - if remaining.is_negative() { - None + pub const fn epochs_in_interval(&self) -> u32 { + self.epochs_in_interval + } + + pub fn force_change_epochs_in_interval(&mut self, epochs_in_interval: u32) { + self.epochs_in_interval = epochs_in_interval; + if self.current_epoch_id >= epochs_in_interval { + // we have to go to the next interval as we can't + // have the same (interval, epoch) combo as we had in the past + self.id += self.current_epoch_id / epochs_in_interval; + self.current_epoch_id %= epochs_in_interval; + } + } + + pub fn change_epoch_length(&mut self, epoch_length: Duration) { + self.epoch_length = epoch_length + } + + pub const fn total_elapsed_epochs(&self) -> u32 { + self.total_elapsed_epochs + } + + pub const fn current_epoch_absolute_id(&self) -> u32 { + // since we count epochs starting from 0, if n epochs have elapsed, the current one has absolute id of n + self.total_elapsed_epochs + } + + #[inline] + pub fn is_current_epoch_over(&self, env: &Env) -> bool { + self.current_epoch_end_unix_timestamp() <= env.block.time.seconds() as i64 + } + + pub fn secs_until_current_epoch_end(&self, env: &Env) -> i64 { + if self.is_current_epoch_over(env) { + 0 } else { - remaining.try_into().ok() + self.current_epoch_end_unix_timestamp() - env.block.time.seconds() as i64 + } + } + + #[inline] + pub fn is_current_interval_over(&self, env: &Env) -> bool { + self.current_interval_end_unix_timestamp() <= env.block.time.seconds() as i64 + } + + pub fn secs_until_current_interval_end(&self, env: &Env) -> i64 { + if self.is_current_interval_over(env) { + 0 + } else { + self.current_interval_end_unix_timestamp() - env.block.time.seconds() as i64 + } + } + + pub fn current_epoch_in_progress(&self, env: &Env) -> bool { + let block_time = env.block.time.seconds() as i64; + self.current_epoch_start_unix_timestamp() <= block_time + && block_time < self.current_epoch_end_unix_timestamp() + } + + pub fn update_epoch_duration(&mut self, secs: u64) { + self.epoch_length = Duration::from_secs(secs); + } + + pub const fn epoch_length_secs(&self) -> u64 { + self.epoch_length.as_secs() + } + + /// Returns the next epoch. If if would result in advancing the interval, + /// the relevant changes are applied. + #[must_use] + pub fn advance_epoch(&self) -> Self { + // remember we start from 0th epoch, so if we're supposed to have 100 epochs in interval, + // epoch 99 is going to be the last one + if self.current_epoch_id == self.epochs_in_interval - 1 { + Interval { + id: self.id + 1, + epochs_in_interval: self.epochs_in_interval, + current_epoch_start: self.current_epoch_end(), + current_epoch_id: 0, + epoch_length: self.epoch_length, + total_elapsed_epochs: self.total_elapsed_epochs + 1, + } + } else { + Interval { + id: self.id, + epochs_in_interval: self.epochs_in_interval, + current_epoch_start: self.current_epoch_end(), + current_epoch_id: self.current_epoch_id + 1, + epoch_length: self.epoch_length, + total_elapsed_epochs: self.total_elapsed_epochs + 1, + } } } /// Returns the starting datetime of this interval. - pub const fn start(&self) -> OffsetDateTime { - self.start + pub const fn current_epoch_start(&self) -> OffsetDateTime { + self.current_epoch_start } /// Returns the length of this interval. - pub const fn length(&self) -> Duration { - self.length + pub const fn epoch_length(&self) -> Duration { + self.epoch_length } - /// Returns the ending datetime of this interval. - pub fn end(&self) -> OffsetDateTime { - self.start + self.length + /// Returns the ending datetime of the current epoch. + pub fn current_epoch_end(&self) -> OffsetDateTime { + self.current_epoch_start + self.epoch_length } - /// Returns the unix timestamp of the start of this interval. - pub const fn start_unix_timestamp(&self) -> i64 { - self.start().unix_timestamp() + pub fn epochs_until_interval_end(&self) -> u32 { + self.epochs_in_interval - self.current_epoch_id } - /// Returns the unix timestamp of the end of this interval. - pub fn end_unix_timestamp(&self) -> i64 { - self.end().unix_timestamp() + /// Returns the ending datetime of the current interval. + pub fn current_interval_end(&self) -> OffsetDateTime { + self.current_epoch_start + self.epochs_until_interval_end() * self.epoch_length + } + + /// Returns the unix timestamp of the start of the current epoch. + pub const fn current_epoch_start_unix_timestamp(&self) -> i64 { + self.current_epoch_start().unix_timestamp() + } + + /// Returns the unix timestamp of the end of the current epoch. + #[inline] + pub fn current_epoch_end_unix_timestamp(&self) -> i64 { + self.current_epoch_end().unix_timestamp() + } + + /// Returns the unix timestamp of the end of the current interval. + #[inline] + pub fn current_interval_end_unix_timestamp(&self) -> i64 { + self.current_interval_end().unix_timestamp() } } impl Display for Interval { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let length = self.length().as_secs(); + let length = self.epoch_length_secs(); let full_hours = length / 3600; let rem = length % 3600; write!( f, - "Interval {}: {} - {} ({}h {}s)", + "Interval {}: epoch {}/{} (current epoch begun at: {}; epoch lengths: {}h {}s)", self.id, - self.start(), - self.end(), + self.current_epoch_id + 1, + self.epochs_in_interval, + self.current_epoch_start, full_hours, rem ) } } +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct CurrentIntervalResponse { + pub interval: Interval, + pub current_blocktime: u64, + pub is_current_interval_over: bool, + pub is_current_epoch_over: bool, +} + +impl CurrentIntervalResponse { + pub fn new(interval: Interval, env: Env) -> Self { + CurrentIntervalResponse { + interval, + current_blocktime: env.block.time.seconds(), + is_current_interval_over: interval.is_current_interval_over(&env), + is_current_epoch_over: interval.is_current_epoch_over(&env), + } + } + + pub fn time_until_current_epoch_end(&self) -> Duration { + if self.is_current_epoch_over { + Duration::from_secs(0) + } else { + let remaining_secs = + self.interval.current_epoch_end_unix_timestamp() - self.current_blocktime as i64; + // this should never be negative, but better safe than sorry and guard ourselves against that case + if remaining_secs <= 0 { + Duration::from_secs(0) + } else { + Duration::from_secs(remaining_secs as u64) + } + } + } +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct PendingEpochEventsResponse { + pub seconds_until_executable: i64, + pub events: Vec, + pub start_next_after: Option, +} + +impl PendingEpochEventsResponse { + pub fn new( + seconds_until_executable: i64, + events: Vec, + start_next_after: Option, + ) -> Self { + PendingEpochEventsResponse { + seconds_until_executable, + events, + start_next_after, + } + } +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct PendingIntervalEventsResponse { + pub seconds_until_executable: i64, + pub events: Vec, + pub start_next_after: Option, +} + +impl PendingIntervalEventsResponse { + pub fn new( + seconds_until_executable: i64, + events: Vec, + start_next_after: Option, + ) -> Self { + PendingIntervalEventsResponse { + seconds_until_executable, + events, + start_next_after, + } + } +} + #[cfg(test)] mod tests { use super::*; + use cosmwasm_std::testing::mock_env; + use rand_chacha::rand_core::{RngCore, SeedableRng}; #[test] - fn previous() { + fn advancing_epoch() { + // just advancing epoch let interval = Interval { - id: 1, - start: time::macros::datetime!(2021-08-23 12:00 UTC), - length: Duration::from_secs(24 * 60 * 60), + id: 0, + epochs_in_interval: 100, + current_epoch_start: time::macros::datetime!(2021-08-23 12:00 UTC), + current_epoch_id: 23, + epoch_length: Duration::from_secs(60 * 60), + total_elapsed_epochs: 0, }; let expected = Interval { id: 0, - start: time::macros::datetime!(2021-08-22 12:00 UTC), - length: Duration::from_secs(24 * 60 * 60), + epochs_in_interval: 100, + current_epoch_start: time::macros::datetime!(2021-08-23 13:00 UTC), + current_epoch_id: 24, + epoch_length: Duration::from_secs(60 * 60), + total_elapsed_epochs: 1, }; - assert_eq!(expected, interval.previous().unwrap()); + assert_eq!(expected, interval.advance_epoch()); - let genesis_interval = Interval { - id: 0, - start: time::macros::datetime!(2021-08-23 12:00 UTC), - length: Duration::from_secs(24 * 60 * 60), - }; - assert!(genesis_interval.previous().is_none()); - } - - #[test] - fn next() { + // results in advancing interval let interval = Interval { id: 0, - start: time::macros::datetime!(2021-08-23 12:00 UTC), - length: Duration::from_secs(24 * 60 * 60), + epochs_in_interval: 100, + current_epoch_start: time::macros::datetime!(2021-08-23 12:00 UTC), + current_epoch_id: 99, + epoch_length: Duration::from_secs(60 * 60), + total_elapsed_epochs: 42, }; let expected = Interval { id: 1, - start: time::macros::datetime!(2021-08-24 12:00 UTC), - length: Duration::from_secs(24 * 60 * 60), + epochs_in_interval: 100, + current_epoch_start: time::macros::datetime!(2021-08-23 13:00 UTC), + current_epoch_id: 0, + epoch_length: Duration::from_secs(60 * 60), + total_elapsed_epochs: 43, }; - assert_eq!(expected, interval.next()) + assert_eq!(expected, interval.advance_epoch()) } #[test] - fn checking_for_datetime_inclusion() { + fn checking_for_epoch_ends() { + let env = mock_env(); + + // epoch just begun let interval = Interval { - id: 100, - start: time::macros::datetime!(2021-08-23 12:00 UTC), - length: Duration::from_secs(24 * 60 * 60), + id: 0, + epochs_in_interval: 100, + current_epoch_start: OffsetDateTime::from_unix_timestamp( + env.block.time.seconds() as i64 - 100, + ) + .unwrap(), + current_epoch_id: 23, + epoch_length: Duration::from_secs(60 * 60), + total_elapsed_epochs: 0, }; + assert!(!interval.is_current_epoch_over(&env)); - // it must contain its own boundaries - assert!(interval.contains(interval.start)); - assert!(interval.contains(interval.end())); + // current time == current epoch start + let mut interval = interval; + interval.current_epoch_start = + OffsetDateTime::from_unix_timestamp(env.block.time.seconds() as i64).unwrap(); + assert!(!interval.is_current_epoch_over(&env)); - let in_the_midle = interval.start + Duration::from_secs(interval.length.as_secs() / 2); - assert!(interval.contains(in_the_midle)); + // epoch HASN'T yet begun (weird edge case, but can happen if we decide to manually adjust things) + let mut interval = interval; + interval.current_epoch_start = + OffsetDateTime::from_unix_timestamp(env.block.time.seconds() as i64 + 100).unwrap(); + assert!(!interval.is_current_epoch_over(&env)); - assert!(!interval.contains(interval.next().end())); - assert!(!interval.contains(interval.previous().unwrap().start())); + // current_time = EXACTLY end of the epoch + let mut interval = interval; + interval.current_epoch_start = + OffsetDateTime::from_unix_timestamp(env.block.time.seconds() as i64).unwrap() + - interval.epoch_length; + assert!(interval.is_current_epoch_over(&env)); + + // revert time a bit more + interval.current_epoch_start -= Duration::from_secs(42); + assert!(interval.is_current_epoch_over(&env)); + + // revert by A LOT -> epoch still should be in finished state + interval.current_epoch_start -= Duration::from_secs(5 * 31 * 60 * 60); + assert!(interval.is_current_epoch_over(&env)); } #[test] - fn determining_current_interval() { - let first_interval = Interval { - id: 100, - start: time::macros::datetime!(2021-08-23 12:00 UTC), - length: Duration::from_secs(24 * 60 * 60), + fn interval_end() { + let mut interval = Interval { + id: 0, + epochs_in_interval: 100, + current_epoch_start: time::macros::datetime!(2021-08-23 12:00 UTC), + current_epoch_id: 99, + epoch_length: Duration::from_secs(60 * 60), + total_elapsed_epochs: 0, }; - // interval just before - let fake_now = first_interval.start - Duration::from_secs(123); - assert_eq!(first_interval.previous(), first_interval.current(fake_now)); - - // this interval (start boundary) assert_eq!( - first_interval, - first_interval.current(first_interval.start).unwrap() + interval.current_epoch_start + interval.epoch_length, + interval.current_interval_end() ); - // this interval (in the middle) - let fake_now = first_interval.start + Duration::from_secs(123); - assert_eq!(first_interval, first_interval.current(fake_now).unwrap()); - - // this interval (end boundary) + interval.current_epoch_id -= 1; assert_eq!( - first_interval, - first_interval.current(first_interval.end()).unwrap() + interval.current_epoch_start + 2 * interval.epoch_length, + interval.current_interval_end() ); - // next interval - let fake_now = first_interval.end() + Duration::from_secs(123); + interval.current_epoch_id -= 10; assert_eq!( - first_interval.next(), - first_interval.current(fake_now).unwrap() + interval.current_epoch_start + 12 * interval.epoch_length, + interval.current_interval_end() ); - // few intervals in the past - let fake_now = first_interval.start() - - first_interval.length - - first_interval.length - - first_interval.length; + interval.current_epoch_id = 0; assert_eq!( - first_interval - .previous() - .unwrap() - .previous() - .unwrap() - .previous() - .unwrap(), - first_interval.current(fake_now).unwrap() + interval.current_epoch_start + interval.epochs_in_interval * interval.epoch_length, + interval.current_interval_end() ); + } - // few intervals in the future - let fake_now = first_interval.end() - + first_interval.length - + first_interval.length - + first_interval.length; - assert_eq!( - first_interval.next().next().next(), - first_interval.current(fake_now).unwrap() - ); + #[test] + fn checking_for_interval_ends() { + let env = mock_env(); + + let epoch_length = Duration::from_secs(60 * 60); + + let mut interval = Interval { + id: 0, + epochs_in_interval: 100, + current_epoch_start: OffsetDateTime::from_unix_timestamp( + env.block.time.seconds() as i64 + ) + .unwrap(), + current_epoch_id: 98, + epoch_length, + total_elapsed_epochs: 0, + }; + + // current epoch just started (we still have to finish 2 epochs) + assert!(!interval.is_current_interval_over(&env)); + + // still need to finish the 99th epoch + interval.current_epoch_start -= epoch_length; + assert!(!interval.is_current_interval_over(&env)); + + // it JUST finished + interval.current_epoch_start -= epoch_length; + assert!(interval.is_current_interval_over(&env)); + + // nobody updated the interval data, but the current one should still be in finished state + interval.current_epoch_start -= 10 * epoch_length; + assert!(interval.is_current_interval_over(&env)); + } + + #[test] + fn getting_current_full_epoch_id() { + let env = mock_env(); + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let epoch_length = Duration::from_secs(60 * 60); + + let mut interval = Interval::init_interval(100, epoch_length, &env); + + // normal situation + for i in 0u32..2000 { + assert_eq!(interval.current_epoch_absolute_id(), i); + interval = interval.advance_epoch(); + } + + let mut interval = Interval::init_interval(100, epoch_length, &env); + + for i in 0u32..2000 { + // every few epochs decide to change epochs in interval + if i % 7 == 0 { + let new_epochs_in_interval = (rng.next_u32() % 200) + 42; + interval.force_change_epochs_in_interval(new_epochs_in_interval) + } + + // make sure full epoch id is always monotonically increasing + assert_eq!(interval.current_epoch_absolute_id(), i); + + interval = interval.advance_epoch(); + } } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs index bd0c7feeea..5e0bcb34d1 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs @@ -1,6 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +mod constants; pub mod delegation; pub mod error; pub mod events; @@ -8,29 +9,31 @@ pub mod gateway; mod interval; pub mod mixnode; mod msg; +pub mod pending_events; pub mod reward_params; +pub mod rewarding; mod types; -pub const MIXNODE_DELEGATORS_PAGE_LIMIT: usize = 250; - -pub use cosmwasm_std::{Addr, Coin}; +pub use contracts_common::types::*; +pub use cosmwasm_std::{Addr, Coin, Decimal, Fraction}; pub use delegation::{ Delegation, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, - PagedMixDelegationsResponse, + PagedMixNodeDelegationsResponse, }; pub use gateway::{ Gateway, GatewayBond, GatewayBondResponse, GatewayOwnershipResponse, PagedGatewayResponse, }; -pub use interval::Interval; +pub use interval::{ + CurrentIntervalResponse, Interval, PendingEpochEventsResponse, PendingIntervalEventsResponse, +}; pub use mixnode::{ - Layer, MixNode, MixNodeBond, MixOwnershipResponse, MixnodeBondResponse, PagedMixnodeResponse, - RewardedSetNodeStatus, + Layer, MixNode, MixNodeBond, MixNodeConfigUpdate, MixNodeCostParams, MixNodeDetails, + MixNodeRewarding, MixOwnershipResponse, MixnodeDetailsResponse, PagedMixnodeBondsResponse, + RewardedSetNodeStatus, UnbondedMixnode, }; pub use msg::*; +pub use pending_events::{ + PendingEpochEvent, PendingEpochEventData, PendingIntervalEvent, PendingIntervalEventData, +}; +pub use reward_params::{IntervalRewardParams, IntervalRewardingParamsUpdate, RewardingParams}; pub use types::*; - -pub type U128 = fixed::types::U75F53; - -fixed::const_fixed_from_int! { - const ONE: U128 = 1; -} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index d951ec4db2..898cba50cf 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -1,18 +1,19 @@ +// Copyright 2021-2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + // due to code generated by JsonSchema #![allow(clippy::field_reassign_with_default)] +use crate::constants::UNIT_DELEGATION_BASE; use crate::error::MixnetContractError; -use crate::reward_params::RewardParams; -use crate::{Delegation, IdentityKey, SphinxKey}; -use crate::{ONE, U128}; -use az::CheckedCast; -use cosmwasm_std::{coin, Addr, Coin, Uint128}; -use log::error; +use crate::reward_params::{NodeRewardParams, RewardingParams}; +use crate::rewarding::helpers::truncate_reward; +use crate::rewarding::RewardDistribution; +use crate::{Delegation, EpochId, IdentityKey, NodeId, Percent, SphinxKey}; +use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; -use std::cmp::Ordering; -use std::fmt::Display; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( @@ -31,102 +32,511 @@ impl RewardedSetNodeStatus { } } -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub enum DelegationEvent { - Delegate(Delegation), - Undelegate(PendingUndelegate), +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct MixNodeDetails { + pub bond_information: MixNodeBond, + + pub rewarding_details: MixNodeRewarding, } -impl DelegationEvent { - pub fn delegation_amount(&self) -> Option { - match self { - DelegationEvent::Delegate(delegation) => Some(delegation.amount.clone()), - // I think it would be nice to also expose an amount here to know how much we're undelegating - DelegationEvent::Undelegate(_) => None, +impl MixNodeDetails { + pub fn new(bond_information: MixNodeBond, rewarding_details: MixNodeRewarding) -> Self { + MixNodeDetails { + bond_information, + rewarding_details, } } + + pub fn mix_id(&self) -> NodeId { + self.bond_information.id + } + + pub fn is_unbonding(&self) -> bool { + self.bond_information.is_unbonding + } + + pub fn original_pledge(&self) -> &Coin { + &self.bond_information.original_pledge + } + + pub fn pending_operator_reward(&self) -> Coin { + let pledge = self.original_pledge(); + self.rewarding_details.pending_operator_reward(pledge) + } + + pub fn pending_detailed_operator_reward(&self) -> Decimal { + let pledge = self.original_pledge(); + self.rewarding_details + .pending_detailed_operator_reward(pledge) + } + + pub fn total_stake(&self) -> Decimal { + self.rewarding_details.node_bond() + } } -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub struct PendingUndelegate { - mix_identity: IdentityKey, - delegate: Addr, - proxy: Option, - block_height: u64, +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct MixNodeRewarding { + /// Information provided by the operator that influence the cost function. + #[serde(rename = "cp")] + pub cost_params: MixNodeCostParams, + + /// Total pledge and compounded reward earned by the node operator. + #[serde(rename = "op")] + pub operator: Decimal, + + /// Total delegation and compounded reward earned by all node delegators. + #[serde(rename = "dg")] + pub delegates: Decimal, + + /// Cumulative reward earned by the "unit delegation" since the block 0. + #[serde(rename = "tur")] + pub total_unit_reward: Decimal, + + /// Value of the theoretical "unit delegation" that has delegated to this mixnode at block 0. + #[serde(rename = "ud")] + pub unit_delegation: Decimal, + + /// Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt + /// to reward it multiple times in the same epoch. + #[serde(rename = "le")] + pub last_rewarded_epoch: EpochId, + + // technically we don't need that field to determine reward magnitude or anything + // but it saves on extra queries to determine if we're removing the final delegation + // (so that we could zero the field correctly) + #[serde(rename = "uqd")] + pub unique_delegations: u32, } -impl PendingUndelegate { - pub fn new( - mix_identity: IdentityKey, - delegate: Addr, - proxy: Option, - block_height: u64, +impl MixNodeRewarding { + pub fn initialise_new( + cost_params: MixNodeCostParams, + initial_pledge: &Coin, + current_epoch: EpochId, ) -> Self { - Self { - mix_identity, - delegate, - proxy, - block_height, + MixNodeRewarding { + cost_params, + operator: Decimal::from_atomics(initial_pledge.amount, 0).unwrap(), + delegates: Decimal::zero(), + total_unit_reward: Decimal::zero(), + unit_delegation: UNIT_DELEGATION_BASE, + last_rewarded_epoch: current_epoch, + unique_delegations: 0, } } - pub fn mix_identity(&self) -> IdentityKey { - self.mix_identity.clone() + /// 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 + /// have not yet removed their tokens. + pub fn still_bonded(&self) -> bool { + self.operator != Decimal::zero() } - pub fn delegate(&self) -> Addr { - self.delegate.clone() + pub fn pending_operator_reward(&self, original_pledge: &Coin) -> Coin { + let reward_with_pledge = truncate_reward(self.operator, &original_pledge.denom); + Coin { + denom: reward_with_pledge.denom, + amount: reward_with_pledge.amount - original_pledge.amount, + } } - pub fn proxy(&self) -> Option { - self.proxy.clone() + pub fn pending_detailed_operator_reward(&self, original_pledge: &Coin) -> Decimal { + let initial_dec = Decimal::from_atomics(original_pledge.amount, 0).unwrap(); + if initial_dec > self.operator { + panic!( + "seems slashing has occurred while it has not been implemented nor accounted for!" + ) + } + self.operator - initial_dec } - pub fn block_height(&self) -> u64 { - self.block_height + pub fn operator_pledge_with_reward(&self, denom: impl Into) -> Coin { + truncate_reward(self.operator, denom) } - pub fn proxy_storage_key(&self) -> Vec { - if let Some(proxy) = &self.proxy { - self.delegate() - .as_bytes() - .iter() - .zip(proxy.as_bytes()) - .map(|(x, y)| x ^ y) - .collect() + pub fn pending_delegator_reward(&self, delegation: &Delegation) -> Coin { + let delegator_reward = self.determine_delegation_reward(delegation); + truncate_reward(delegator_reward, &delegation.amount.denom) + } + + pub fn withdraw_operator_reward(&mut self, original_pledge: &Coin) -> Coin { + let initial_dec = Decimal::from_atomics(original_pledge.amount, 0).unwrap(); + if initial_dec > self.operator { + panic!( + "seems slashing has occurred while it has not been implemented nor accounted for!" + ) + } + let diff = self.operator - initial_dec; + self.operator = initial_dec; + + truncate_reward(diff, &original_pledge.denom) + } + + pub fn withdraw_delegator_reward( + &mut self, + delegation: &mut Delegation, + ) -> Result { + let reward = self.determine_delegation_reward(delegation); + self.decrease_delegates_decimal(reward)?; + + delegation.cumulative_reward_ratio = self.full_reward_ratio(); + Ok(truncate_reward(reward, &delegation.amount.denom)) + } + + pub fn node_bond(&self) -> Decimal { + self.operator + self.delegates + } + + /// Saturation over the tokens pledged by the node operator. + pub fn pledge_saturation(&self, reward_params: &RewardingParams) -> Decimal { + // make sure our saturation is never greater than 1 + if self.operator > reward_params.interval.stake_saturation_point { + Decimal::one() } else { - self.delegate().as_bytes().to_vec() + self.operator / reward_params.interval.stake_saturation_point } } - pub fn storage_key(&self) -> (IdentityKey, Vec) { - (self.mix_identity(), self.proxy_storage_key()) + /// Saturation over all the tokens staked over this node. + pub fn bond_saturation(&self, reward_params: &RewardingParams) -> Decimal { + // make sure our saturation is never greater than 1 + if self.node_bond() > reward_params.interval.stake_saturation_point { + Decimal::one() + } else { + self.node_bond() / reward_params.interval.stake_saturation_point + } } - pub fn delegation_key(&self, block_height: u64) -> (IdentityKey, Vec, u64) { - (self.mix_identity(), self.proxy_storage_key(), block_height) + pub fn uncapped_bond_saturation(&self, reward_params: &RewardingParams) -> Decimal { + self.node_bond() / reward_params.interval.stake_saturation_point } - pub fn event_storage_key(&self) -> (Vec, u64, IdentityKey) { - ( - self.proxy_storage_key(), - self.block_height(), - self.mix_identity(), - ) + pub fn node_reward( + &self, + reward_params: &RewardingParams, + node_params: NodeRewardParams, + ) -> Decimal { + let work = if node_params.in_active_set { + reward_params.active_node_work() + } else { + reward_params.standby_node_work() + }; + + let alpha = reward_params.interval.sybil_resistance; + + reward_params.interval.epoch_reward_budget + * node_params.performance.value() + * self.bond_saturation(reward_params) + * (work + + alpha.value() * self.pledge_saturation(reward_params) + / reward_params.dec_rewarded_set_size()) + / (Decimal::one() + alpha.value()) + } + + pub fn determine_reward_split( + &self, + node_reward: Decimal, + node_performance: Percent, + // I don't like this argument here, makes things look, idk, messy... + epochs_in_interval: u32, + ) -> RewardDistribution { + let node_cost = + self.cost_params.epoch_operating_cost(epochs_in_interval) * node_performance.value(); + + // check if profit is positive + if node_reward > node_cost { + let profit = node_reward - node_cost; + let profit_margin = self.cost_params.profit_margin_percent.value(); + let one = Decimal::one(); + + let operator_share = self.operator / self.node_bond(); + + let operator = profit * (profit_margin + (one - profit_margin) * operator_share); + let delegates = profit - operator; + + debug_assert_eq!(operator + delegates + node_cost, node_reward); + + RewardDistribution { + operator: operator + node_cost, + delegates, + } + } else { + RewardDistribution { + operator: node_reward, + delegates: Decimal::zero(), + } + } + } + + pub fn calculate_epoch_reward( + &self, + reward_params: &RewardingParams, + node_params: NodeRewardParams, + epochs_in_interval: u32, + ) -> RewardDistribution { + let node_reward = self.node_reward(reward_params, node_params); + self.determine_reward_split(node_reward, node_params.performance, epochs_in_interval) + } + + pub fn distribute_rewards( + &mut self, + distribution: RewardDistribution, + absolute_epoch_id: EpochId, + ) { + let unit_delegation_reward = distribution.delegates + * self.delegator_share(self.unit_delegation + self.total_unit_reward); + + self.operator += distribution.operator; + self.delegates += distribution.delegates; + + // self.current_period_reward += unit_delegation_reward; + self.total_unit_reward += unit_delegation_reward; + self.last_rewarded_epoch = absolute_epoch_id; + } + + pub fn epoch_rewarding( + &mut self, + reward_params: &RewardingParams, + node_params: NodeRewardParams, + epochs_in_interval: u32, + absolute_epoch_id: EpochId, + ) { + let reward_distribution = + self.calculate_epoch_reward(reward_params, node_params, epochs_in_interval); + self.distribute_rewards(reward_distribution, absolute_epoch_id) + } + + pub fn determine_delegation_reward(&self, delegation: &Delegation) -> Decimal { + let starting_ratio = delegation.cumulative_reward_ratio; + let ending_ratio = self.full_reward_ratio(); + let adjust = starting_ratio + UNIT_DELEGATION_BASE; + + (ending_ratio - starting_ratio) * delegation.dec_amount() / adjust + } + + // this updates `unique_delegations` field + pub fn add_base_delegation(&mut self, amount: Uint128) { + self.increase_delegates_uint128(amount); + self.unique_delegations += 1; + } + + pub fn increase_delegates_uint128(&mut self, amount: Uint128) { + // the unwrap here is fine as the value is guaranteed to fit under provided constraints + self.delegates += Decimal::from_atomics(amount, 0).unwrap() + } + + // this updates `unique_delegations` field + // special care must be taken when calling this method as the caller has to ensure + // the corresponding delegation has not accumulated any rewards + pub fn remove_delegation_uint128( + &mut self, + amount: Uint128, + ) -> Result<(), MixnetContractError> { + self.decrease_delegates_uint128(amount)?; + self.decrement_unique_delegations() + } + + pub fn decrease_delegates_uint128( + &mut self, + amount: Uint128, + ) -> Result<(), MixnetContractError> { + let amount_dec = Decimal::from_atomics(amount, 0).unwrap(); + self.decrease_delegates_decimal(amount_dec) + } + + fn decrement_unique_delegations(&mut self) -> Result<(), MixnetContractError> { + if self.unique_delegations == 0 { + return Err(MixnetContractError::OverflowSubtraction { + minuend: 0, + subtrahend: 1, + }); + } + self.unique_delegations -= 1; + Ok(()) + } + + // this updates `unique_delegations` field + pub fn remove_delegation_decimal( + &mut self, + amount: Decimal, + ) -> Result<(), MixnetContractError> { + self.decrease_delegates_decimal(amount)?; + self.decrement_unique_delegations()?; + + // if this was last delegation, move all leftover decimal tokens to the operator + // (this is literally in the order of a millionth of a micronym) + if self.unique_delegations == 0 { + self.operator += self.delegates; + self.delegates = Decimal::zero(); + } + Ok(()) + } + + pub fn undelegate(&mut self, delegation: &Delegation) -> Result { + let reward = self.determine_delegation_reward(delegation); + let full_amount = reward + delegation.dec_amount(); + self.remove_delegation_decimal(full_amount)?; + Ok(truncate_reward(full_amount, &delegation.amount.denom)) + } + + pub fn decrease_delegates_decimal( + &mut self, + amount: Decimal, + ) -> Result<(), MixnetContractError> { + if self.delegates < amount { + return Err(MixnetContractError::OverflowDecimalSubtraction { + minuend: self.delegates, + subtrahend: amount, + }); + } + + self.delegates -= amount; + Ok(()) + } + + pub fn decrease_operator_decimal( + &mut self, + amount: Decimal, + ) -> Result<(), MixnetContractError> { + if self.operator < amount { + return Err(MixnetContractError::OverflowDecimalSubtraction { + minuend: self.operator, + subtrahend: amount, + }); + } + + self.operator -= amount; + Ok(()) + } + + pub fn full_reward_ratio(&self) -> Decimal { + self.total_unit_reward //+ self.current_period_reward + } + + pub fn delegator_share(&self, amount: Decimal) -> Decimal { + if self.delegates.is_zero() { + Decimal::zero() + } else { + amount / self.delegates + } } } -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Serialize, JsonSchema)] +// operator information + data assigned by the contract(s) +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct MixNodeBond { + /// Unique id assigned to the bonded mixnode. + pub id: NodeId, + + /// Address of the owner of this mixnode. + pub owner: Addr, + + /// Original amount pledged by the operator of this node. + pub original_pledge: Coin, + + /// Layer assigned to this mixnode. + pub layer: Layer, + + /// Information provided by the operator for the purposes of bonding. + pub mix_node: MixNode, + + /// Entity who bonded this mixnode on behalf of the owner. + /// If exists, it's most likely the address of the vesting contract. + pub proxy: Option, + + /// Block height at which this mixnode has been bonded. + pub bonding_height: u64, + + /// Flag to indicate whether this node is in the process of unbonding, + /// that will conclude upon the epoch finishing. + pub is_unbonding: bool, +} + +impl MixNodeBond { + pub fn new( + id: NodeId, + owner: Addr, + original_pledge: Coin, + layer: Layer, + mix_node: MixNode, + proxy: Option, + bonding_height: u64, + ) -> Self { + MixNodeBond { + id, + owner, + original_pledge, + layer, + mix_node, + proxy, + bonding_height, + is_unbonding: false, + } + } + + pub fn identity(&self) -> &str { + &self.mix_node.identity_key + } + + pub fn original_pledge(&self) -> &Coin { + &self.original_pledge + } + + pub fn owner(&self) -> &Addr { + &self.owner + } + + pub fn mix_node(&self) -> &MixNode { + &self.mix_node + } +} + +// information provided by the operator +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/Mixnode.ts") +)] pub struct MixNode { + /// Network address of this mixnode, for example 1.1.1.1:1234 or foo.mixnode.com pub host: String, + pub mix_port: u16, + pub verloc_port: u16, + pub http_api_port: u16, + + /// Base58-encoded x25519 public key used for sphinx key derivation. pub sphinx_key: SphinxKey, - /// Base58 encoded ed25519 EdDSA public key. + + /// Base58-encoded ed25519 EdDSA public key. pub identity_key: IdentityKey, + pub version: String, - pub profit_margin_percent: u8, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct MixNodeCostParams { + pub profit_margin_percent: Percent, + + /// Operating cost of the associated mixnode per the entire interval. + pub interval_operating_cost: Coin, +} + +impl MixNodeCostParams { + pub fn to_inline_json(&self) -> String { + serde_json::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) + } +} + +impl MixNodeCostParams { + pub fn epoch_operating_cost(&self, epochs_in_interval: u32) -> Decimal { + Decimal::from_ratio(self.interval_operating_cost.amount, epochs_in_interval) + } } #[derive( @@ -144,7 +554,6 @@ pub struct MixNode { )] #[repr(u8)] pub enum Layer { - Gateway = 0, One = 1, Two = 2, Three = 3, @@ -152,497 +561,100 @@ pub enum Layer { impl From for String { fn from(layer: Layer) -> Self { - if layer == Layer::Gateway { - "gateway".to_string() - } else { - (layer as u8).to_string() - } + (layer as u8).to_string() } } -// cosmwasm's limited serde doesn't work with U128 directly -#[allow(non_snake_case)] -pub mod fixed_U128_as_string { - use super::U128; - use serde::de::Error; - use serde::Deserialize; - #[allow(unused_imports)] - use std::str::FromStr; +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/UnbondedMixnode.ts") +)] +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] +pub struct UnbondedMixnode { + pub identity_key: IdentityKey, - pub fn serialize(val: &U128, serializer: S) -> Result - where - S: serde::Serializer, - { - let s = (*val).to_string(); - serializer.serialize_str(&s) - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - U128::from_str(&s).map_err(|err| { - D::Error::custom(format!( - "failed to deserialize U128 with its string representation - {}", - err - )) - }) - } -} - -// everything required to reward delegator of given mixnode -#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)] -pub struct DelegatorRewardParams { - reward_params: RewardParams, - - // to be completely honest I don't understand all consequences of using `#[schemars(with = "String")]` - // for U128 here, but it seems that CosmWasm is using the same attribute for their Uint128 - #[schemars(with = "String")] - #[serde(with = "fixed_U128_as_string")] - sigma: U128, - #[schemars(with = "String")] - #[serde(with = "fixed_U128_as_string")] - profit_margin: U128, - #[schemars(with = "String")] - #[serde(with = "fixed_U128_as_string")] - node_profit: U128, -} - -impl DelegatorRewardParams { - pub fn new( - sigma: U128, - profit_margin: U128, - node_profit: U128, - reward_params: RewardParams, - ) -> Self { - DelegatorRewardParams { - sigma, - profit_margin, - node_profit, - reward_params, - } - } - - pub fn determine_delegation_reward(&self, delegation_amount: Uint128) -> u128 { - if self.sigma == 0 { - return 0; - } - - // change all values into their fixed representations - let delegation_amount = U128::from_num(delegation_amount.u128()); - let staking_supply = U128::from_num(self.reward_params.staking_supply()); - - let scaled_delegation_amount = delegation_amount / staking_supply; - - // Div by zero checked above - let delegator_reward = - (ONE - self.profit_margin) * (scaled_delegation_amount / self.sigma) * self.node_profit; - - let reward = delegator_reward.max(U128::ZERO); - if let Some(int_reward) = reward.checked_cast() { - int_reward - } else { - error!( - "Could not cast delegator reward ({}) to u128, returning 0", - reward, - ); - 0u128 - } - } - - pub fn node_reward_params(&self) -> RewardParams { - self.reward_params - } -} - -#[derive(Debug, Clone, JsonSchema, PartialEq, Eq, Serialize, Deserialize, Copy)] -pub struct StoredNodeRewardResult { - reward: Uint128, - - #[schemars(with = "String")] - #[serde(with = "fixed_U128_as_string")] - lambda: U128, - - #[schemars(with = "String")] - #[serde(with = "fixed_U128_as_string")] - sigma: U128, -} - -impl StoredNodeRewardResult { - pub fn reward(&self) -> Uint128 { - self.reward - } - - pub fn lambda(&self) -> U128 { - self.lambda - } - - pub fn sigma(&self) -> U128 { - self.sigma - } -} - -impl TryFrom for StoredNodeRewardResult { - type Error = MixnetContractError; - - fn try_from(node_reward_result: NodeRewardResult) -> Result { - Ok(StoredNodeRewardResult { - reward: Uint128::new( - node_reward_result - .reward() - .checked_cast() - .ok_or(MixnetContractError::CastError)?, - ), - lambda: node_reward_result.lambda(), - sigma: node_reward_result.sigma(), - }) - } -} - -#[derive(Debug, Copy, Clone)] -pub struct NodeRewardResult { - reward: U128, - lambda: U128, - sigma: U128, -} - -impl NodeRewardResult { - pub fn reward(&self) -> U128 { - self.reward - } - - pub fn lambda(&self) -> U128 { - self.lambda - } - - pub fn sigma(&self) -> U128 { - self.sigma - } -} - -pub struct RewardEstimate { - pub total_node_reward: u64, - pub operator_reward: u64, - pub delegators_reward: u64, - pub node_profit: u64, - pub operator_cost: u64, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] -pub struct MixNodeBond { - pub pledge_amount: Coin, - pub total_delegation: Coin, + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] pub owner: Addr, - pub layer: Layer, - pub block_height: u64, - pub mix_node: MixNode, + + #[cfg_attr(feature = "generate-ts", ts(type = "string | null"))] pub proxy: Option, - pub accumulated_rewards: Option, + + pub unbonding_height: u64, } -impl MixNodeBond { - pub fn new( - pledge_amount: Coin, - owner: Addr, - layer: Layer, - block_height: u64, - mix_node: MixNode, - proxy: Option, - ) -> Self { - MixNodeBond { - total_delegation: coin(0, &pledge_amount.denom), - pledge_amount, - owner, - layer, - block_height, - mix_node, - proxy, - accumulated_rewards: None, - } - } - - pub fn accumulated_rewards(&self) -> Uint128 { - self.accumulated_rewards.unwrap_or_else(Uint128::zero) - } - - pub fn profit_margin(&self) -> U128 { - U128::from_num(self.mix_node.profit_margin_percent) / U128::from_num(100) - } - - pub fn identity(&self) -> &String { - &self.mix_node.identity_key - } - - pub fn pledge_amount(&self) -> Coin { - self.pledge_amount.clone() - } - - pub fn owner(&self) -> &Addr { - &self.owner - } - - pub fn mix_node(&self) -> &MixNode { - &self.mix_node - } - - // Takes into account accumulated rewards as well as current pledge and delegation amounts - pub fn total_bond(&self) -> Option { - if self.pledge_amount.denom != self.total_delegation.denom { - None - } else { - Some( - self.pledge_amount.amount.u128() - + self.total_delegation.amount.u128() - + self.accumulated_rewards().u128(), - ) - } - } - - pub fn total_delegation(&self) -> Coin { - self.total_delegation.clone() - } - - pub fn stake_saturation(&self, staking_supply: u128, rewarded_set_size: u32) -> U128 { - self.total_bond_to_staking_supply(staking_supply) * U128::from_num(rewarded_set_size) - } - - // TODO: There is an effect here when adding accumulted rewards to the total bond, ie accumulated rewards will not - // affect lambda, but will affect sigma, in turn over time, if left unclaimed operator rewards will not compound, but - // behave similarly to delegations. - // The question is should this be taken into account when calculating operator rewards? - pub fn pledge_to_staking_supply(&self, staking_supply: u128) -> U128 { - U128::from_num(self.pledge_amount().amount.u128()) / U128::from_num(staking_supply) - } - - pub fn total_bond_to_staking_supply(&self, staking_supply: u128) -> U128 { - U128::from_num(self.pledge_amount().amount.u128() + self.total_delegation().amount.u128()) - / U128::from_num(staking_supply) - } - - pub fn lambda_ticked(&self, params: &RewardParams) -> U128 { - // Ratio of a bond to the token circulating supply - self.lambda(params).min(params.one_over_k()) - } - - pub fn lambda(&self, params: &RewardParams) -> U128 { - // Ratio of a bond to the token circulating supply - self.pledge_to_staking_supply(params.staking_supply()) - } - - pub fn sigma_ticked(&self, params: &RewardParams) -> U128 { - // Ratio of a delegation to the the token circulating supply - self.sigma(params).min(params.one_over_k()) - } - - pub fn sigma(&self, params: &RewardParams) -> U128 { - // Ratio of a delegation to the the token circulating supply - self.total_bond_to_staking_supply(params.staking_supply()) - } - - pub fn estimate_reward( - &self, - base_operator_cost: u64, - params: &RewardParams, - ) -> Result { - let total_node_reward = self - .reward(params) - .reward() - .checked_to_num::() - .unwrap_or_default(); - let node_profit = self - .node_profit(params, base_operator_cost) - .checked_to_num::() - .unwrap_or_default(); - let operator_cost = params - .node - .operator_cost(base_operator_cost) - .checked_to_num::() - .unwrap_or_default(); - let operator_reward = self.operator_reward(params, base_operator_cost); - // Total reward has to be the sum of operator and delegator rewards - let delegators_reward = node_profit.saturating_sub(operator_reward); - - Ok(RewardEstimate { - total_node_reward: total_node_reward.try_into()?, - operator_reward: operator_reward.try_into()?, - delegators_reward: delegators_reward.try_into()?, - node_profit: node_profit.try_into()?, - operator_cost: operator_cost.try_into()?, - }) - } - - // keybase://chat/nymtech#dev-core/14473 - pub fn reward(&self, params: &RewardParams) -> NodeRewardResult { - let lambda_ticked = self.lambda_ticked(params); - let sigma_ticked = self.sigma_ticked(params); - - let reward = params.performance() - * params.epoch_reward_pool() - * (sigma_ticked * params.omega() - + params.alpha() * lambda_ticked * sigma_ticked * params.rewarded_set_size()) - / (ONE + params.alpha()); - - // we only need regular lambda and sigma to calculate operator and delegator rewards - NodeRewardResult { - reward, - lambda: self.lambda(params), - sigma: self.sigma(params), - } - } - - pub fn node_profit(&self, params: &RewardParams, base_operator_cost: u64) -> U128 { - self.reward(params) - .reward() - .saturating_sub(params.node.operator_cost(base_operator_cost)) - } - - pub fn operator_reward(&self, params: &RewardParams, base_operator_cost: u64) -> u128 { - let reward = self.reward(params); - if reward.sigma == 0u128 { - return 0; - } - - let profit = reward - .reward - .saturating_sub(params.node.operator_cost(base_operator_cost)); - - let operator_base_reward = reward - .reward - .min(params.node.operator_cost(base_operator_cost)); - // Div by zero checked above - let operator_reward = (self.profit_margin() - + (ONE - self.profit_margin()) * reward.lambda / reward.sigma) - * profit; - - let reward = (operator_reward + operator_base_reward).max(U128::from_num(0)); - - if let Some(int_reward) = reward.checked_cast() { - int_reward - } else { - error!( - "Could not cast reward ({}) to u128, returning 0 - mixnode {}", - reward, - self.identity() - ); - 0u128 - } - } - - pub fn sigma_ratio(&self, params: &RewardParams) -> U128 { - if self.total_bond_to_staking_supply(params.staking_supply()) < params.one_over_k() { - self.total_bond_to_staking_supply(params.staking_supply()) - } else { - params.one_over_k() - } - } - - pub fn reward_delegation( - &self, - delegation_amount: Uint128, - params: &RewardParams, - base_operator_cost: u64, - ) -> u128 { - let reward_params = DelegatorRewardParams::new( - self.sigma(params), - self.profit_margin(), - self.node_profit(params, base_operator_cost), - params.to_owned(), - ); - reward_params.determine_delegation_reward(delegation_amount) - } +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/MixNodeConfigUpdate.ts") +)] +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] +pub struct MixNodeConfigUpdate { + pub host: String, + pub mix_port: u16, + pub verloc_port: u16, + pub http_api_port: u16, + pub version: String, } -impl PartialOrd for MixNodeBond { - fn partial_cmp(&self, other: &Self) -> Option { - // first remove invalid cases - if self.pledge_amount.denom != self.total_delegation.denom { - return None; - } - - if other.pledge_amount.denom != other.total_delegation.denom { - return None; - } - - if self.pledge_amount.denom != other.pledge_amount.denom { - return None; - } - - // try to order by total bond + delegation - let total_cmp = (self.pledge_amount.amount + self.total_delegation.amount) - .partial_cmp(&(self.pledge_amount.amount + self.total_delegation.amount))?; - - if total_cmp != Ordering::Equal { - return Some(total_cmp); - } - - // then if those are equal, prefer higher bond over delegation - let pledge_cmp = self - .pledge_amount - .amount - .partial_cmp(&other.pledge_amount.amount)?; - if pledge_cmp != Ordering::Equal { - return Some(pledge_cmp); - } - - // then look at delegation (I'm not sure we can get here, but better safe than sorry) - let delegation_cmp = self - .total_delegation - .amount - .partial_cmp(&other.total_delegation.amount)?; - if delegation_cmp != Ordering::Equal { - return Some(delegation_cmp); - } - - // then check block height - let height_cmp = self.block_height.partial_cmp(&other.block_height)?; - if height_cmp != Ordering::Equal { - return Some(height_cmp); - } - - // finally go by the rest of the fields in order. It doesn't really matter at this point - // but we should be deterministic. - let owner_cmp = self.owner.partial_cmp(&other.owner)?; - if owner_cmp != Ordering::Equal { - return Some(owner_cmp); - } - - let layer_cmp = self.layer.partial_cmp(&other.layer)?; - if layer_cmp != Ordering::Equal { - return Some(layer_cmp); - } - - self.mix_node.partial_cmp(&other.mix_node) - } -} - -impl Display for MixNodeBond { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "amount: {} {}, owner: {}, identity: {}", - self.pledge_amount.amount, - self.pledge_amount.denom, - self.owner, - self.mix_node.identity_key - ) +impl MixNodeConfigUpdate { + pub fn to_inline_json(&self) -> String { + serde_json::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] -pub struct PagedMixnodeResponse { +pub struct PagedMixnodeBondsResponse { pub nodes: Vec, pub per_page: usize, - pub start_next_after: Option, + pub start_next_after: Option, } -impl PagedMixnodeResponse { +impl PagedMixnodeBondsResponse { + pub fn new(nodes: Vec, per_page: usize, start_next_after: Option) -> Self { + PagedMixnodeBondsResponse { + nodes, + per_page, + start_next_after, + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct PagedMixnodesDetailsResponse { + pub nodes: Vec, + pub per_page: usize, + pub start_next_after: Option, +} + +impl PagedMixnodesDetailsResponse { pub fn new( - nodes: Vec, + nodes: Vec, per_page: usize, - start_next_after: Option, + start_next_after: Option, ) -> Self { - PagedMixnodeResponse { + PagedMixnodesDetailsResponse { + nodes, + per_page, + start_next_after, + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] +pub struct PagedUnbondedMixnodesResponse { + pub nodes: Vec<(NodeId, UnbondedMixnode)>, + pub per_page: usize, + pub start_next_after: Option, +} + +impl PagedUnbondedMixnodesResponse { + pub fn new( + nodes: Vec<(NodeId, UnbondedMixnode)>, + per_page: usize, + start_next_after: Option, + ) -> Self { + PagedUnbondedMixnodesResponse { nodes, per_page, start_next_after, @@ -653,114 +665,30 @@ impl PagedMixnodeResponse { #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct MixOwnershipResponse { pub address: Addr, - pub mixnode: Option, + pub mixnode_details: Option, } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] -pub struct MixnodeBondResponse { - pub identity: IdentityKey, - pub mixnode: Option, +pub struct MixnodeDetailsResponse { + pub mix_id: NodeId, + pub mixnode_details: Option, } -#[cfg(test)] -mod tests { - use super::*; - - fn mixnode_fixture() -> MixNode { - MixNode { - host: "1.1.1.1".to_string(), - mix_port: 123, - verloc_port: 456, - http_api_port: 789, - sphinx_key: "sphinxkey".to_string(), - identity_key: "identitykey".to_string(), - version: "0.11.0".to_string(), - profit_margin_percent: 10, - } - } - - #[test] - fn mixnode_bond_partial_ord() { - let _150foos = Coin::new(150, "foo"); - let _50foos = Coin::new(50, "foo"); - let _0foos = Coin::new(0, "foo"); - - let mix1 = MixNodeBond { - pledge_amount: _150foos.clone(), - total_delegation: _50foos.clone(), - owner: Addr::unchecked("foo1"), - layer: Layer::One, - block_height: 100, - mix_node: mixnode_fixture(), - proxy: None, - accumulated_rewards: Some(Uint128::zero()), - }; - - let mix2 = MixNodeBond { - pledge_amount: _150foos.clone(), - total_delegation: _50foos.clone(), - owner: Addr::unchecked("foo2"), - layer: Layer::One, - block_height: 120, - mix_node: mixnode_fixture(), - proxy: None, - accumulated_rewards: Some(Uint128::zero()), - }; - - let mix3 = MixNodeBond { - pledge_amount: _50foos, - total_delegation: _150foos.clone(), - owner: Addr::unchecked("foo3"), - layer: Layer::One, - block_height: 120, - mix_node: mixnode_fixture(), - proxy: None, - accumulated_rewards: Some(Uint128::zero()), - }; - - let mix4 = MixNodeBond { - pledge_amount: _150foos.clone(), - total_delegation: _0foos.clone(), - owner: Addr::unchecked("foo4"), - layer: Layer::One, - block_height: 120, - mix_node: mixnode_fixture(), - proxy: None, - accumulated_rewards: Some(Uint128::zero()), - }; - - let mix5 = MixNodeBond { - pledge_amount: _0foos, - total_delegation: _150foos, - owner: Addr::unchecked("foo5"), - layer: Layer::One, - block_height: 120, - mix_node: mixnode_fixture(), - proxy: None, - accumulated_rewards: Some(Uint128::zero()), - }; - - // summary: - // mix1: 150bond + 50delegation, foo1, 100 - // mix2: 150bond + 50delegation, foo2, 120 - // mix3: 50bond + 150delegation, foo3, 120 - // mix4: 150bond + 0delegation, foo4, 120 - // mix5: 0bond + 150delegation, foo5, 120 - - // highest total bond+delegation is used - // then bond followed by delegation - // finally just the rest of the fields - - // mix1 has higher total than mix4 or mix5 - assert!(mix1 > mix4); - assert!(mix1 > mix5); - - // mix1 has the same total as mix3, however, mix1 has more tokens in bond - assert!(mix1 > mix3); - // same case for mix4 and mix5 - assert!(mix4 > mix5); - - // same bond and delegation, so it's just ordered by height - assert!(mix1 < mix2); - } +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct MixnodeRewardingDetailsResponse { + pub mix_id: NodeId, + pub rewarding_details: Option, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] +pub struct UnbondedMixnodeResponse { + pub mix_id: NodeId, + pub unbonded_info: Option, +} + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] +pub struct StakeSaturationResponse { + pub mix_id: NodeId, + pub current_saturation: Option, + pub uncapped_saturation: Option, } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index bf1d0d5916..0e7a5dea62 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -1,160 +1,342 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::reward_params::NodeRewardParams; -use crate::ContractStateParams; +use crate::delegation::OwnerProxySubKey; +use crate::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; +use crate::reward_params::{ + IntervalRewardParams, IntervalRewardingParamsUpdate, Performance, RewardingParams, +}; +use crate::{delegation, ContractStateParams, NodeId, Percent}; use crate::{Gateway, IdentityKey, MixNode}; +use cosmwasm_std::Decimal; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::time::Duration; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] pub struct InstantiateMsg { pub rewarding_validator_address: String, - pub mixnet_denom: String, + pub vesting_contract_address: String, + + pub rewarding_denom: String, + pub epochs_in_interval: u32, + pub epoch_duration: Duration, + pub initial_rewarding_params: InitialRewardingParams, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] +pub struct InitialRewardingParams { + pub initial_reward_pool: Decimal, + pub initial_staking_supply: Decimal, + + pub sybil_resistance: Percent, + pub active_set_work_factor: Decimal, + pub interval_pool_emission: Percent, + + pub rewarded_set_size: u32, + pub active_set_size: u32, +} + +impl InitialRewardingParams { + pub fn into_rewarding_params(self, epochs_in_interval: u32) -> RewardingParams { + let epoch_reward_budget = self.initial_reward_pool + / Decimal::from_atomics(epochs_in_interval, 0).unwrap() + * self.interval_pool_emission; + let stake_saturation_point = + self.initial_staking_supply / Decimal::from_atomics(self.rewarded_set_size, 0).unwrap(); + + RewardingParams { + interval: IntervalRewardParams { + reward_pool: self.initial_reward_pool, + staking_supply: self.initial_staking_supply, + epoch_reward_budget, + stake_saturation_point, + sybil_resistance: self.sybil_resistance, + active_set_work_factor: self.active_set_work_factor, + interval_pool_emission: self.interval_pool_emission, + }, + rewarded_set_size: self.rewarded_set_size, + active_set_size: self.active_set_size, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] pub enum ExecuteMsg { + // state/sys-params-related UpdateRewardingValidatorAddress { address: String, }, - InitEpoch {}, - ReconcileDelegations {}, - CheckpointMixnodes {}, - CompoundOperatorRewardOnBehalf { - owner: String, + UpdateContractStateParams { + updated_parameters: ContractStateParams, }, - CompoundDelegatorRewardOnBehalf { - owner: String, - mix_identity: IdentityKey, + UpdateActiveSetSize { + active_set_size: u32, + force_immediately: bool, }, - CompoundOperatorReward {}, - CompoundDelegatorReward { - mix_identity: IdentityKey, + UpdateRewardingParams { + updated_params: IntervalRewardingParamsUpdate, + force_immediately: bool, }, - CompoundReward { - operator: Option, - delegator: Option, - mix_identity: Option, - proxy: Option, + UpdateIntervalConfig { + epochs_in_interval: u32, + epoch_duration_secs: u64, + force_immediately: bool, }, + AdvanceCurrentEpoch { + new_rewarded_set: Vec, + expected_active_set_size: u32, + }, + ReconcileEpochEvents { + limit: Option, + }, + + // mixnode-related: BondMixnode { mix_node: MixNode, + cost_params: MixNodeCostParams, owner_signature: String, }, - UnbondMixnode {}, - UpdateMixnodeConfig { - profit_margin_percent: u8, - }, - UpdateMixnodeConfigOnBehalf { - profit_margin_percent: u8, - owner: String, - }, - BondGateway { - gateway: Gateway, - owner_signature: String, - }, - UnbondGateway {}, - UpdateContractStateParams(ContractStateParams), - - DelegateToMixnode { - mix_identity: IdentityKey, - }, - - UndelegateFromMixnode { - mix_identity: IdentityKey, - }, - - RewardMixnode { - identity: IdentityKey, - // percentage value in range 0-100 - params: NodeRewardParams, - }, - // RewardNextMixDelegators { - // mix_identity: IdentityKey, - // // id of the current rewarding interval - // interval_id: u32, - // }, - DelegateToMixnodeOnBehalf { - mix_identity: IdentityKey, - delegate: String, - }, - UndelegateFromMixnodeOnBehalf { - mix_identity: IdentityKey, - delegate: String, - }, BondMixnodeOnBehalf { mix_node: MixNode, - owner: String, + cost_params: MixNodeCostParams, owner_signature: String, + owner: String, }, + UnbondMixnode {}, UnbondMixnodeOnBehalf { owner: String, }, + UpdateMixnodeCostParams { + new_costs: MixNodeCostParams, + }, + UpdateMixnodeCostParamsOnBehalf { + new_costs: MixNodeCostParams, + owner: String, + }, + UpdateMixnodeConfig { + new_config: MixNodeConfigUpdate, + }, + UpdateMixnodeConfigOnBehalf { + new_config: MixNodeConfigUpdate, + owner: String, + }, + + // gateway-related: + BondGateway { + gateway: Gateway, + owner_signature: String, + }, BondGatewayOnBehalf { gateway: Gateway, owner: String, owner_signature: String, }, + UnbondGateway {}, UnbondGatewayOnBehalf { owner: String, }, - WriteRewardedSet { - rewarded_set: Vec, - expected_active_set_size: u32, + + // delegation-related: + DelegateToMixnode { + mix_id: NodeId, }, - // AdvanceCurrentInterval {}, - AdvanceCurrentEpoch {}, - ClaimOperatorReward {}, - ClaimOperatorRewardOnBehalf { + DelegateToMixnodeOnBehalf { + mix_id: NodeId, + delegate: String, + }, + UndelegateFromMixnode { + mix_id: NodeId, + }, + UndelegateFromMixnodeOnBehalf { + mix_id: NodeId, + delegate: String, + }, + + // reward-related + RewardMixnode { + mix_id: NodeId, + performance: Performance, + }, + WithdrawOperatorReward {}, + WithdrawOperatorRewardOnBehalf { owner: String, }, - ClaimDelegatorReward { - mix_identity: IdentityKey, + WithdrawDelegatorReward { + mix_id: NodeId, }, - ClaimDelegatorRewardOnBehalf { - mix_identity: IdentityKey, + WithdrawDelegatorRewardOnBehalf { + mix_id: NodeId, owner: String, }, } +impl ExecuteMsg { + pub fn default_memo(&self) -> String { + match self { + ExecuteMsg::UpdateRewardingValidatorAddress { address } => { + format!("updating rewarding validator to {}", address) + } + ExecuteMsg::UpdateContractStateParams { .. } => { + "updating mixnet state parameters".into() + } + ExecuteMsg::UpdateActiveSetSize { + active_set_size, + force_immediately, + } => format!( + "updating active set size to {}. forced: {}", + active_set_size, force_immediately + ), + ExecuteMsg::UpdateRewardingParams { + force_immediately, .. + } => format!( + "updating mixnet rewarding parameters. forced: {}", + force_immediately + ), + ExecuteMsg::UpdateIntervalConfig { + force_immediately, .. + } => format!( + "updating mixnet interval configuration. forced: {}", + force_immediately + ), + ExecuteMsg::AdvanceCurrentEpoch { .. } => "advancing current epoch".into(), + ExecuteMsg::ReconcileEpochEvents { .. } => "reconciling epoch events".into(), + ExecuteMsg::BondMixnode { mix_node, .. } => { + format!("bonding mixnode {}", mix_node.identity_key) + } + ExecuteMsg::BondMixnodeOnBehalf { mix_node, .. } => { + format!("bonding mixnode {} on behalf", mix_node.identity_key) + } + ExecuteMsg::UnbondMixnode { .. } => "unbonding mixnode".into(), + ExecuteMsg::UnbondMixnodeOnBehalf { .. } => "unbonding mixnode on behalf".into(), + ExecuteMsg::UpdateMixnodeCostParams { .. } => "updating mixnode cost parameters".into(), + ExecuteMsg::UpdateMixnodeCostParamsOnBehalf { .. } => { + "updating mixnode cost parameters on behalf".into() + } + ExecuteMsg::UpdateMixnodeConfig { .. } => "updating mixnode configuration".into(), + ExecuteMsg::UpdateMixnodeConfigOnBehalf { .. } => { + "updating mixnode configuration on behalf".into() + } + ExecuteMsg::BondGateway { gateway, .. } => { + format!("bonding gateway {}", gateway.identity_key) + } + ExecuteMsg::BondGatewayOnBehalf { gateway, .. } => { + format!("bonding gateway {} on behalf", gateway.identity_key) + } + ExecuteMsg::UnbondGateway { .. } => "unbonding gateway".into(), + ExecuteMsg::UnbondGatewayOnBehalf { .. } => "unbonding gateway on behalf".into(), + ExecuteMsg::DelegateToMixnode { mix_id } => format!("delegating to mixnode {}", mix_id), + ExecuteMsg::DelegateToMixnodeOnBehalf { mix_id, .. } => { + format!("delegating to mixnode {} on behalf", mix_id) + } + ExecuteMsg::UndelegateFromMixnode { mix_id } => { + format!("removing delegation from mixnode {}", mix_id) + } + ExecuteMsg::UndelegateFromMixnodeOnBehalf { mix_id, .. } => { + format!("removing delegation from mixnode {} on behalf", mix_id) + } + ExecuteMsg::RewardMixnode { + mix_id, + performance, + } => format!( + "rewarding mixnode {} for performance {}", + mix_id, performance + ), + ExecuteMsg::WithdrawOperatorReward { .. } => "withdrawing operator reward".into(), + ExecuteMsg::WithdrawOperatorRewardOnBehalf { .. } => { + "withdrawing operator reward on behalf".into() + } + ExecuteMsg::WithdrawDelegatorReward { mix_id } => { + format!("withdrawing delegator reward from mixnode {}", mix_id) + } + ExecuteMsg::WithdrawDelegatorRewardOnBehalf { mix_id, .. } => format!( + "withdrawing delegator reward from mixnode {} on behalf", + mix_id + ), + } + } +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryMsg { - GetBlacklistedNodes {}, - GetCurrentOperatorCost {}, - GetRewardingValidatorAddress {}, - GetAllDelegationKeys {}, - DebugGetAllDelegationValues {}, + // state/sys-params-related GetContractVersion {}, - GetMixNodes { + GetRewardingValidatorAddress {}, + GetStateParams {}, + GetState {}, + GetRewardingParams {}, + GetCurrentIntervalDetails {}, + GetRewardedSet { limit: Option, - start_after: Option, + start_after: Option, }, + + // mixnode-related: + GetMixNodeBonds { + limit: Option, + start_after: Option, + }, + GetMixNodesDetailed { + limit: Option, + start_after: Option, + }, + GetUnbondedMixNodes { + limit: Option, + start_after: Option, + }, + GetUnbondedMixNodesByOwner { + owner: String, + limit: Option, + start_after: Option, + }, + GetUnbondedMixNodesByIdentityKey { + identity_key: String, + limit: Option, + start_after: Option, + }, + GetOwnedMixnode { + address: String, + }, + GetMixnodeDetails { + mix_id: NodeId, + }, + GetMixnodeRewardingDetails { + mix_id: NodeId, + }, + GetStakeSaturation { + mix_id: NodeId, + }, + GetUnbondedMixNodeInformation { + mix_id: NodeId, + }, + GetBondedMixnodeDetailsByIdentity { + mix_identity: IdentityKey, + }, + GetLayerDistribution {}, + + // gateway-related: GetGateways { start_after: Option, limit: Option, }, - OwnsMixnode { - address: String, - }, - OwnsGateway { - address: String, - }, - GetMixnodeBond { - identity: IdentityKey, - }, GetGatewayBond { identity: IdentityKey, }, - StateParams {}, + GetOwnedGateway { + address: String, + }, + + // delegation-related: // gets all [paged] delegations associated with particular mixnode GetMixnodeDelegations { - mix_identity: IdentityKey, + mix_id: NodeId, // since `start_after` is user-provided input, we can't use `Addr` as we // can't guarantee it's validated. - start_after: Option<(String, u64)>, + start_after: Option, limit: Option, }, // gets all [paged] delegations associated with particular delegator @@ -162,81 +344,56 @@ pub enum QueryMsg { // since `delegator` is user-provided input, we can't use `Addr` as we // can't guarantee it's validated. delegator: String, - start_after: Option, + start_after: Option<(NodeId, OwnerProxySubKey)>, limit: Option, }, // gets delegation associated with particular mixnode, delegator pair GetDelegationDetails { - mix_identity: IdentityKey, + mix_id: NodeId, delegator: String, proxy: Option, }, - LayerDistribution {}, - GetRewardPool {}, - GetCirculatingSupply {}, - GetStakingSupply {}, - GetIntervalRewardPercent {}, - GetSybilResistancePercent {}, - GetActiveSetWorkFactor {}, - GetRewardingStatus { - mix_identity: IdentityKey, - interval_id: u32, - }, - GetRewardedSet { - height: Option, - start_after: Option, + // gets all delegations in the system + GetAllDelegations { + start_after: Option, limit: Option, }, - GetRewardedSetUpdateDetails {}, - GetCurrentRewardedSetHeight {}, - GetRewardedSetRefreshBlocks {}, - GetCurrentEpoch {}, - GetEpochsInInterval {}, - QueryOperatorReward { + + // rewards related + GetPendingOperatorReward { address: String, }, - QueryDelegatorReward { + GetPendingMixNodeOperatorReward { + mix_id: NodeId, + }, + GetPendingDelegatorReward { address: String, - mix_identity: IdentityKey, + mix_id: NodeId, proxy: Option, }, - GetPendingDelegationEvents { - owner_address: String, - proxy_address: Option, + // given the provided performance, estimate the reward at the end of the current epoch + GetEstimatedCurrentEpochOperatorReward { + mix_id: NodeId, + estimated_performance: Performance, }, - GetCheckpointsForMixnode { - mix_identity: IdentityKey, + GetEstimatedCurrentEpochDelegatorReward { + address: String, + mix_id: NodeId, + proxy: Option, + estimated_performance: Performance, }, - GetMixnodeAtHeight { - mix_identity: IdentityKey, - height: u64, + + // interval-related + GetPendingEpochEvents { + limit: Option, + start_after: Option, + }, + GetPendingIntervalEvents { + limit: Option, + start_after: Option, }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub struct MigrateMsg { - pub nodes_to_remove: Option>, -} - -impl MigrateMsg { - pub fn nodes_to_remove(&self) -> Vec { - self.nodes_to_remove.clone().unwrap_or_default() - } -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] -pub struct NodeToRemove { - owner: String, - proxy: Option, -} - -impl NodeToRemove { - pub fn owner(&self) -> &str { - &self.owner - } - - pub fn proxy(&self) -> Option<&String> { - self.proxy.as_ref() - } -} +pub struct MigrateMsg {} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs new file mode 100644 index 0000000000..a2f749f068 --- /dev/null +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs @@ -0,0 +1,77 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::mixnode::MixNodeCostParams; +use crate::reward_params::IntervalRewardingParamsUpdate; +use crate::{EpochEventId, IntervalEventId, NodeId}; +use cosmwasm_std::{Addr, Coin}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct PendingEpochEvent { + pub id: EpochEventId, + pub event: PendingEpochEventData, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum PendingEpochEventData { + // can't just pass the `Delegation` struct here as it's impossible to determine + // `cumulative_reward_ratio` ahead of time + Delegate { + owner: Addr, + mix_id: NodeId, + amount: Coin, + proxy: Option, + }, + Undelegate { + owner: Addr, + mix_id: NodeId, + proxy: Option, + }, + UnbondMixnode { + mix_id: NodeId, + }, + UpdateActiveSetSize { + new_size: u32, + }, +} + +impl From<(EpochEventId, PendingEpochEventData)> for PendingEpochEvent { + fn from(data: (EpochEventId, PendingEpochEventData)) -> Self { + PendingEpochEvent { + id: data.0, + event: data.1, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct PendingIntervalEvent { + pub id: EpochEventId, + pub event: PendingIntervalEventData, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum PendingIntervalEventData { + ChangeMixCostParams { + mix_id: NodeId, + new_costs: MixNodeCostParams, + }, + + UpdateRewardingParams { + update: IntervalRewardingParamsUpdate, + }, + UpdateIntervalConfig { + epochs_in_interval: u32, + epoch_duration_secs: u64, + }, +} + +impl From<(IntervalEventId, PendingIntervalEventData)> for PendingIntervalEvent { + fn from(data: (IntervalEventId, PendingIntervalEventData)) -> Self { + PendingIntervalEvent { + id: data.0, + event: data.1, + } + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs index c4bafa0b90..fc543a3c02 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs @@ -1,288 +1,264 @@ -use crate::{error::MixnetContractError, mixnode::StoredNodeRewardResult, ONE, U128}; -use az::CheckedCast; -use cosmwasm_std::Uint128; +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{error::MixnetContractError, Percent}; +use cosmwasm_std::Decimal; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, JsonSchema, PartialEq, Eq, Serialize, Deserialize, Copy)] -pub struct NodeEpochRewards { - params: NodeRewardParams, - result: StoredNodeRewardResult, - epoch_id: u32, +pub type Performance = Percent; + +/// Parameters required by the mix-mining reward distribution that do not change during an interval. +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/IntervalRewardParams.ts") +)] +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Serialize, JsonSchema)] +pub struct IntervalRewardParams { + /// Current value of the rewarding pool. + /// It is expected to be constant throughout the interval. + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub reward_pool: Decimal, + + /// Current value of the staking supply. + /// It is expected to be constant throughout the interval. + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub staking_supply: Decimal, + + // computed values + /// Current value of the computed reward budget per epoch, per node. + /// It is expected to be constant throughout the interval. + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub epoch_reward_budget: Decimal, + + /// Current value of the stake saturation point. + /// It is expected to be constant throughout the interval. + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub stake_saturation_point: Decimal, + + // constants(-ish) + // default: 30% + /// Current value of the sybil resistance percent (`alpha`). + /// It is not really expected to be changing very often. + /// As a matter of fact, unless there's a very specific reason, it should remain constant. + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub sybil_resistance: Percent, + + // default: 10 + /// Current active set work factor. + /// It is not really expected to be changing very often. + /// As a matter of fact, unless there's a very specific reason, it should remain constant. + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub active_set_work_factor: Decimal, + + // default: 2% + /// Current maximum interval pool emission. + /// Assuming all nodes in the rewarded set are fully saturated and have 100% performance, + /// this % of the reward pool would get distributed in rewards to all operators and its delegators. + /// It is not really expected to be changing very often. + /// As a matter of fact, unless there's a very specific reason, it should remain constant. + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub interval_pool_emission: Percent, } -impl NodeEpochRewards { - pub fn new(params: NodeRewardParams, result: StoredNodeRewardResult, epoch_id: u32) -> Self { - Self { - params, - result, - epoch_id, - } - } - - pub fn epoch_id(&self) -> u32 { - self.epoch_id - } - - pub fn sigma(&self) -> U128 { - self.result.sigma() - } - - pub fn lambda(&self) -> U128 { - self.result.lambda() - } - - pub fn params(&self) -> NodeRewardParams { - self.params - } - - pub fn reward(&self) -> Uint128 { - self.result.reward() - } - - pub fn operator_cost(&self, base_operator_cost: u64) -> U128 { - self.params.operator_cost(base_operator_cost) - } - - pub fn node_profit(&self, base_operator_cost: u64) -> U128 { - let reward = U128::from_num(self.reward().u128()); - // if operating cost is higher then the reward node profit is 0 - reward.saturating_sub(self.operator_cost(base_operator_cost)) - } - - pub fn operator_reward( - &self, - profit_margin: U128, - base_operator_cost: u64, - ) -> Result { - let reward = self.node_profit(base_operator_cost); - let operator_base_reward = reward.min(self.operator_cost(base_operator_cost)); - let div_by_zero_check = if let Some(value) = self.lambda().checked_div(self.sigma()) { - value - } else { - return Err(MixnetContractError::DivisionByZero); - }; - let operator_reward = (profit_margin + (ONE - profit_margin) * div_by_zero_check) * reward; - - let reward = (operator_reward + operator_base_reward).max(U128::from_num(0u128)); - - if let Some(int_reward) = reward.checked_cast() { - Ok(Uint128::new(int_reward)) - } else { - Err(MixnetContractError::CastError) - } - } - - pub fn delegation_reward( - &self, - delegation_amount: Uint128, - profit_margin: U128, - base_operator_cost: u64, - epoch_reward_params: EpochRewardParams, - ) -> Result { - // change all values into their fixed representations - let delegation_amount = U128::from_num(delegation_amount.u128()); - let staking_supply = U128::from_num(epoch_reward_params.staking_supply()); - - let scaled_delegation_amount = delegation_amount / staking_supply; - - let check_div_by_zero = - if let Some(value) = scaled_delegation_amount.checked_div(self.sigma()) { - value - } else { - return Err(MixnetContractError::DivisionByZero); - }; - - let delegator_reward = - (ONE - profit_margin) * check_div_by_zero * self.node_profit(base_operator_cost); - - let reward = delegator_reward.max(U128::ZERO); - if let Some(int_reward) = reward.checked_cast() { - Ok(Uint128::new(int_reward)) - } else { - Err(MixnetContractError::CastError) - } +impl IntervalRewardParams { + pub fn to_inline_json(&self) -> String { + serde_json::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } -#[derive(Debug, Clone, JsonSchema, PartialEq, Eq, Serialize, Deserialize, Copy)] -pub struct EpochRewardParams { - epoch_reward_pool: Uint128, - rewarded_set_size: Uint128, - active_set_size: Uint128, - #[serde(alias = "circulating_supply")] - staking_supply: Uint128, - sybil_resistance_percent: u8, - active_set_work_factor: u8, +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/RewardingParams.ts") +)] +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Serialize, JsonSchema)] +pub struct RewardingParams { + /// Parameters that should remain unchanged throughout an interval. + pub interval: IntervalRewardParams, + + // while the active set size can change between epochs to accommodate for bandwidth demands, + // the active set size should be unchanged between epochs and should only be adjusted between + // intervals. However, it makes more sense to keep both of those values together as they're + // very strongly related to each other. + pub rewarded_set_size: u32, + pub active_set_size: u32, } -impl EpochRewardParams { - pub fn new( - epoch_reward_pool: u128, - rewarded_set_size: u128, - active_set_size: u128, - staking_supply: u128, - sybil_resistance_percent: u8, - active_set_work_factor: u8, - ) -> EpochRewardParams { - EpochRewardParams { - epoch_reward_pool: Uint128::new(epoch_reward_pool), - rewarded_set_size: Uint128::new(rewarded_set_size), - active_set_size: Uint128::new(active_set_size), - staking_supply: Uint128::new(staking_supply), - sybil_resistance_percent, - active_set_work_factor, +impl RewardingParams { + pub fn active_node_work(&self) -> Decimal { + self.interval.active_set_work_factor * self.standby_node_work() + } + + pub fn standby_node_work(&self) -> Decimal { + let f = self.interval.active_set_work_factor; + let k = self.dec_rewarded_set_size(); + let one = Decimal::one(); + + // nodes in reserve + let k_r = self.dec_standby_set_size(); + + one / (f * k - (f - one) * k_r) + } + + pub fn dec_rewarded_set_size(&self) -> Decimal { + // the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal + // with 0 decimal places + Decimal::from_atomics(self.rewarded_set_size, 0).unwrap() + } + + pub fn dec_active_set_size(&self) -> Decimal { + // the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal + // with 0 decimal places + Decimal::from_atomics(self.active_set_size, 0).unwrap() + } + + fn dec_standby_set_size(&self) -> Decimal { + // the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal + // with 0 decimal places + Decimal::from_atomics(self.rewarded_set_size - self.active_set_size, 0).unwrap() + } + + pub fn apply_epochs_in_interval_change(&mut self, new_epochs_in_interval: u32) { + self.interval.epoch_reward_budget = self.interval.reward_pool + / Decimal::from_atomics(new_epochs_in_interval, 0).unwrap() + * self.interval.interval_pool_emission; + } + + pub fn try_change_active_set_size( + &mut self, + new_active_set_size: u32, + ) -> Result<(), MixnetContractError> { + if new_active_set_size == 0 { + return Err(MixnetContractError::ZeroActiveSet); } - } - // technically it's identical to what would have been derived with a Default implementation, - // however, I prefer to be explicit about it, as a `Default::default` value makes no sense - // apart from the `ValidatorCacheInner` context, where this value is not going to be touched anyway - // (it's guarded behind an `initialised` flag) - pub fn new_empty() -> Self { - EpochRewardParams { - epoch_reward_pool: Uint128::new(0), - staking_supply: Uint128::new(0), - sybil_resistance_percent: 0, - rewarded_set_size: Uint128::new(0), - active_set_size: Uint128::new(0), - active_set_work_factor: 0, + if new_active_set_size > self.rewarded_set_size { + return Err(MixnetContractError::InvalidActiveSetSize); } + + self.active_set_size = new_active_set_size; + Ok(()) } - pub fn rewarded_set_size(&self) -> u128 { - self.rewarded_set_size.u128() - } + pub fn try_apply_updates( + &mut self, + updates: IntervalRewardingParamsUpdate, + epochs_in_interval: u32, + ) -> Result<(), MixnetContractError> { + if !updates.contains_updates() { + return Err(MixnetContractError::EmptyParamsChangeMsg); + } - pub fn active_set_size(&self) -> u128 { - self.active_set_size.u128() - } + let mut recompute_epoch_budget = false; + let mut recompute_saturation_point = false; - pub fn staking_supply(&self) -> u128 { - self.staking_supply.u128() - } + if let Some(reward_pool) = updates.reward_pool { + recompute_epoch_budget = true; + self.interval.reward_pool = reward_pool; + } - pub fn epoch_reward_pool(&self) -> u128 { - self.epoch_reward_pool.u128() - } + if let Some(staking_supply) = updates.staking_supply { + recompute_saturation_point = true; + self.interval.staking_supply = staking_supply; + } - pub fn sybil_resistance_percent(&self) -> u8 { - self.sybil_resistance_percent - } + if let Some(sybil_resistance_percent) = updates.sybil_resistance_percent { + self.interval.sybil_resistance = sybil_resistance_percent; + } - pub fn active_set_work_factor(&self) -> u8 { - self.active_set_work_factor + if let Some(active_set_work_factor) = updates.active_set_work_factor { + self.interval.active_set_work_factor = active_set_work_factor; + } + + if let Some(interval_pool_emission) = updates.interval_pool_emission { + recompute_epoch_budget = true; + self.interval.interval_pool_emission = interval_pool_emission; + } + + if let Some(rewarded_set_size) = updates.rewarded_set_size { + if rewarded_set_size == 0 { + return Err(MixnetContractError::ZeroRewardedSet); + } + if rewarded_set_size < self.active_set_size { + return Err(MixnetContractError::InvalidRewardedSetSize); + } + + recompute_saturation_point = true; + self.rewarded_set_size = rewarded_set_size; + } + + if recompute_epoch_budget { + self.interval.epoch_reward_budget = self.interval.reward_pool + / Decimal::from_atomics(epochs_in_interval, 0).unwrap() + * self.interval.interval_pool_emission; + } + + if recompute_saturation_point { + self.interval.stake_saturation_point = self.interval.staking_supply + / Decimal::from_atomics(self.rewarded_set_size, 0).unwrap(); + } + + Ok(()) } } -#[derive(Debug, Clone, JsonSchema, PartialEq, Eq, Serialize, Deserialize, Copy)] +// TODO: possibly refactor this +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Serialize, JsonSchema)] pub struct NodeRewardParams { - reward_blockstamp: u64, - uptime: Uint128, - in_active_set: bool, + pub performance: Percent, + pub in_active_set: bool, } impl NodeRewardParams { - pub fn new(reward_blockstamp: u64, uptime: u128, in_active_set: bool) -> NodeRewardParams { + pub fn new(performance: Percent, in_active_set: bool) -> Self { NodeRewardParams { - reward_blockstamp, - uptime: Uint128::new(uptime), + performance, in_active_set, } } - - pub fn operator_cost(&self, base_operator_cost: u64) -> U128 { - self.performance() * U128::from_num(base_operator_cost) - } - - pub fn uptime(&self) -> Uint128 { - self.uptime - } - - pub fn performance(&self) -> U128 { - U128::from_num(self.uptime.u128()) / U128::from_num(100) - } - - pub fn set_reward_blockstamp(&mut self, blockstamp: u64) { - self.reward_blockstamp = blockstamp; - } } -#[derive(Debug, Clone, JsonSchema, PartialEq, Eq, Serialize, Deserialize, Copy)] -pub struct RewardParams { - pub epoch: EpochRewardParams, - pub node: NodeRewardParams, +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/IntervalRewardingParamsUpdate.ts") +)] +#[derive( + Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, PartialOrd, Serialize, JsonSchema, +)] +pub struct IntervalRewardingParamsUpdate { + #[cfg_attr(feature = "generate-ts", ts(type = "string | null"))] + pub reward_pool: Option, + + #[cfg_attr(feature = "generate-ts", ts(type = "string | null"))] + pub staking_supply: Option, + + #[cfg_attr(feature = "generate-ts", ts(type = "string | null"))] + pub sybil_resistance_percent: Option, + + #[cfg_attr(feature = "generate-ts", ts(type = "string | null"))] + pub active_set_work_factor: Option, + + #[cfg_attr(feature = "generate-ts", ts(type = "string | null"))] + pub interval_pool_emission: Option, + + pub rewarded_set_size: Option, } -impl RewardParams { - pub fn new(epoch: EpochRewardParams, node: NodeRewardParams) -> RewardParams { - RewardParams { epoch, node } +impl IntervalRewardingParamsUpdate { + pub fn contains_updates(&self) -> bool { + // essentially at least a single field has to be a `Some` + self.reward_pool.is_some() + || self.staking_supply.is_some() + || self.sybil_resistance_percent.is_some() + || self.active_set_work_factor.is_some() + || self.interval_pool_emission.is_some() + || self.rewarded_set_size.is_some() } - pub fn omega(&self) -> U128 { - // As per keybase://chat/nymtech#tokeneconomics/1179 - let denom = self.active_set_work_factor() * U128::from_num(self.rewarded_set_size()) - - (self.active_set_work_factor() - ONE) * U128::from_num(self.idle_nodes().u128()); - - if denom == 0 { - return U128::ZERO; - } - - // Div by zero checked above - if self.in_active_set() { - // work_active = factor / (factor * self.network.k[month] - (factor - 1) * idle_nodes) - self.active_set_work_factor() / denom * self.rewarded_set_size() - } else { - // work_idle = 1 / (factor * self.network.k[month] - (factor - 1) * idle_nodes) - ONE / denom * self.rewarded_set_size() - } - } - - pub fn idle_nodes(&self) -> Uint128 { - self.epoch.rewarded_set_size - self.epoch.active_set_size - } - - pub fn active_set_work_factor(&self) -> U128 { - U128::from_num(self.epoch.active_set_work_factor) - } - - pub fn in_active_set(&self) -> bool { - self.node.in_active_set - } - - pub fn performance(&self) -> U128 { - self.node.performance() - } - - pub fn set_reward_blockstamp(&mut self, blockstamp: u64) { - self.node.reward_blockstamp = blockstamp; - } - - pub fn epoch_reward_pool(&self) -> u128 { - self.epoch.epoch_reward_pool.u128() - } - - pub fn rewarded_set_size(&self) -> u128 { - self.epoch.rewarded_set_size.u128() - } - - pub fn staking_supply(&self) -> u128 { - self.epoch.staking_supply.u128() - } - - pub fn reward_blockstamp(&self) -> u64 { - self.node.reward_blockstamp - } - - pub fn uptime(&self) -> Uint128 { - self.node.uptime() - } - - pub fn one_over_k(&self) -> U128 { - ONE / U128::from_num(self.epoch.rewarded_set_size.u128()) - } - - pub fn alpha(&self) -> U128 { - U128::from_num(self.epoch.sybil_resistance_percent) / U128::from_num(100) + pub fn to_inline_json(&self) -> String { + serde_json::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs new file mode 100644 index 0000000000..8a6a8f3366 --- /dev/null +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs @@ -0,0 +1,23 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{Coin, Decimal, Uint128}; + +/// Truncates all decimal points so that the reward would fit in a `Coin` and so that we would +/// never attempt to reward more than the owner is due +/// for example it truncates "23.9" into "23" +pub fn truncate_reward(reward: Decimal, denom: impl Into) -> Coin { + let amount = truncate_reward_amount(reward); + Coin { + denom: denom.into(), + amount, + } +} + +pub fn truncate_reward_amount(reward: Decimal) -> Uint128 { + truncate_decimal(reward) +} + +pub fn truncate_decimal(amount: Decimal) -> Uint128 { + amount * Uint128::new(1) +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs new file mode 100644 index 0000000000..9aaaa8a1a5 --- /dev/null +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs @@ -0,0 +1,61 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{Coin, Decimal}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +pub mod helpers; +pub mod simulator; + +#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq, Eq)] +pub struct RewardEstimate { + pub total_node_reward: Decimal, + + // note that operator reward includes the operating_cost, + // i.e. say total_node_reward was `1nym` and operating_cost was `2nym` + // in that case the operator reward would still be `1nym` as opposed to 0 + pub operator: Decimal, + pub delegates: Decimal, + pub operating_cost: Decimal, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq)] +pub struct RewardDistribution { + pub operator: Decimal, + pub delegates: Decimal, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq)] +pub struct PendingRewardResponse { + pub amount_staked: Option, + pub amount_earned: Option, + pub amount_earned_detailed: Option, + + /// The associated mixnode is still fully bonded, meaning it is neither unbonded + /// nor in the process of unbonding that would have finished at the epoch transition. + pub mixnode_still_fully_bonded: bool, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq)] +pub struct EstimatedCurrentEpochRewardResponse { + pub original_stake: Option, + + pub current_stake_value: Option, + pub current_stake_value_detailed_amount: Option, + + pub estimation: Option, + pub detailed_estimation_amount: Option, +} + +impl EstimatedCurrentEpochRewardResponse { + pub fn empty_response() -> Self { + EstimatedCurrentEpochRewardResponse { + original_stake: None, + current_stake_value: None, + current_stake_value_detailed_amount: None, + estimation: None, + detailed_estimation_amount: None, + } + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator.rs new file mode 100644 index 0000000000..9fa4fc743b --- /dev/null +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator.rs @@ -0,0 +1,465 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::MixnetContractError; +use crate::mixnode::{MixNodeCostParams, MixNodeRewarding}; +use crate::reward_params::{IntervalRewardParams, NodeRewardParams, RewardingParams}; +use crate::rewarding::helpers::truncate_reward; +use crate::rewarding::RewardDistribution; +use crate::{Delegation, Interval, Percent}; +use cosmwasm_std::{Addr, Coin, Decimal}; + +pub struct Simulator { + pub node_rewarding_details: MixNodeRewarding, + pub node_delegations: Vec, + pub system_rewarding_params: RewardingParams, + + pub interval: Interval, +} + +impl Simulator { + pub fn new( + profit_margin_percent: Percent, + interval_operating_cost: Coin, + system_rewarding_params: RewardingParams, + initial_pledge: Coin, + interval: Interval, + ) -> Self { + let cost_params = MixNodeCostParams { + profit_margin_percent, + interval_operating_cost, + }; + + Simulator { + node_rewarding_details: MixNodeRewarding::initialise_new( + cost_params, + &initial_pledge, + Default::default(), + ), + node_delegations: vec![], + system_rewarding_params, + interval, + } + } + + pub fn delegate(&mut self, amount: Coin) { + let cumulative_reward_ratio = self.node_rewarding_details.total_unit_reward; + // let record = self.node_rewarding_details.increment_period(); + // self.node_historical_records.insert(period, record); + self.node_rewarding_details + .add_base_delegation(amount.amount); + + // we don't care about the owner/node details here + self.node_delegations.push(Delegation::new( + Addr::unchecked("bob"), + 42, + cumulative_reward_ratio, + amount, + 123, + None, + )) + } + + pub fn determine_delegation_reward(&self, delegation: &Delegation) -> Decimal { + self.node_rewarding_details + .determine_delegation_reward(delegation) + } + + // since this is a simulator only, not something to be used in the production code, the unwraps are fine + // if user inputs are invalid + pub fn undelegate( + &mut self, + delegation_index: usize, + ) -> Result<(Coin, Coin), MixnetContractError> { + let delegation = self.node_delegations.remove(delegation_index); + let reward = self.determine_delegation_reward(&delegation); + self.node_rewarding_details + .decrease_delegates_decimal(delegation.dec_amount() + reward)?; + self.node_rewarding_details.unique_delegations -= 1; + + let reward_denom = &delegation.amount.denom; + let truncated_reward = truncate_reward(reward, reward_denom); + + // if this was last delegation, move all leftover decimal tokens to the operator + // (this is literally in the order of a millionth of a micronym) + if self.node_delegations.is_empty() { + assert_eq!(self.node_rewarding_details.unique_delegations, 0); + self.node_rewarding_details.operator += self.node_rewarding_details.delegates; + self.node_rewarding_details.delegates = Decimal::zero(); + } + + Ok((delegation.amount, truncated_reward)) + } + + pub fn determine_total_delegation_reward(&self) -> Decimal { + let mut total = Decimal::zero(); + for delegation in &self.node_delegations { + total += self.determine_delegation_reward(delegation) + } + total + } + + pub fn simulate_epoch(&mut self, node_params: NodeRewardParams) -> RewardDistribution { + let reward_distribution = self.node_rewarding_details.calculate_epoch_reward( + &self.system_rewarding_params, + node_params, + self.interval.epochs_in_interval(), + ); + self.node_rewarding_details.distribute_rewards( + reward_distribution, + self.interval.current_epoch_absolute_id(), + ); + self.interval = self.interval.advance_epoch(); + + reward_distribution + } + + // assume node state doesn't change in the interval (kinda unrealistic) + pub fn simulate_interval(&mut self, node_params: NodeRewardParams) { + assert_eq!(self.interval.current_epoch_id(), 0); + let id = self.interval.current_interval_id(); + let mut distributed = Decimal::zero(); + for _ in 0..self.interval.epochs_in_interval() { + let distr = self.simulate_epoch(node_params); + distributed += distr.operator; + distributed += distr.delegates; + } + assert_eq!(id + 1, self.interval.current_interval_id()); + + // update reward pool and all of that + let old = self.system_rewarding_params.interval; + let reward_pool = old.reward_pool - distributed; + let staking_supply = old.staking_supply + distributed; + let epoch_reward_budget = reward_pool + / Decimal::from_atomics(self.interval.epochs_in_interval(), 0).unwrap() + * old.interval_pool_emission.value(); + let stake_saturation_point = staking_supply + / Decimal::from_atomics(self.system_rewarding_params.rewarded_set_size, 0).unwrap(); + + let updated_params = RewardingParams { + interval: IntervalRewardParams { + reward_pool, + staking_supply, + epoch_reward_budget, + stake_saturation_point, + sybil_resistance: old.sybil_resistance, + active_set_work_factor: old.active_set_work_factor, + interval_pool_emission: old.interval_pool_emission, + }, + rewarded_set_size: self.system_rewarding_params.rewarded_set_size, + active_set_size: self.system_rewarding_params.active_set_size, + }; + + self.system_rewarding_params = updated_params; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::reward_params::IntervalRewardParams; + use crate::rewarding::helpers::truncate_reward_amount; + use cosmwasm_std::testing::mock_env; + use cosmwasm_std::{coin, Uint128}; + use std::time::Duration; + + fn base_simulator(initial_pledge: u128) -> Simulator { + let profit_margin = Percent::from_percentage_value(10).unwrap(); + let interval_operating_cost = Coin::new(40_000_000, "unym"); + let epochs_in_interval = 720u32; + let rewarded_set_size = 240; + let active_set_size = 100; + let interval_pool_emission = Percent::from_percentage_value(2).unwrap(); + + let reward_pool = 250_000_000_000_000u128; + let staking_supply = 100_000_000_000_000u128; + let epoch_reward_budget = + interval_pool_emission * Decimal::from_ratio(reward_pool, epochs_in_interval); + let stake_saturation_point = Decimal::from_ratio(staking_supply, rewarded_set_size); + + let rewarding_params = RewardingParams { + interval: IntervalRewardParams { + reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens) + staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M + epoch_reward_budget, + stake_saturation_point, + sybil_resistance: Percent::from_percentage_value(30).unwrap(), + active_set_work_factor: Decimal::percent(1000), // value '10' + interval_pool_emission, + }, + rewarded_set_size, + active_set_size, + }; + + let interval = Interval::init_interval( + epochs_in_interval, + Duration::from_secs(60 * 60), + &mock_env(), + ); + let initial_pledge = Coin::new(initial_pledge, "unym"); + Simulator::new( + profit_margin, + interval_operating_cost, + rewarding_params, + initial_pledge, + interval, + ) + } + + fn compare_decimals(a: Decimal, b: Decimal) { + let epsilon = Decimal::from_ratio(1u128, 100_000_000u128); + if a > b { + assert!(a - b < epsilon, "{} != {}", a, b) + } else { + assert!(b - a < epsilon, "{} != {}", a, b) + } + } + + // essentially our delegations + estimated rewards HAVE TO equal to what we actually determined + fn check_rewarding_invariant(simulator: &Simulator) { + let delegation_sum: Decimal = simulator + .node_delegations + .iter() + .map(|d| d.dec_amount()) + .sum(); + let reward_sum = simulator.determine_total_delegation_reward(); + compare_decimals( + delegation_sum + reward_sum, + simulator.node_rewarding_details.delegates, + ) + } + + #[test] + fn simulator_returns_expected_values_for_base_case() { + let mut simulator = base_simulator(10000_000000); + + let epoch_params = + NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + let rewards = simulator.simulate_epoch(epoch_params); + + assert_eq!(rewards.delegates, Decimal::zero()); + compare_decimals(rewards.operator, "1128452.5416104363".parse().unwrap()); + } + + #[test] + fn single_delegation_at_genesis() { + let mut simulator = base_simulator(10000_000000); + simulator.delegate(Coin::new(18000_000000, "unym")); + + let node_params = NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + let rewards = simulator.simulate_epoch(node_params); + + compare_decimals(rewards.delegates, "1795950.2602660495".parse().unwrap()); + compare_decimals(rewards.operator, "1363716.856243172".parse().unwrap()); + + compare_decimals( + rewards.delegates, + simulator.determine_total_delegation_reward(), + ); + assert_eq!( + simulator.node_rewarding_details.operator, + rewards.operator + Decimal::from_atomics(10000_000000u128, 0).unwrap() + ); + assert_eq!( + simulator.node_rewarding_details.delegates, + rewards.delegates + Decimal::from_atomics(18000_000000u128, 0).unwrap() + ); + } + + #[test] + fn delegation_and_undelegation() { + let mut simulator = base_simulator(10000_000000); + let node_params = NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + + let rewards1 = simulator.simulate_epoch(node_params); + let expected_operator1 = "1128452.5416104363".parse().unwrap(); + assert_eq!(rewards1.delegates, Decimal::zero()); + compare_decimals(rewards1.operator, expected_operator1); + + simulator.delegate(Coin::new(18000_000000, "unym")); + + let rewards2 = simulator.simulate_epoch(node_params); + let expected_operator2 = "1363843.413584609".parse().unwrap(); + let expected_delegator_reward1 = "1795952.25874404".parse().unwrap(); + compare_decimals(rewards2.delegates, expected_delegator_reward1); + compare_decimals(rewards2.operator, expected_operator2); + + let rewards3 = simulator.simulate_epoch(node_params); + let expected_operator3 = "1364017.7824440491".parse().unwrap(); + let expected_delegator_reward2 = "1796135.9269468693".parse().unwrap(); + compare_decimals(rewards3.delegates, expected_delegator_reward2); + compare_decimals(rewards3.operator, expected_operator3); + + let (delegation, reward) = simulator.undelegate(0).unwrap(); + assert_eq!(delegation.amount.u128(), 18000_000000); + assert_eq!( + reward.amount, + (expected_delegator_reward1 + expected_delegator_reward2) * Uint128::new(1) + ); + + let base_op = Decimal::from_atomics(10000_000000u128, 0).unwrap(); + compare_decimals( + simulator.node_rewarding_details.operator, + base_op + expected_operator1 + expected_operator2 + expected_operator3, + ); + assert_eq!(Decimal::zero(), simulator.node_rewarding_details.delegates); + } + + #[test] + fn withdrawing_operator_reward() { + // essentially all delegators' rewards (and the operator itself) are still correctly computed + let original_pledge = coin(10000_000000, "unym"); + let mut simulator = base_simulator(original_pledge.amount.u128()); + let node_params = NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + + // add 2 delegations at genesis (because it makes things easier and as shown with previous tests + // delegating at different times still work) + simulator.delegate(Coin::new(18000_000000, "unym")); + simulator.delegate(Coin::new(4000_000000, "unym")); + + // "normal", sanity check rewarding + let rewards1 = simulator.simulate_epoch(node_params); + let expected_operator1 = "1411087.1007647323".parse().unwrap(); + let expected_delegator_reward1 = "2199961.032388664".parse().unwrap(); + compare_decimals(rewards1.delegates, expected_delegator_reward1); + compare_decimals(rewards1.operator, expected_operator1); + check_rewarding_invariant(&simulator); + + let reward = simulator + .node_rewarding_details + .withdraw_operator_reward(&original_pledge); + assert_eq!(reward.amount, truncate_reward_amount(expected_operator1)); + assert_eq!( + simulator.node_rewarding_details.operator, + Decimal::from_atomics(original_pledge.amount, 0).unwrap() + ); + + let rewards2 = simulator.simulate_epoch(node_params); + let expected_operator2 = "1411113.0004067947".parse().unwrap(); + let expected_delegator_reward2 = "2200183.3879084454".parse().unwrap(); + compare_decimals(rewards2.delegates, expected_delegator_reward2); + compare_decimals(rewards2.operator, expected_operator2); + check_rewarding_invariant(&simulator); + } + + #[test] + fn withdrawing_delegator_reward() { + // essentially all delegators' rewards (and the operator itself) are still correctly computed + let mut simulator = base_simulator(10000_000000); + let node_params = NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + + // add 2 delegations at genesis (because it makes things easier and as shown with previous tests + // delegating at different times still work) + simulator.delegate(Coin::new(18000_000000, "unym")); + simulator.delegate(Coin::new(4000_000000, "unym")); + + // "normal", sanity check rewarding + let rewards1 = simulator.simulate_epoch(node_params); + let expected_operator1 = "1411087.1007647323".parse().unwrap(); + let expected_delegator_reward1 = "2199961.032388664".parse().unwrap(); + compare_decimals(rewards1.delegates, expected_delegator_reward1); + compare_decimals(rewards1.operator, expected_operator1); + check_rewarding_invariant(&simulator); + + // reference to our `18000_000000` delegation + let delegation1 = &mut simulator.node_delegations[0]; + let reward = simulator + .node_rewarding_details + .withdraw_delegator_reward(delegation1) + .unwrap(); + let expected_del1_reward = "1799968.1174089068".parse().unwrap(); + assert_eq!(reward.amount, truncate_reward_amount(expected_del1_reward)); + + // new reward after withdrawal + let rewards2 = simulator.simulate_epoch(node_params); + let expected_operator2 = "1411250.1907492676".parse().unwrap(); + let expected_delegator_reward2 = "2200004.051009689".parse().unwrap(); + compare_decimals(rewards2.delegates, expected_delegator_reward2); + compare_decimals(rewards2.operator, expected_operator2); + check_rewarding_invariant(&simulator); + + // check final values + let reward_del1 = simulator + .node_rewarding_details + .withdraw_delegator_reward(&mut simulator.node_delegations[0]) + .unwrap(); + let expected_del1_reward = "1799970.5883041779".parse().unwrap(); + assert_eq!( + reward_del1.amount, + truncate_reward_amount(expected_del1_reward) + ); + + let reward_del2 = simulator + .node_rewarding_details + .withdraw_delegator_reward(&mut simulator.node_delegations[1]) + .unwrap(); + let first: Decimal = "399992.91497975704".parse().unwrap(); + let second: Decimal = "400033.4627055114".parse().unwrap(); + let expected_del2_reward = first + second; + assert_eq!( + reward_del2.amount, + truncate_reward_amount(expected_del2_reward) + ); + } + + #[test] + fn simulating_multiple_epochs() { + let mut simulator = base_simulator(10000_000000); + + let mut is_active = true; + let mut performance = Percent::from_percentage_value(100).unwrap(); + for epoch in 0..720 { + if epoch == 0 { + simulator.delegate(Coin::new(18000_000000, "unym")) + } + if epoch == 42 { + simulator.delegate(Coin::new(2000_000000, "unym")) + } + if epoch == 89 { + is_active = false; + } + if epoch == 123 { + simulator.delegate(Coin::new(6666_000000, "unym")) + } + if epoch == 167 { + performance = Percent::from_percentage_value(90).unwrap(); + } + if epoch == 245 { + simulator.delegate(Coin::new(2050_000000, "unym")) + } + if epoch == 264 { + let (delegation, _reward) = simulator.undelegate(1).unwrap(); + // sanity check to make sure we undelegated what we wanted to undelegate : ) + assert_eq!(delegation.amount.u128(), 2000_000000); + // TODO: figure out if there's a good way to verify whether `reward` is what we expect it to be + } + if epoch == 345 { + is_active = true; + } + if epoch == 358 { + performance = Percent::from_percentage_value(100).unwrap(); + } + if epoch == 458 { + let (delegation, _reward) = simulator.undelegate(0).unwrap(); + // sanity check to make sure we undelegated what we wanted to undelegate : ) + assert_eq!(delegation.amount.u128(), 18000_000000); + // TODO: figure out if there's a good way to verify whether `reward` is what we expect it to be + } + if epoch == 545 { + simulator.delegate(Coin::new(5000_000000, "unym")) + } + + // this has to always hold + check_rewarding_invariant(&simulator); + let node_params = NodeRewardParams::new(performance, is_active); + simulator.simulate_epoch(node_params); + } + + // after everyone undelegates, there should be nothing left in the delegates pool + let delegations = simulator.node_delegations.len(); + for _ in 0..delegations { + simulator.undelegate(0).unwrap(); + } + assert_eq!(Decimal::zero(), simulator.node_rewarding_details.delegates); + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index c93acd80a4..71faa991a6 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -1,16 +1,111 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::mixnode::DelegatorRewardParams; +use crate::error::MixnetContractError; +use crate::rewarding::helpers::truncate_decimal; use crate::{Layer, RewardedSetNodeStatus}; use cosmwasm_std::{Addr, Uint128}; +use cosmwasm_std::{Coin, Decimal}; use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; +use serde::de::Error; +use serde::{Deserialize, Deserializer, Serialize}; use std::fmt::{self, Display, Formatter}; +use std::ops::{Index, Mul}; + +// type aliases for better reasoning about available data +pub type IdentityKey = String; +pub type IdentityKeyRef<'a> = &'a str; +pub type SphinxKey = String; +pub type SphinxKeyRef<'a> = &'a str; +pub type EpochId = u32; +pub type IntervalId = u32; +pub type NodeId = u32; +pub type EpochEventId = u32; +pub type IntervalEventId = u32; + +/// Percent represents a value between 0 and 100% +/// (i.e. between 0.0 and 1.0) +#[derive( + Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Serialize, Deserialize, JsonSchema, +)] +pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal); + +impl Percent { + pub fn new(value: Decimal) -> Result { + if value > Decimal::one() { + Err(MixnetContractError::InvalidPercent) + } else { + Ok(Percent(value)) + } + } + + pub fn is_zero(&self) -> bool { + self.0 == Decimal::zero() + } + + pub fn from_percentage_value(value: u64) -> Result { + Percent::new(Decimal::percent(value)) + } + + pub fn value(&self) -> Decimal { + self.0 + } + + pub fn round_to_integer(&self) -> u8 { + let hundred = Decimal::from_ratio(100u32, 1u32); + // we know the cast from u128 to u8 is a safe one since the internal value must be within 0 - 1 range + truncate_decimal(hundred * self.0).u128() as u8 + } +} + +impl Display for Percent { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let adjusted = Decimal::from_atomics(100u32, 0).unwrap() * self.0; + write!(f, "{}%", adjusted) + } +} + +impl Mul for Percent { + type Output = Decimal; + + fn mul(self, rhs: Decimal) -> Self::Output { + self.0 * rhs + } +} + +impl Mul for Decimal { + type Output = Decimal; + + fn mul(self, rhs: Percent) -> Self::Output { + rhs * self + } +} + +impl Mul for Percent { + type Output = Uint128; + + fn mul(self, rhs: Uint128) -> Self::Output { + self.0 * rhs + } +} + +// implement custom Deserialize because we want to validate Percent has the correct range +fn de_decimal_percent<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let v = Decimal::deserialize(deserializer)?; + if v > Decimal::one() { + Err(D::Error::custom( + "provided decimal percent is larger than 100%", + )) + } else { + Ok(v) + } +} #[derive(Debug, Default, Serialize, Deserialize, Copy, Clone, Eq, PartialEq)] pub struct LayerDistribution { - pub gateways: u64, pub layer1: u64, pub layer2: u64, pub layer3: u64, @@ -25,121 +120,133 @@ impl LayerDistribution { ]; layers.iter().min_by_key(|x| x.1).unwrap().0 } -} -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] -pub struct ContractStateParams { - // so currently interval_length is being unused and validator API performs rewarding - // based on its own interval length config value. I guess that's fine for time being - // however, in the future, the contract constant should be controlling it instead. - // pub interval_length: u32, // length of a rewarding interval/interval, expressed in hours - pub minimum_mixnode_pledge: Uint128, // minimum amount a mixnode must pledge to get into the system - pub minimum_gateway_pledge: Uint128, // minimum amount a gateway must pledge to get into the system + pub fn increment_layer_count(&mut self, layer: Layer) { + match layer { + Layer::One => self.layer1 += 1, + Layer::Two => self.layer2 += 1, + Layer::Three => self.layer3 += 1, + } + } - // number of mixnode that are going to get rewarded during current rewarding interval (k_m) - // based on overall demand for private bandwidth- - pub mixnode_rewarded_set_size: u32, + pub fn decrement_layer_count(&mut self, layer: Layer) -> Result<(), MixnetContractError> { + match layer { + Layer::One => { + self.layer1 = + self.layer1 + .checked_sub(1) + .ok_or(MixnetContractError::OverflowSubtraction { + minuend: self.layer1, + subtrahend: 1, + })? + } + Layer::Two => { + self.layer2 = + self.layer2 + .checked_sub(1) + .ok_or(MixnetContractError::OverflowSubtraction { + minuend: self.layer2, + subtrahend: 1, + })? + } + Layer::Three => { + self.layer3 = + self.layer3 + .checked_sub(1) + .ok_or(MixnetContractError::OverflowSubtraction { + minuend: self.layer3, + subtrahend: 1, + })? + } + } - // subset of rewarded mixnodes that are actively receiving mix traffic - // used to handle shorter-term (e.g. hourly) fluctuations of demand - pub mixnode_active_set_size: u32, - pub staking_supply: Uint128, -} - -impl Display for ContractStateParams { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "Contract state parameters: [ ")?; - write!( - f, - "minimum mixnode pledge: {}; ", - self.minimum_mixnode_pledge - )?; - write!( - f, - "minimum gateway pledge: {}; ", - self.minimum_gateway_pledge - )?; - write!( - f, - "mixnode rewarded set size: {}", - self.mixnode_rewarded_set_size - )?; - write!( - f, - "mixnode active set size: {}", - self.mixnode_active_set_size - ) + Ok(()) } } -#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct RewardingResult { - pub node_reward: Uint128, +impl Index for LayerDistribution { + type Output = u64; + + fn index(&self, index: Layer) -> &Self::Output { + match index { + Layer::One => &self.layer1, + Layer::Two => &self.layer2, + Layer::Three => &self.layer3, + } + } } -#[derive(Debug, Serialize, Deserialize)] -pub struct PendingDelegatorRewarding { - // keep track of the running rewarding results so we'd known how much was the operator and its delegators rewarded - pub running_results: RewardingResult, +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct ContractState { + pub owner: Addr, // only the owner account can update state + pub rewarding_validator_address: Addr, - pub next_start: Addr, - - pub rewarding_params: DelegatorRewardParams, + /// Address of the vesting contract to which the mixnet contract would be sending all + /// track-related messages. + pub vesting_contract_address: Addr, + pub rewarding_denom: String, + pub params: ContractStateParams, } -#[derive(Debug, Serialize, Deserialize)] -pub enum RewardingStatus { - Complete(RewardingResult), - PendingNextDelegatorPage(PendingDelegatorRewarding), +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct ContractStateParams { + /// Minimum amount a delegator must stake in orders for his delegation to get accepted. + pub minimum_mixnode_delegation: Option, + + /// Minimum amount a mixnode must pledge to get into the system. + pub minimum_mixnode_pledge: Coin, + + /// Minimum amount a gateway must pledge to get into the system. + pub minimum_gateway_pledge: Coin, } -#[derive(Debug, Serialize, Deserialize)] -pub struct MixnodeRewardingStatusResponse { - pub status: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct MixnetContractVersion { - // VERGEN_BUILD_TIMESTAMP - pub build_timestamp: String, - - // VERGEN_BUILD_SEMVER - pub build_version: String, - - // VERGEN_GIT_SHA - pub commit_sha: String, - - // VERGEN_GIT_COMMIT_TIMESTAMP - pub commit_timestamp: String, - - // VERGEN_GIT_BRANCH - pub commit_branch: String, - - // VERGEN_RUSTC_SEMVER - pub rustc_version: String, -} - -// type aliases for better reasoning about available data -pub type IdentityKey = String; -pub type IdentityKeyRef<'a> = &'a str; -pub type SphinxKey = String; - #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] pub struct PagedRewardedSetResponse { - pub identities: Vec<(IdentityKey, RewardedSetNodeStatus)>, - pub start_next_after: Option, - pub at_height: u64, + pub nodes: Vec<(NodeId, RewardedSetNodeStatus)>, + pub start_next_after: Option, } -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] -pub struct RewardedSetUpdateDetails { - pub refresh_rate_blocks: u64, - pub last_refreshed_block: u64, - pub current_height: u64, -} +#[cfg(test)] +mod tests { + use super::*; -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] -pub struct IntervalRewardedSetHeightsResponse { - pub interval_id: u32, - pub heights: Vec, + #[test] + fn percent_serde() { + let valid_value = Percent::from_percentage_value(80).unwrap(); + let serialized = serde_json::to_string(&valid_value).unwrap(); + + println!("{}", serialized); + let deserialized: Percent = serde_json::from_str(&serialized).unwrap(); + assert_eq!(valid_value, deserialized); + + let invalid_values = vec!["\"42\"", "\"1.1\"", "\"1.00000001\"", "\"foomp\"", "\"1a\""]; + for invalid_value in invalid_values { + assert!(serde_json::from_str::<'_, Percent>(invalid_value).is_err()) + } + assert_eq!( + serde_json::from_str::<'_, Percent>("\"0.95\"").unwrap(), + Percent::from_percentage_value(95).unwrap() + ) + } + + #[test] + fn percent_to_absolute_integer() { + let p = serde_json::from_str::<'_, Percent>("\"0.0001\"").unwrap(); + assert_eq!(p.round_to_integer(), 0); + + let p = serde_json::from_str::<'_, Percent>("\"0.0099\"").unwrap(); + assert_eq!(p.round_to_integer(), 0); + + let p = serde_json::from_str::<'_, Percent>("\"0.0199\"").unwrap(); + assert_eq!(p.round_to_integer(), 1); + + let p = serde_json::from_str::<'_, Percent>("\"0.45123\"").unwrap(); + assert_eq!(p.round_to_integer(), 45); + + let p = serde_json::from_str::<'_, Percent>("\"0.999999999\"").unwrap(); + assert_eq!(p.round_to_integer(), 99); + + let p = serde_json::from_str::<'_, Percent>("\"1.00\"").unwrap(); + assert_eq!(p.round_to_integer(), 100); + } } diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs index 93ca6814dd..896ead2c9e 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs @@ -16,6 +16,8 @@ pub const VESTING_GATEWAY_UNBONDING_EVENT_TYPE: &str = "vesting_gateway_unbondin pub const VESTING_MIXNODE_BONDING_EVENT_TYPE: &str = "vesting_mixnode_bonding"; pub const VESTING_MIXNODE_UNBONDING_EVENT_TYPE: &str = "vesting_mixnode_unbonding"; pub const VESTING_UPDATE_MIXNODE_CONFIG_EVENT_TYPE: &str = "vesting_update_mixnode_config"; +pub const VESTING_UPDATE_MIXNODE_COST_PARAMS_EVENT_TYPE: &str = + "vesting_update_mixnode_cost_params"; pub const TRACK_MIXNODE_UNBOND_EVENT_TYPE: &str = "track_mixnode_unbond"; pub const TRACK_GATEWAY_UNBOND_EVENT_TYPE: &str = "track_gateway_unbond"; @@ -114,6 +116,10 @@ pub fn new_vesting_update_mixnode_config_event() -> Event { Event::new(VESTING_UPDATE_MIXNODE_CONFIG_EVENT_TYPE) } +pub fn new_vesting_update_mixnode_cost_params_event() -> Event { + Event::new(VESTING_UPDATE_MIXNODE_COST_PARAMS_EVENT_TYPE) +} + pub fn new_vesting_mixnode_unbonding_event() -> Event { Event::new(VESTING_MIXNODE_UNBONDING_EVENT_TYPE) } diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs index 60c3745a3c..e81a8068fa 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs @@ -1,19 +1,15 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use cosmwasm_std::{Addr, Coin, Timestamp, Uint128}; +use mixnet_contract_common::NodeId; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; pub use messages::{ExecuteMsg, InitMsg, MigrateMsg, QueryMsg}; -use mixnet_contract_common::IdentityKey; pub mod events; pub mod messages; -pub fn one_ucoin(denom: String) -> Coin { - Coin::new(1, denom) -} - #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", @@ -78,18 +74,14 @@ impl OriginalVestingResponse { #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, JsonSchema)] pub struct VestingDelegation { pub account_id: u32, - pub mix_identity: IdentityKey, + pub mix_id: NodeId, pub block_timestamp: u64, pub amount: Uint128, } impl VestingDelegation { - pub fn storage_key(&self) -> (u32, IdentityKey, u64) { - ( - self.account_id, - self.mix_identity.clone(), - self.block_timestamp, - ) + pub fn storage_key(&self) -> (u32, NodeId, u64) { + (self.account_id, self.mix_id, self.block_timestamp) } } @@ -97,12 +89,12 @@ impl VestingDelegation { pub struct DelegationTimesResponse { pub owner: Addr, pub account_id: u32, - pub mix_identity: IdentityKey, + pub mix_id: NodeId, pub delegation_timestamps: Vec, } #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, JsonSchema)] pub struct AllDelegationsResponse { pub delegations: Vec, - pub start_next_after: Option<(u32, IdentityKey, u64)>, + pub start_next_after: Option<(u32, NodeId, u64)>, } diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs index 660c4a5dc8..aa4f26afdd 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs @@ -1,5 +1,8 @@ use cosmwasm_std::{Coin, Timestamp, Uint128}; -use mixnet_contract_common::{Gateway, IdentityKey, MixNode}; +use mixnet_contract_common::{ + mixnode::{MixNodeConfigUpdate, MixNodeCostParams}, + Gateway, MixNode, NodeId, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -12,7 +15,9 @@ pub struct InitMsg { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub struct MigrateMsg {} +pub struct MigrateMsg { + pub v2_mixnet_contract_address: String, +} #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, Default)] pub struct VestingSpecification { @@ -56,24 +61,23 @@ pub enum ExecuteMsg { }, ClaimOperatorReward {}, ClaimDelegatorReward { - mix_identity: String, + mix_id: NodeId, }, - CompoundDelegatorReward { - mix_identity: String, + UpdateMixnodeCostParams { + new_costs: MixNodeCostParams, }, - CompoundOperatorReward {}, UpdateMixnodeConfig { - profit_margin_percent: u8, + new_config: MixNodeConfigUpdate, }, UpdateMixnetAddress { address: String, }, DelegateToMixnode { - mix_identity: IdentityKey, + mix_id: NodeId, amount: Coin, }, UndelegateFromMixnode { - mix_identity: IdentityKey, + mix_id: NodeId, }, CreateAccount { owner_address: String, @@ -85,11 +89,12 @@ pub enum ExecuteMsg { }, TrackUndelegation { owner: String, - mix_identity: IdentityKey, + mix_id: NodeId, amount: Coin, }, BondMixnode { mix_node: MixNode, + cost_params: MixNodeCostParams, owner_signature: String, amount: Coin, }, @@ -119,6 +124,35 @@ pub enum ExecuteMsg { }, } +impl ExecuteMsg { + pub fn name(&self) -> &str { + match self { + ExecuteMsg::TrackReward { .. } => "VestingExecuteMsg::TrackReward", + ExecuteMsg::ClaimOperatorReward { .. } => "VestingExecuteMsg::ClaimOperatorReward", + ExecuteMsg::ClaimDelegatorReward { .. } => "VestingExecuteMsg::ClaimDelegatorReward", + ExecuteMsg::UpdateMixnodeConfig { .. } => "VestingExecuteMsg::UpdateMixnodeConfig", + ExecuteMsg::UpdateMixnodeCostParams { .. } => { + "VestingExecuteMsg::UpdateMixnodeCostParams" + } + ExecuteMsg::UpdateMixnetAddress { .. } => "VestingExecuteMsg::UpdateMixnetAddress", + ExecuteMsg::DelegateToMixnode { .. } => "VestingExecuteMsg::DelegateToMixnode", + ExecuteMsg::UndelegateFromMixnode { .. } => "VestingExecuteMsg::UndelegateFromMixnode", + ExecuteMsg::CreateAccount { .. } => "VestingExecuteMsg::CreateAccount", + ExecuteMsg::WithdrawVestedCoins { .. } => "VestingExecuteMsg::WithdrawVestedCoins", + ExecuteMsg::TrackUndelegation { .. } => "VestingExecuteMsg::TrackUndelegation", + ExecuteMsg::BondMixnode { .. } => "VestingExecuteMsg::BondMixnode", + ExecuteMsg::UnbondMixnode { .. } => "VestingExecuteMsg::UnbondMixnode", + ExecuteMsg::TrackUnbondMixnode { .. } => "VestingExecuteMsg::TrackUnbondMixnode", + ExecuteMsg::BondGateway { .. } => "VestingExecuteMsg::BondGateway", + ExecuteMsg::UnbondGateway { .. } => "VestingExecuteMsg::UnbondGateway", + ExecuteMsg::TrackUnbondGateway { .. } => "VestingExecuteMsg::TrackUnbondGateway", + ExecuteMsg::TransferOwnership { .. } => "VestingExecuteMsg::TransferOwnership", + ExecuteMsg::UpdateStakingAddress { .. } => "VestingExecuteMsg::UpdateStakingAddress", + ExecuteMsg::UpdateLockedPledgeCap { .. } => "VestingExecuteMsg::UpdateLockedPledgeCap", + } + } +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryMsg { @@ -170,10 +204,10 @@ pub enum QueryMsg { GetLockedPledgeCap {}, GetDelegationTimes { address: String, - mix_identity: IdentityKey, + mix_id: NodeId, }, GetAllDelegations { - start_after: Option<(u32, IdentityKey, u64)>, + start_after: Option<(u32, NodeId, u64)>, limit: Option, }, } diff --git a/common/credential-storage/build.rs b/common/credential-storage/build.rs index 0640039329..ad3cfe4bff 100644 --- a/common/credential-storage/build.rs +++ b/common/credential-storage/build.rs @@ -11,7 +11,7 @@ async fn main() { let out_dir = env::var("OUT_DIR").unwrap(); let database_path = format!("{}/coconut-credential-example.sqlite", out_dir); - let mut conn = SqliteConnection::connect(&*format!("sqlite://{}?mode=rwc", database_path)) + let mut conn = SqliteConnection::connect(&format!("sqlite://{}?mode=rwc", database_path)) .await .expect("Failed to create SQLx database connection"); diff --git a/common/crypto/dkg/src/bte/mod.rs b/common/crypto/dkg/src/bte/mod.rs index f3b4445c54..cb3dd07fa4 100644 --- a/common/crypto/dkg/src/bte/mod.rs +++ b/common/crypto/dkg/src/bte/mod.rs @@ -144,7 +144,7 @@ impl Tau { let tau_mem = self.0.as_raw_slice(); assert_eq!(tau_mem.len(), 1, "tau length invariant was broken"); - random_oracle_builder.update(&tau_mem[0].to_be_bytes()); + random_oracle_builder.update(tau_mem[0].to_be_bytes()); let oracle_output = random_oracle_builder.finalize(); debug_assert_eq!(oracle_output.len() * 8, HASH_SECURITY_PARAM); diff --git a/common/ledger/Cargo.toml b/common/ledger/Cargo.toml new file mode 100644 index 0000000000..9bea382a73 --- /dev/null +++ b/common/ledger/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "ledger" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bip32 = "0.3.0" +k256 = "0.10.4" +ledger-transport = "0.10.0" +ledger-transport-hid = "0.10.0" +thiserror = "1" \ No newline at end of file diff --git a/common/ledger/src/addr_secp265k1.rs b/common/ledger/src/addr_secp265k1.rs new file mode 100644 index 0000000000..93fe82b8c6 --- /dev/null +++ b/common/ledger/src/addr_secp265k1.rs @@ -0,0 +1,40 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::LedgerError; +use crate::helpers::answer_bytes; +use bip32::{PublicKey, PublicKeyBytes}; +use ledger_transport::APDUAnswer; + +/// SECP265K1 address of the device. +pub struct AddrSecp265k1Response { + /// SECP265K1 public key. + pub public_key: k256::PublicKey, + /// String representation of the Cosmos address. + pub address: String, +} + +impl TryFrom>> for AddrSecp265k1Response { + type Error = LedgerError; + + fn try_from(answer: APDUAnswer>) -> Result { + let bytes = answer_bytes(&answer)?; + if bytes.len() < 33 { + return Err(Self::Error::InvalidAnswerLength { + expected: 33, + received: bytes.len(), + }); + } + + let (pub_key, addr) = bytes.split_at(33); + let public_key = k256::PublicKey::from_bytes( + PublicKeyBytes::try_from(pub_key).expect("Public key should be 33 bytes"), + )?; + let address = String::from_utf8(addr.to_vec()).unwrap(); + + Ok(AddrSecp265k1Response { + public_key, + address, + }) + } +} diff --git a/common/ledger/src/error.rs b/common/ledger/src/error.rs new file mode 100644 index 0000000000..fc4a0a1b49 --- /dev/null +++ b/common/ledger/src/error.rs @@ -0,0 +1,37 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use thiserror::Error; + +pub(crate) type Result = std::result::Result; + +/// Ledger specific errors. +#[derive(Debug, Error)] +pub enum LedgerError { + #[error("HID API - {0}")] + HidAPI(#[from] ledger_transport_hid::hidapi::HidError), + + #[error("HID transport - {0}")] + HidTransport(#[from] ledger_transport_hid::LedgerHIDError), + + #[error("Unknown error code - {err_code}")] + UnknownErrorCode { err_code: u16 }, + + #[error("APDU error - {reason}")] + APDU { reason: String }, + + #[error("Not enough bytes in answer. Expected at least {expected}, received {received}")] + InvalidAnswerLength { expected: usize, received: usize }, + + #[error("Not enough components in derivation path. Expected {expected}, received {received}")] + InvalidDerivationPath { expected: usize, received: usize }, + + #[error("Bip32 - {0}")] + Bip32(#[from] bip32::Error), + + #[error("Signature error - {0}")] + Signature(#[from] k256::ecdsa::Error), + + #[error("No message found for signing transaction")] + NoMessageFound, +} diff --git a/common/ledger/src/helpers.rs b/common/ledger/src/helpers.rs new file mode 100644 index 0000000000..df21167397 --- /dev/null +++ b/common/ledger/src/helpers.rs @@ -0,0 +1,37 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::{LedgerError, Result}; +use bip32::DerivationPath; +use ledger_transport::{APDUAnswer, APDUErrorCode}; + +pub(crate) fn answer_bytes(answer: &APDUAnswer>) -> Result<&[u8]> { + let error_code = answer + .error_code() + .map_err(|err_code| LedgerError::UnknownErrorCode { err_code })?; + match error_code { + APDUErrorCode::NoError => Ok(answer.data()), + e => Err(LedgerError::APDU { + reason: e.description(), + }), + } +} + +pub(crate) fn path_bytes(path: DerivationPath) -> Result<[[u8; 4]; 5]> { + let received = path.len(); + let components: Vec<[u8; 4]> = path.into_iter().map(|c| c.0.to_le_bytes()).collect(); + if components.len() != 5 { + Err(LedgerError::InvalidDerivationPath { + expected: 5, + received, + }) + } else { + Ok([ + components[0], + components[1], + components[2], + components[3], + components[4], + ]) + } +} diff --git a/common/ledger/src/lib.rs b/common/ledger/src/lib.rs new file mode 100644 index 0000000000..2ff4a18956 --- /dev/null +++ b/common/ledger/src/lib.rs @@ -0,0 +1,140 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod addr_secp265k1; +pub mod error; +pub(crate) mod helpers; +mod sign_secp265k1; +pub mod version; + +use crate::addr_secp265k1::AddrSecp265k1Response; +use crate::error::LedgerError; +use crate::helpers::path_bytes; +use crate::sign_secp265k1::SignSecp265k1Response; +use crate::version::VersionResponse; +use bip32::DerivationPath; +use error::Result; +use ledger_transport::APDUCommand; +use ledger_transport_hid::hidapi::HidApi; +use ledger_transport_hid::TransportNativeHID; +use std::fmt::Debug; +use std::fmt::Formatter; +use std::sync::Arc; + +const CLA: u8 = 0x55; +const INS_GET_VERSION: u8 = 0x00; +const INS_SIGN_SECP256K1: u8 = 0x02; +const INS_GET_ADDR_SECP256K1: u8 = 0x04; + +const PAYLOAD_TYPE_INIT: u8 = 0x00; +const PAYLOAD_TYPE_ADD: u8 = 0x01; +const PAYLOAD_TYPE_LAST: u8 = 0x02; +const CHUNK_SIZE: usize = 250; + +/// Manage hardware Ledger device with Cosmos specific operations, as described in the +/// specification: https://github.com/cosmos/ledger-cosmos/blob/main/docs/APDUSPEC.md +#[derive(Clone)] +pub struct CosmosLedger { + path: DerivationPath, + prefix: String, + transport: Arc, +} + +impl Debug for CosmosLedger { + fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + write!(f, "()") + } +} + +impl CosmosLedger { + /// Create the connection to the first Ledger device that we can find. + pub fn new(path: DerivationPath, prefix: String) -> Result { + let api = HidApi::new()?; + let transport = Arc::new(TransportNativeHID::new(&api)?); + + Ok(CosmosLedger { + path, + prefix, + transport, + }) + } + + /// Get the version of the device. + pub fn get_version(&self) -> Result { + let command = APDUCommand { + cla: CLA, + ins: INS_GET_VERSION, + p1: 0, + p2: 0, + data: vec![], + }; + let response = self.transport.exchange(&command)?; + VersionResponse::try_from(response) + } + + /// Get the SECP265K1 address of the device. + pub fn get_addr_secp265k1(&self, display: bool) -> Result { + let display = u8::from(display); + let components = path_bytes(self.path.clone())?; + let data: Vec = vec![ + [self.prefix.len() as u8].as_slice(), + self.prefix.as_bytes(), + components[0].as_slice(), + components[1].as_slice(), + components[2].as_slice(), + components[3].as_slice(), + components[4].as_slice(), + ] + .into_iter() + .flatten() + .copied() + .collect(); + + let command = APDUCommand { + cla: CLA, + ins: INS_GET_ADDR_SECP256K1, + p1: display, + p2: 0, + data, + }; + let response = self.transport.exchange(&command)?; + AddrSecp265k1Response::try_from(response) + } + + pub fn sign_secp265k1(&self, message: String) -> Result { + let serialized_path: Vec = path_bytes(self.path.clone())? + .into_iter() + .flatten() + .collect(); + let mut chunks = vec![serialized_path]; + if message.is_empty() { + return Err(LedgerError::NoMessageFound); + } + for chunk in message.into_bytes().chunks(CHUNK_SIZE) { + chunks.push(chunk.to_vec()); + } + let length = chunks.len(); + for (idx, chunk) in chunks.into_iter().enumerate() { + let payload_desc = if idx == 0 { + PAYLOAD_TYPE_INIT + } else if idx + 1 == length { + PAYLOAD_TYPE_LAST + } else { + PAYLOAD_TYPE_ADD + }; + let command = APDUCommand { + cla: CLA, + ins: INS_SIGN_SECP256K1, + p1: payload_desc, + p2: 0, + data: chunk, + }; + let sign_response = self.transport.exchange(&command)?; + if payload_desc == PAYLOAD_TYPE_LAST { + return SignSecp265k1Response::try_from(sign_response); + } + } + // It should never reach this, as the message is not empty + Err(LedgerError::NoMessageFound) + } +} diff --git a/common/ledger/src/sign_secp265k1.rs b/common/ledger/src/sign_secp265k1.rs new file mode 100644 index 0000000000..25596d6492 --- /dev/null +++ b/common/ledger/src/sign_secp265k1.rs @@ -0,0 +1,25 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::LedgerError; +use crate::helpers::answer_bytes; +use k256::ecdsa::Signature; +use ledger_transport::APDUAnswer; + +/// Version and status data of the device. +pub struct SignSecp265k1Response { + /// DER encoded signature data + pub signature: Signature, +} + +impl TryFrom>> for SignSecp265k1Response { + type Error = LedgerError; + + fn try_from(answer: APDUAnswer>) -> Result { + let bytes = answer_bytes(&answer)?; + + Ok(SignSecp265k1Response { + signature: Signature::from_der(bytes)?, + }) + } +} diff --git a/common/ledger/src/version.rs b/common/ledger/src/version.rs new file mode 100644 index 0000000000..c2ee169958 --- /dev/null +++ b/common/ledger/src/version.rs @@ -0,0 +1,42 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::LedgerError; +use crate::helpers::answer_bytes; +use ledger_transport::APDUAnswer; + +/// Version and status data of the device. +pub struct VersionResponse { + /// Activation status of test mode. + pub test_mode: bool, + /// Major part of Cosmos application version. + pub major: u8, + /// Minor part of Cosmos application version. + pub minor: u8, + /// Patch part of Cosmos application version. + pub patch: u8, + /// PIN locked status. + pub device_locked: bool, +} + +impl TryFrom>> for VersionResponse { + type Error = LedgerError; + + fn try_from(answer: APDUAnswer>) -> Result { + let bytes = answer_bytes(&answer)?; + if bytes.len() != 5 { + return Err(Self::Error::InvalidAnswerLength { + expected: 5, + received: bytes.len(), + }); + } + + Ok(VersionResponse { + test_mode: bytes[0] != 0, + major: bytes[1], + minor: bytes[2], + patch: bytes[3], + device_locked: bytes[4] != 0, + }) + } +} diff --git a/common/mixnode-common/src/verloc/mod.rs b/common/mixnode-common/src/verloc/mod.rs index 610a7a1a66..48e9a7b483 100644 --- a/common/mixnode-common/src/verloc/mod.rs +++ b/common/mixnode-common/src/verloc/mod.rs @@ -321,17 +321,18 @@ impl VerlocMeasurer { let tested_nodes = all_mixes .into_iter() .filter_map(|node| { + let mix_node = node.bond_information.mix_node; // check if the node has sufficient version to be able to understand the packets - let node_version = parse_version(&node.mix_node.version).ok()?; + let node_version = parse_version(&mix_node.version).ok()?; if node_version < self.config.minimum_compatible_node_version { return None; } // try to parse the identity and host let node_identity = - identity::PublicKey::from_base58_string(node.mix_node.identity_key).ok()?; + identity::PublicKey::from_base58_string(mix_node.identity_key).ok()?; - let verloc_host = (&*node.mix_node.host, node.mix_node.verloc_port) + let verloc_host = (&*mix_node.host, mix_node.verloc_port) .to_socket_addrs() .ok()? .next()?; diff --git a/common/nymsphinx/src/receiver.rs b/common/nymsphinx/src/receiver.rs index 1707983ca3..0b6a8e96dd 100644 --- a/common/nymsphinx/src/receiver.rs +++ b/common/nymsphinx/src/receiver.rs @@ -203,9 +203,8 @@ mod message_receiver { mixes.insert( 1, vec![mix::Node { + mix_id: 123, owner: "foomp1".to_string(), - stake: 123, - delegation: 456, host: "10.20.30.40".parse().unwrap(), mix_host: "10.20.30.40:1789".parse().unwrap(), identity_key: identity::PublicKey::from_base58_string( @@ -224,9 +223,8 @@ mod message_receiver { mixes.insert( 2, vec![mix::Node { + mix_id: 234, owner: "foomp2".to_string(), - stake: 123, - delegation: 456, host: "11.21.31.41".parse().unwrap(), mix_host: "11.21.31.41:1789".parse().unwrap(), identity_key: identity::PublicKey::from_base58_string( @@ -245,9 +243,8 @@ mod message_receiver { mixes.insert( 3, vec![mix::Node { + mix_id: 456, owner: "foomp3".to_string(), - stake: 123, - delegation: 456, host: "12.22.32.42".parse().unwrap(), mix_host: "12.22.32.42:1789".parse().unwrap(), identity_key: identity::PublicKey::from_base58_string( diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 7d5b9843d4..e21e074d34 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -3,7 +3,8 @@ use crate::filter::VersionFilterable; use log::warn; -use mixnet_contract_common::{GatewayBond, MixNodeBond}; +use mixnet_contract_common::mixnode::MixNodeDetails; +use mixnet_contract_common::GatewayBond; use nymsphinx_addressing::nodes::NodeIdentity; use nymsphinx_types::Node as SphinxNode; use rand::Rng; @@ -224,12 +225,15 @@ impl NymTopology { } } -pub fn nym_topology_from_bonds( - mix_bonds: Vec, +pub fn nym_topology_from_detailed( + mix_details: Vec, gateway_bonds: Vec, ) -> NymTopology { let mut mixes = HashMap::new(); - for bond in mix_bonds.into_iter() { + for bond in mix_details + .into_iter() + .map(|details| details.bond_information) + { let layer = bond.layer as MixLayer; if layer == 0 || layer > 3 { warn!( @@ -238,13 +242,14 @@ pub fn nym_topology_from_bonds( ); continue; } - let mix_id = bond.mix_node.identity_key.clone(); + let mix_id = bond.id; + let mix_identity = bond.mix_node.identity_key.clone(); let layer_entry = mixes.entry(layer).or_insert_with(Vec::new); match bond.try_into() { Ok(mix) => layer_entry.push(mix), Err(err) => { - warn!("Mix {} is malformed - {}", mix_id, err); + warn!("Mix {} / {} is malformed - {}", mix_id, mix_identity, err); continue; } } @@ -279,9 +284,8 @@ mod converting_mixes_to_vec { #[test] fn returns_a_vec_with_hashmap_values() { let node1 = mix::Node { + mix_id: 42, owner: "N/A".to_string(), - stake: 0, - delegation: 0, host: "3.3.3.3".parse().unwrap(), mix_host: "3.3.3.3:1789".parse().unwrap(), identity_key: identity::PublicKey::from_base58_string( diff --git a/common/topology/src/mix.rs b/common/topology/src/mix.rs index 9d3eaf3b65..aa071c4fe2 100644 --- a/common/topology/src/mix.rs +++ b/common/topology/src/mix.rs @@ -3,7 +3,7 @@ use crate::{filter, NetworkAddress}; use crypto::asymmetric::{encryption, identity}; -use mixnet_contract_common::{Layer, MixNodeBond}; +use mixnet_contract_common::{Layer, MixNodeBond, NodeId}; use nymsphinx_addressing::nodes::NymNodeRoutingAddress; use nymsphinx_types::Node as SphinxNode; use std::convert::{TryFrom, TryInto}; @@ -68,11 +68,8 @@ impl Display for MixnodeConversionError { #[derive(Debug, Clone)] pub struct Node { + pub mix_id: NodeId, pub owner: String, - // somebody correct me if I'm wrong, but we should only ever have a single denom of currency - // on the network at a type, right? - pub stake: u128, - pub delegation: u128, pub host: NetworkAddress, // we're keeping this as separate resolved field since we do not want to be resolving the potential // hostname every time we want to construct a path via this node @@ -116,9 +113,8 @@ impl<'a> TryFrom<&'a MixNodeBond> for Node { })?[0]; Ok(Node { + mix_id: bond.id, owner: bond.owner.as_str().to_owned(), - stake: bond.pledge_amount.amount.into(), - delegation: bond.total_delegation.amount.into(), host, mix_host, identity_key: identity::PublicKey::from_base58_string(&bond.mix_node.identity_key)?, diff --git a/common/types/Cargo.toml b/common/types/Cargo.toml index 8e9d7c3edf..54603c02d2 100644 --- a/common/types/Cargo.toml +++ b/common/types/Cargo.toml @@ -19,7 +19,7 @@ thiserror = "1.0" url = "2.2" ts-rs = "6.1.2" -cosmwasm-std = "1.0.0-beta8" +cosmwasm-std = "1.0.0" cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support" } validator-client = { path = "../../common/client-libs/validator-client", features = [ @@ -37,4 +37,4 @@ tempfile = "3.3.0" [features] default = [] -generate-ts = [] +generate-ts = ["mixnet-contract-common/generate-ts"] diff --git a/common/types/src/currency.rs b/common/types/src/currency.rs index d2bafe589c..e6927a1721 100644 --- a/common/types/src/currency.rs +++ b/common/types/src/currency.rs @@ -71,6 +71,50 @@ impl RegisteredCoins { self.0.remove(denom) } + pub fn attempt_create_display_coin_from_base_dec_amount( + &self, + denom: &Denom, + base_amount: Decimal, + ) -> Result { + for registered_coin in self.0.values() { + if let Some(exponent) = registered_coin.get_exponent(denom) { + // if this fails it means we haven't registered our display denom which honestly should never be the case + // unless somebody is rocking their own custom network + let display_exponent = registered_coin + .get_exponent(®istered_coin.display) + .ok_or_else(|| TypesError::UnknownCoinDenom(denom.clone()))?; + + return match exponent.cmp(&display_exponent) { + Ordering::Greater => { + // we need to scale up, unlikely to ever be needed but included for completion sake, + // for example if we decided to created knym with exponent 9 and wanted to convert to nym with exponent 6 + Ok(DecCoin { + denom: denom.into(), + amount: try_scale_up_decimal(base_amount, exponent - display_exponent)?, + }) + } + // we're already in the display denom + Ordering::Equal => Ok(DecCoin { + denom: denom.into(), + amount: base_amount, + }), + Ordering::Less => { + // we need to scale down, the most common case, for example we're in base unym with exponent 0 and want to convert to nym with exponent 6 + Ok(DecCoin { + denom: denom.into(), + amount: try_scale_down_decimal( + base_amount, + display_exponent - exponent, + )?, + }) + } + }; + } + } + + Err(TypesError::UnknownCoinDenom(denom.clone())) + } + pub fn attempt_convert_to_base_coin(&self, coin: DecCoin) -> Result { // check if this is already in the base denom if self.0.contains_key(&coin.denom) { diff --git a/common/types/src/delegation.rs b/common/types/src/delegation.rs index 0f5c5c0c96..e2a60b002c 100644 --- a/common/types/src/delegation.rs +++ b/common/types/src/delegation.rs @@ -1,8 +1,9 @@ use crate::currency::{DecCoin, RegisteredCoins}; +use crate::deprecated::DelegationEvent; use crate::error::TypesError; -use mixnet_contract_common::mixnode::DelegationEvent as ContractDelegationEvent; -use mixnet_contract_common::mixnode::PendingUndelegate as ContractPendingUndelegate; -use mixnet_contract_common::Delegation as MixnetContractDelegation; +use crate::mixnode::MixNodeCostParams; +use cosmwasm_std::Decimal; +use mixnet_contract_common::{Delegation as MixnetContractDelegation, NodeId}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -14,9 +15,9 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] pub struct Delegation { pub owner: String, - pub node_identity: String, + pub mix_id: NodeId, pub amount: DecCoin, - pub block_height: u64, + pub height: u64, pub proxy: Option, // proxy address used to delegate the funds on behalf of another address } @@ -27,48 +28,40 @@ impl Delegation { ) -> Result { Ok(Delegation { owner: delegation.owner.to_string(), - node_identity: delegation.node_identity, + mix_id: delegation.node_id, amount: reg.attempt_convert_to_display_dec_coin(delegation.amount.into())?, - block_height: delegation.block_height, + height: delegation.height, proxy: delegation.proxy.map(|d| d.to_string()), }) } } -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/DelegationRecord.ts") -)] -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] -pub struct DelegationRecord { - pub amount: DecCoin, - pub block_height: u64, - pub delegated_on_iso_datetime: String, - pub uses_vesting_contract_tokens: bool, -} - #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", ts(export_to = "ts-packages/types/src/types/rust/DelegationWithEverything.ts") )] -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] pub struct DelegationWithEverything { pub owner: String, + pub mix_id: NodeId, pub node_identity: String, pub amount: DecCoin, - pub total_delegation: Option, - pub pledge_amount: Option, + pub accumulated_by_delegates: Option, + pub accumulated_by_operator: Option, pub block_height: u64, pub delegated_on_iso_datetime: String, - pub profit_margin_percent: Option, + pub cost_params: Option, pub avg_uptime_percent: Option, - pub stake_saturation: Option, + + #[cfg_attr(feature = "generate-ts", ts(type = "string | null"))] + pub stake_saturation: Option, + pub uses_vesting_contract_tokens: bool, - pub accumulated_rewards: Option, + pub unclaimed_rewards: Option, + + // DEPRECATED, IF POSSIBLE TRY TO DISCONTINUE USE OF IT! pub pending_events: Vec, - pub history: Vec, } #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] @@ -83,82 +76,6 @@ pub struct DelegationResult { amount: Option, } -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/DelegationEventKind.ts") -)] -#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Debug)] -pub enum DelegationEventKind { - Delegate, - Undelegate, -} - -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/DelegationEvent.ts") -)] -#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Debug)] -pub struct DelegationEvent { - pub kind: DelegationEventKind, - pub node_identity: String, - pub address: String, - pub amount: Option, - pub block_height: u64, - pub proxy: Option, -} - -impl DelegationEvent { - pub fn from_mixnet_contract( - event: ContractDelegationEvent, - reg: &RegisteredCoins, - ) -> Result { - Ok(match event { - ContractDelegationEvent::Delegate(delegation) => DelegationEvent { - kind: DelegationEventKind::Delegate, - block_height: delegation.block_height, - address: delegation.owner.into_string(), - node_identity: delegation.node_identity, - amount: Some(reg.attempt_convert_to_display_dec_coin(delegation.amount.into())?), - proxy: delegation.proxy.map(|p| p.into_string()), - }, - ContractDelegationEvent::Undelegate(pending_undelegate) => DelegationEvent { - kind: DelegationEventKind::Undelegate, - block_height: pending_undelegate.block_height(), - address: pending_undelegate.delegate().into_string(), - node_identity: pending_undelegate.mix_identity(), - amount: None, - proxy: pending_undelegate.proxy().map(|p| p.into_string()), - }, - }) - } -} - -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/PendingUndelegate.ts") -)] -#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -pub struct PendingUndelegate { - mix_identity: String, - delegate: String, - proxy: Option, - block_height: u64, -} - -impl From for PendingUndelegate { - fn from(pending_undelegate: ContractPendingUndelegate) -> Self { - PendingUndelegate { - mix_identity: pending_undelegate.mix_identity(), - delegate: pending_undelegate.delegate().to_string(), - proxy: pending_undelegate.proxy().map(|p| p.to_string()), - block_height: pending_undelegate.block_height(), - } - } -} - #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", diff --git a/common/types/src/deprecated.rs b/common/types/src/deprecated.rs new file mode 100644 index 0000000000..a40dea8c4c --- /dev/null +++ b/common/types/src/deprecated.rs @@ -0,0 +1,109 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::currency::DecCoin; +use crate::error::TypesError; +use crate::pending_events::{PendingEpochEvent, PendingEpochEventData}; +use mixnet_contract_common::{IdentityKey, NodeId}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/DelegationEventKind.ts") +)] +#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Debug)] +pub enum DelegationEventKind { + Delegate, + Undelegate, +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/DelegationEvent.ts") +)] +#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Debug)] +pub struct DelegationEvent { + pub kind: DelegationEventKind, + pub mix_id: NodeId, + pub address: String, + pub amount: Option, + pub proxy: Option, +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/WrappedDelegationEvent.ts") +)] +#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Debug)] +pub struct WrappedDelegationEvent { + pub event: DelegationEvent, + pub node_identity: IdentityKey, +} + +impl WrappedDelegationEvent { + pub fn new(event: DelegationEvent, node_identity: IdentityKey) -> Self { + Self { + event, + node_identity, + } + } +} + +impl DelegationEvent { + pub fn address_matches(&self, address: &str) -> bool { + self.address == address + } +} + +pub fn convert_to_delegation_events(epoch_events: Vec) -> Vec { + epoch_events + .into_iter() + // filter out all events that are not delegation-related + .filter_map(|e| e.try_into().ok()) + .collect() +} + +impl TryFrom for DelegationEvent { + type Error = TypesError; + + fn try_from(value: PendingEpochEvent) -> Result { + value.event.try_into() + } +} + +impl TryFrom for DelegationEvent { + type Error = TypesError; + + fn try_from(value: PendingEpochEventData) -> Result { + match value { + PendingEpochEventData::Delegate { + owner, + mix_id, + amount, + proxy, + } => Ok(DelegationEvent { + kind: DelegationEventKind::Delegate, + address: owner, + mix_id, + proxy, + amount: Some(amount), + }), + PendingEpochEventData::Undelegate { + owner, + mix_id, + proxy, + } => Ok(DelegationEvent { + kind: DelegationEventKind::Undelegate, + address: owner, + mix_id, + proxy, + amount: None, + }), + _ => Err(TypesError::NotADelegationEvent), + } + } +} diff --git a/common/types/src/error.rs b/common/types/src/error.rs index 20c3736152..a49f7587e2 100644 --- a/common/types/src/error.rs +++ b/common/types/src/error.rs @@ -70,6 +70,8 @@ pub enum TypesError { LossyCoinConversion, #[error("The provided coin has an unknown denomination - {0}")] UnknownCoinDenom(String), + #[error("Provided event is not a delegation event")] + NotADelegationEvent, } impl Serialize for TypesError { diff --git a/common/types/src/lib.rs b/common/types/src/lib.rs index 8a64ecac07..86aee3b75c 100644 --- a/common/types/src/lib.rs +++ b/common/types/src/lib.rs @@ -1,10 +1,15 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + pub mod account; pub mod currency; pub mod delegation; +pub mod deprecated; pub mod error; pub mod fees; pub mod gas; pub mod gateway; pub mod mixnode; +pub mod pending_events; pub mod transaction; pub mod vesting; diff --git a/common/types/src/mixnode.rs b/common/types/src/mixnode.rs index d1a302d489..5535ec97f8 100644 --- a/common/types/src/mixnode.rs +++ b/common/types/src/mixnode.rs @@ -1,53 +1,44 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + use crate::currency::{DecCoin, RegisteredCoins}; use crate::error::TypesError; +use cosmwasm_std::Decimal; use mixnet_contract_common::{ - MixNode as MixnetContractMixNode, MixNodeBond as MixnetContractMixNodeBond, + EpochId, MixNode, MixNodeBond as MixnetContractMixNodeBond, + MixNodeCostParams as MixnetContractMixNodeCostParams, + MixNodeDetails as MixnetContractMixNodeDetails, + MixNodeRewarding as MixnetContractMixNodeRewarding, NodeId, Percent, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use validator_client::nymd::Coin; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/Mixnode.ts") + ts(export_to = "ts-packages/types/src/types/rust/MixNodeDetails.ts") )] -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Serialize, JsonSchema)] -pub struct MixNode { - pub host: String, - pub mix_port: u16, - pub verloc_port: u16, - pub http_api_port: u16, - pub sphinx_key: String, - /// Base58 encoded ed25519 EdDSA public key. - pub identity_key: String, - pub version: String, - pub profit_margin_percent: u8, +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] +pub struct MixNodeDetails { + pub bond_information: MixNodeBond, + pub rewarding_details: MixNodeRewarding, } -impl From for MixNode { - fn from(value: MixnetContractMixNode) -> Self { - let MixnetContractMixNode { - host, - mix_port, - verloc_port, - http_api_port, - sphinx_key, - identity_key, - version, - profit_margin_percent, - } = value; - - Self { - host, - mix_port, - verloc_port, - http_api_port, - sphinx_key, - identity_key, - version, - profit_margin_percent, - } +impl MixNodeDetails { + pub fn from_mixnet_contract_mixnode_details( + details: MixnetContractMixNodeDetails, + reg: &RegisteredCoins, + ) -> Result { + Ok(MixNodeDetails { + bond_information: MixNodeBond::from_mixnet_contract_mixnode_bond( + details.bond_information, + reg, + )?, + rewarding_details: MixNodeRewarding::from_mixnet_contract_mixnode_rewarding( + details.rewarding_details, + reg, + )?, + }) } } @@ -58,14 +49,14 @@ impl From for MixNode { )] #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] pub struct MixNodeBond { - pub pledge_amount: DecCoin, - pub total_delegation: DecCoin, + pub id: NodeId, pub owner: String, + pub original_pledge: DecCoin, pub layer: String, - pub block_height: u64, pub mix_node: MixNode, pub proxy: Option, - pub accumulated_rewards: Option, + pub bonding_height: u64, + pub is_unbonding: bool, } impl MixNodeBond { @@ -73,24 +64,100 @@ impl MixNodeBond { bond: MixnetContractMixNodeBond, reg: &RegisteredCoins, ) -> Result { - let denom = bond.pledge_amount.denom.clone(); Ok(MixNodeBond { - pledge_amount: reg.attempt_convert_to_display_dec_coin(bond.pledge_amount.into())?, - total_delegation: reg - .attempt_convert_to_display_dec_coin(bond.total_delegation.into())?, + id: bond.id, owner: bond.owner.into_string(), + original_pledge: reg + .attempt_convert_to_display_dec_coin(bond.original_pledge.into())?, layer: bond.layer.into(), - block_height: bond.block_height, - mix_node: bond.mix_node.into(), - proxy: bond.proxy.map(|p| p.to_string()), - accumulated_rewards: bond - .accumulated_rewards - .map(|reward| { - // here we're making an assumption that rewards always use the same denom as the pledge - // (which I think is a reasonable assumption) - reg.attempt_convert_to_display_dec_coin(Coin::new(reward.u128(), denom)) - }) - .transpose()?, + mix_node: bond.mix_node, + proxy: bond.proxy.map(|p| p.into_string()), + bonding_height: bond.bonding_height, + is_unbonding: bond.is_unbonding, + }) + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/MixNodeRewarding.ts") +)] +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] +pub struct MixNodeRewarding { + pub cost_params: MixNodeCostParams, + + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub operator: Decimal, + + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub delegates: Decimal, + + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub total_unit_reward: Decimal, + + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub unit_delegation: Decimal, + + pub last_rewarded_epoch: EpochId, + + pub unique_delegations: u32, +} + +impl MixNodeRewarding { + pub fn from_mixnet_contract_mixnode_rewarding( + mix_rewarding: MixnetContractMixNodeRewarding, + reg: &RegisteredCoins, + ) -> Result { + Ok(MixNodeRewarding { + cost_params: MixNodeCostParams::from_mixnet_contract_mixnode_cost_params( + mix_rewarding.cost_params, + reg, + )?, + operator: mix_rewarding.operator, + delegates: mix_rewarding.delegates, + total_unit_reward: mix_rewarding.total_unit_reward, + unit_delegation: mix_rewarding.unit_delegation, + last_rewarded_epoch: mix_rewarding.last_rewarded_epoch, + unique_delegations: mix_rewarding.unique_delegations, + }) + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/MixNodeCostParams.ts") +)] +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] +pub struct MixNodeCostParams { + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub profit_margin_percent: Percent, + + pub interval_operating_cost: DecCoin, +} + +impl MixNodeCostParams { + pub fn from_mixnet_contract_mixnode_cost_params( + cost_params: MixnetContractMixNodeCostParams, + reg: &RegisteredCoins, + ) -> Result { + Ok(MixNodeCostParams { + profit_margin_percent: cost_params.profit_margin_percent, + interval_operating_cost: reg + .attempt_convert_to_display_dec_coin(cost_params.interval_operating_cost.into())?, + }) + } + + pub fn try_convert_to_mixnet_contract_cost_params( + self, + reg: &RegisteredCoins, + ) -> Result { + Ok(MixnetContractMixNodeCostParams { + profit_margin_percent: self.profit_margin_percent, + interval_operating_cost: reg + .attempt_convert_to_base_coin(self.interval_operating_cost)? + .into(), }) } } diff --git a/common/types/src/pending_events.rs b/common/types/src/pending_events.rs new file mode 100644 index 0000000000..65f157abd7 --- /dev/null +++ b/common/types/src/pending_events.rs @@ -0,0 +1,172 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::currency::{DecCoin, RegisteredCoins}; +use crate::error::TypesError; +use crate::mixnode::MixNodeCostParams; +use mixnet_contract_common::{ + EpochEventId, IntervalEventId, IntervalRewardingParamsUpdate, NodeId, + PendingEpochEvent as MixnetContractPendingEpochEvent, + PendingEpochEventData as MixnetContractPendingEpochEventData, + PendingIntervalEvent as MixnetContractPendingIntervalEvent, + PendingIntervalEventData as MixnetContractPendingIntervalEventData, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/PendingEpochEvent.ts") +)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] +pub struct PendingEpochEvent { + pub id: EpochEventId, + pub event: PendingEpochEventData, +} + +impl PendingEpochEvent { + pub fn try_from_mixnet_contract( + pending_event: MixnetContractPendingEpochEvent, + reg: &RegisteredCoins, + ) -> Result { + Ok(PendingEpochEvent { + id: pending_event.id, + event: PendingEpochEventData::try_from_mixnet_contract(pending_event.event, reg)?, + }) + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/PendingEpochEventData.ts") +)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] +pub enum PendingEpochEventData { + Delegate { + owner: String, + mix_id: NodeId, + amount: DecCoin, + proxy: Option, + }, + Undelegate { + owner: String, + mix_id: NodeId, + proxy: Option, + }, + UnbondMixnode { + mix_id: NodeId, + }, + UpdateActiveSetSize { + new_size: u32, + }, +} + +impl PendingEpochEventData { + pub fn try_from_mixnet_contract( + pending_event: MixnetContractPendingEpochEventData, + reg: &RegisteredCoins, + ) -> Result { + match pending_event { + MixnetContractPendingEpochEventData::Delegate { + owner, + mix_id, + amount, + proxy, + } => Ok(PendingEpochEventData::Delegate { + owner: owner.into_string(), + mix_id, + amount: reg.attempt_convert_to_display_dec_coin(amount.into())?, + proxy: proxy.map(|p| p.into_string()), + }), + MixnetContractPendingEpochEventData::Undelegate { + owner, + mix_id, + proxy, + } => Ok(PendingEpochEventData::Undelegate { + owner: owner.into_string(), + mix_id, + proxy: proxy.map(|p| p.into_string()), + }), + MixnetContractPendingEpochEventData::UnbondMixnode { mix_id } => { + Ok(PendingEpochEventData::UnbondMixnode { mix_id }) + } + MixnetContractPendingEpochEventData::UpdateActiveSetSize { new_size } => { + Ok(PendingEpochEventData::UpdateActiveSetSize { new_size }) + } + } + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/PendingIntervalEvent.ts") +)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] +pub struct PendingIntervalEvent { + pub id: IntervalEventId, + pub event: PendingIntervalEventData, +} + +impl PendingIntervalEvent { + pub fn try_from_mixnet_contract( + pending_event: MixnetContractPendingIntervalEvent, + reg: &RegisteredCoins, + ) -> Result { + Ok(PendingIntervalEvent { + id: pending_event.id, + event: PendingIntervalEventData::try_from_mixnet_contract(pending_event.event, reg)?, + }) + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/PendingIntervalEventData.ts") +)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] +pub enum PendingIntervalEventData { + ChangeMixCostParams { + mix_id: NodeId, + new_costs: MixNodeCostParams, + }, + + UpdateRewardingParams { + update: IntervalRewardingParamsUpdate, + }, + UpdateIntervalConfig { + epochs_in_interval: u32, + epoch_duration_secs: u64, + }, +} + +impl PendingIntervalEventData { + pub fn try_from_mixnet_contract( + pending_event: MixnetContractPendingIntervalEventData, + reg: &RegisteredCoins, + ) -> Result { + match pending_event { + MixnetContractPendingIntervalEventData::ChangeMixCostParams { mix_id, new_costs } => { + Ok(PendingIntervalEventData::ChangeMixCostParams { + mix_id, + new_costs: MixNodeCostParams::from_mixnet_contract_mixnode_cost_params( + new_costs, reg, + )?, + }) + } + MixnetContractPendingIntervalEventData::UpdateRewardingParams { update } => { + Ok(PendingIntervalEventData::UpdateRewardingParams { update }) + } + MixnetContractPendingIntervalEventData::UpdateIntervalConfig { + epochs_in_interval, + epoch_duration_secs, + } => Ok(PendingIntervalEventData::UpdateIntervalConfig { + epochs_in_interval, + epoch_duration_secs, + }), + } + } +} diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index d7979b69f5..c0609e5ea2 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -15,20 +15,11 @@ dependencies = [ "opaque-debug 0.3.0", ] -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "anyhow" -version = "1.0.64" +version = "1.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9a8f622bcf6ff3df478e9deba3e03e4e04b300f8e6a139e192c05fa3490afc7" +checksum = "84450d0b4a8bd1ba4144ce8ce718fbc5d071358b1e5384bace6536b3d1f2d5b3" [[package]] name = "arrayref" @@ -38,15 +29,9 @@ checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "az" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "bandwidth-claim-contract" @@ -70,9 +55,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "base64ct" -version = "1.5.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2b2456fd614d856680dcd9fcc660a51a820fa09daef2e49772b56a193c8474" +checksum = "dea908e7347a8c64e378c17e30ef880ad73e3b4498346b055c2c00ea342f3179" [[package]] name = "bitflags" @@ -98,16 +83,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array 0.14.6", -] - -[[package]] -name = "block-buffer" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" -dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.5", ] [[package]] @@ -118,9 +94,9 @@ checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" [[package]] name = "bumpalo" -version = "3.11.0" +version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" [[package]] name = "byte-tools" @@ -128,12 +104,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" -[[package]] -name = "bytemuck" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da" - [[package]] name = "byteorder" version = "1.4.3" @@ -142,15 +112,15 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.2.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cc" -version = "1.0.73" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" dependencies = [ "jobserver", ] @@ -173,16 +143,14 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.22" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" dependencies = [ - "iana-time-zone", - "js-sys", + "libc", "num-integer", "num-traits", - "time 0.1.44", - "wasm-bindgen", + "time 0.1.43", "winapi", ] @@ -192,7 +160,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.5", ] [[package]] @@ -242,45 +210,27 @@ dependencies = [ "thiserror", ] -[[package]] -name = "config" -version = "0.1.0" -dependencies = [ - "cfg-if", - "handlebars", - "log", - "network-defaults", - "serde", - "toml", - "url", -] - [[package]] name = "const-oid" -version = "0.9.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "722e23542a15cea1f65d4a1419c4cfd7a26706c70871a13a04238ca3f40f1661" +checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" [[package]] name = "contracts-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "serde", ] -[[package]] -name = "core-foundation-sys" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" - [[package]] name = "cosmwasm-crypto" -version = "1.1.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16c50d753d44148c7ff3279ac44b87b5b91e790f4c70db0e3900df211310a2bf" +checksum = "5eb0afef2325df81aadbf9be1233f522ed8f6e91df870c764bc44cca2b1415bd" dependencies = [ - "digest 0.10.3", + "digest 0.9.0", "ed25519-zebra", "k256", "rand_core 0.6.3", @@ -289,47 +239,32 @@ dependencies = [ [[package]] name = "cosmwasm-derive" -version = "1.1.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9053ebe2ad85831e9f9e2124fa2b22807528a78b25cc447483ce2a4aadfba394" +checksum = "4b36e527620a2a3e00e46b6e731ab6c9b68d11069c986f7d7be8eba79ef081a4" dependencies = [ "syn", ] [[package]] name = "cosmwasm-schema" -version = "1.1.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c742fc698a88cf02ea304cc2b5bc18ef975c5bb9eff93c3e44d2cd565e1d458" +checksum = "772e80bbad231a47a2068812b723a1ff81dd4a0d56c9391ac748177bea3a61da" dependencies = [ - "cosmwasm-schema-derive", "schemars", - "serde", "serde_json", - "thiserror", -] - -[[package]] -name = "cosmwasm-schema-derive" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a7c4c07be11add09dd3af3064c4f4cbc2dc99c6859129bdaf820131730e996" -dependencies = [ - "proc-macro2", - "quote", - "syn", ] [[package]] name = "cosmwasm-std" -version = "1.1.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb8da0ae28693d892af2944319b48adc23c42725dc0fe7271b8baa38c10865da" +checksum = "875994993c2082a6fcd406937bf0fca21c349e4a624f3810253a14fa83a3a195" dependencies = [ "base64", "cosmwasm-crypto", "cosmwasm-derive", - "derivative", "forward_ref", "schemars", "serde", @@ -340,9 +275,9 @@ dependencies = [ [[package]] name = "cosmwasm-storage" -version = "1.1.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3205d5210ba4c7b7cc7329b9607c37d00dc08263c2479fd99541a2194ebe3b22" +checksum = "d18403b07304d15d304dad11040d45bbcaf78d603b4be3fb5e2685c16f9229b5" dependencies = [ "cosmwasm-std", "serde", @@ -350,9 +285,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" dependencies = [ "libc", ] @@ -378,26 +313,16 @@ dependencies = [ [[package]] name = "crypto-bigint" -version = "0.4.8" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f2b443d17d49dad5ef0ede301c3179cc923b8822f3393b4d2c28c269dd4a122" +checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.5", "rand_core 0.6.3", "subtle 2.4.1", "zeroize", ] -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array 0.14.6", - "typenum", -] - [[package]] name = "crypto-mac" version = "0.7.0" @@ -414,7 +339,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.5", "subtle 2.4.1", ] @@ -456,9 +381,9 @@ dependencies = [ [[package]] name = "cw-multi-test" -version = "0.13.4" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f9a8ab7c3c29ec93cb7a39ce4b14a05e053153b4a17ef7cf2246af1b7c087e" +checksum = "cbea57e5be4a682268a5eca1a57efece57a54ff216bfd87603d5e864aad40e12" dependencies = [ "anyhow", "cosmwasm-std", @@ -586,12 +511,11 @@ dependencies = [ [[package]] name = "der" -version = "0.6.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dd2ae565c0a381dde7fade45fce95984c568bdcb4700a4fdbe3175e0380b2f" +checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" dependencies = [ "const-oid", - "zeroize", ] [[package]] @@ -620,37 +544,20 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.5", ] -[[package]] -name = "digest" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" -dependencies = [ - "block-buffer 0.10.3", - "crypto-common", - "subtle 2.4.1", -] - -[[package]] -name = "dotenv" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" - [[package]] name = "dyn-clone" -version = "1.0.9" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f94fa09c2aeea5b8839e414b7b841bf429fd25b9c522116ac97ee87856d88b2" +checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf" [[package]] name = "ecdsa" -version = "0.14.5" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1826508d57f3140a2e8e3c307b19915a266c92a1b8c2f6bb54e29e5d72a394ae" +checksum = "d0d69ae62e0ce582d56380743515fefaf1a8c70cec685d9677636d7e30ae9dc9" dependencies = [ "der", "elliptic-curve", @@ -660,9 +567,9 @@ dependencies = [ [[package]] name = "ed25519" -version = "1.5.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" +checksum = "eed12bbf7b5312f8da1c2722bc06d8c6b12c2d86a7fb35a194c7f3e6fc2bbe39" dependencies = [ "signature", ] @@ -677,7 +584,7 @@ dependencies = [ "ed25519", "rand", "serde", - "sha2 0.9.9", + "sha2", "zeroize", ] @@ -691,31 +598,29 @@ dependencies = [ "hex", "rand_core 0.6.3", "serde", - "sha2 0.9.9", + "sha2", "thiserror", "zeroize", ] [[package]] name = "either" -version = "1.8.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "elliptic-curve" -version = "0.12.3" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +checksum = "25b477563c2bfed38a3b7a60964c49e058b2510ad3f12ba3483fd8f62c2306d6" dependencies = [ "base16ct", "crypto-bigint", "der", - "digest 0.10.3", "ff", - "generic-array 0.14.6", + "generic-array 0.14.5", "group", - "pkcs8", "rand_core 0.6.3", "sec1", "subtle 2.4.1", @@ -724,18 +629,18 @@ dependencies = [ [[package]] name = "enum-iterator" -version = "0.8.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2953d1df47ac0eb70086ccabf0275aa8da8591a28bd358ee2b52bd9f9e3ff9e9" +checksum = "4eeac5c5edb79e4e39fe8439ef35207780a11f69c52cbe424ce3dfad4cb78de6" dependencies = [ "enum-iterator-derive", ] [[package]] name = "enum-iterator-derive" -version = "0.8.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8958699f9359f0b04e691a13850d48b7de329138023876d07cbd024c2c820598" +checksum = "c134c37760b27a871ba422106eedbb8247da973a09e82558bf26d619c882b159" dependencies = [ "proc-macro2", "quote", @@ -744,33 +649,21 @@ dependencies = [ [[package]] name = "ff" -version = "0.12.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df689201f395c6b90dfe87127685f8dbfc083a5e779e613575d8bd7314300c3e" +checksum = "131655483be284720a17d74ff97592b8e76576dc25563148601df2d7c9080924" dependencies = [ "rand_core 0.6.3", "subtle 2.4.1", ] -[[package]] -name = "fixed" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0371cd413fb63f8ec1b9eb4dff47fa2c88b21abc681771234c84808b9920991" -dependencies = [ - "az", - "bytemuck", - "half", - "serde", - "typenum", -] - [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" dependencies = [ + "matches", "percent-encoding", ] @@ -791,9 +684,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" dependencies = [ "typenum", "version_check", @@ -814,13 +707,13 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.7" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.10.2+wasi-snapshot-preview1", ] [[package]] @@ -837,9 +730,9 @@ dependencies = [ [[package]] name = "git2" -version = "0.15.0" +version = "0.13.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2994bee4a3a6a51eb90c218523be382fd7ea09b16380b9312e9dbe955ff7c7d1" +checksum = "f29229cc1b24c0e6062f6e742aa3e256492a5323365e5ed3413599f8a5eff7d6" dependencies = [ "bitflags", "libc", @@ -850,50 +743,21 @@ dependencies = [ [[package]] name = "group" -version = "0.12.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7391856def869c1c81063a03457c676fbcd419709c3dfb33d8d319de484b154d" +checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89" dependencies = [ "ff", "rand_core 0.6.3", "subtle 2.4.1", ] -[[package]] -name = "half" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad6a9459c9c30b177b925162351f97e7d967c7ea8bab3b8352805327daf45554" -dependencies = [ - "crunchy", -] - -[[package]] -name = "handlebars" -version = "3.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4498fc115fa7d34de968184e473529abb40eeb6be8bc5f7faba3d08c316cb3e3" -dependencies = [ - "log", - "pest", - "pest_derive", - "quick-error", - "serde", - "serde_json", -] - [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hex-literal" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" - [[package]] name = "hkdf" version = "0.11.0" @@ -901,7 +765,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b" dependencies = [ "digest 0.9.0", - "hmac 0.11.0", + "hmac", ] [[package]] @@ -914,35 +778,13 @@ dependencies = [ "digest 0.9.0", ] -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.3", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c495f162af0bf17656d0014a0eded5f3cd2f365fdd204548c2869db89359dc7" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "js-sys", - "once_cell", - "wasm-bindgen", - "winapi", -] - [[package]] name = "idna" -version = "0.3.0" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" dependencies = [ + "matches", "unicode-bidi", "unicode-normalization", ] @@ -958,9 +800,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.3" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" [[package]] name = "jobserver" @@ -973,23 +815,24 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.59" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" dependencies = [ "wasm-bindgen", ] [[package]] name = "k256" -version = "0.11.4" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2573d3fd3e4cc741affc9b5ce1a8ce36cf29f09f80f36da4309d0ae6d7854" +checksum = "19c3a5e0a0b8450278feda242592512e09f61c72e018b8cd5c859482802daf2d" dependencies = [ "cfg-if", "ecdsa", "elliptic-curve", - "sha2 0.10.5", + "sec1", + "sha2", ] [[package]] @@ -999,16 +842,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28" [[package]] -name = "libc" -version = "0.2.132" +name = "lazy_static" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" [[package]] name = "libgit2-sys" -version = "0.14.0+1.5.0" +version = "0.12.26+1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47a00859c70c8a4f7218e6d1cc32875c4b55f6799445b842b0d8ed5e4c3d959b" +checksum = "19e1c899248e606fbfe68dcb31d8b0176ebab833b103824af31bddf4b7457494" dependencies = [ "cc", "libc", @@ -1018,15 +867,15 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.5" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "292a948cd991e376cf75541fe5b97a1081d713c618b4f1b9500f8844e49eb565" +checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" [[package]] name = "libz-sys" -version = "1.1.8" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" +checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66" dependencies = [ "cc", "libc", @@ -1048,34 +897,35 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" dependencies = [ "cfg-if", ] +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + [[package]] name = "mixnet-contract" version = "1.0.2" dependencies = [ - "az", "bs58", - "config", "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", "crypto", "cw-storage-plus", - "fixed", - "hex", "mixnet-contract-common", - "rand", "rand_chacha", "schemars", "serde", "thiserror", - "time 0.3.14", + "time 0.3.6", "vergen", "vesting-contract-common", ] @@ -1084,16 +934,16 @@ dependencies = [ name = "mixnet-contract-common" version = "0.1.0" dependencies = [ - "az", + "bs58", "contracts-common", "cosmwasm-std", - "fixed", "log", "schemars", "serde", + "serde_json", "serde_repr", "thiserror", - "time 0.3.14", + "time 0.3.6", ] [[package]] @@ -1108,24 +958,11 @@ dependencies = [ "serde", ] -[[package]] -name = "network-defaults" -version = "0.1.0" -dependencies = [ - "cfg-if", - "dotenv", - "hex-literal", - "once_cell", - "serde", - "thiserror", - "url", -] - [[package]] name = "num-integer" -version = "0.1.45" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" dependencies = [ "autocfg", "num-traits", @@ -1133,9 +970,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ "autocfg", "libm", @@ -1143,9 +980,9 @@ dependencies = [ [[package]] name = "num_threads" -version = "0.1.6" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +checksum = "71a1eb3a36534514077c1e079ada2fb170ef30c47d203aa6916138cf882ecd52" dependencies = [ "libc", ] @@ -1159,9 +996,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.14.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" +checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" [[package]] name = "opaque-debug" @@ -1195,69 +1032,26 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" - -[[package]] -name = "pest" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb779fcf4bb850fbbb0edc96ff6cf34fd90c4b1a112ce042653280d9a7364048" -dependencies = [ - "thiserror", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502b62a6d0245378b04ffe0a7fb4f4419a4815fce813bd8a0ec89a56e07d67b1" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "451e629bf49b750254da26132f1a5a9d11fd8a95a3df51d15c4abd1ba154cb6c" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pest_meta" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcec162c71c45e269dfc3fc2916eaeb97feab22993a21bcce4721d08cd7801a6" -dependencies = [ - "once_cell", - "pest", - "sha1", -] +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "pkcs8" -version = "0.9.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" dependencies = [ "der", "spki", + "zeroize", ] [[package]] name = "pkg-config" -version = "0.3.25" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" [[package]] name = "ppv-lite86" @@ -1291,11 +1085,11 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.43" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" dependencies = [ - "unicode-ident", + "unicode-xid", ] [[package]] @@ -1321,17 +1115,11 @@ dependencies = [ "syn", ] -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - [[package]] name = "quote" -version = "1.0.21" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" dependencies = [ "proc-macro2", ] @@ -1374,7 +1162,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.3", ] [[package]] @@ -1413,12 +1201,12 @@ checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" [[package]] name = "rfc6979" -version = "0.3.0" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88c86280f057430a52f4861551b092a01b419b8eacefc7c995eacb9dc132fe32" +checksum = "96ef608575f6392792f9ecf7890c00086591d29a83910939d430753f7c050525" dependencies = [ "crypto-bigint", - "hmac 0.12.1", + "hmac", "zeroize", ] @@ -1433,21 +1221,21 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.9" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" +checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" [[package]] name = "ryu" -version = "1.0.11" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" [[package]] name = "schemars" -version = "0.8.10" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1847b767a3d62d95cbf3d8a9f0e421cf57a0d8aa4f411d4b16525afb0284d4ed" +checksum = "c6b5a3c80cea1ab61f4260238409510e814e38b4b563c06044edf91e7dc070e3" dependencies = [ "dyn-clone", "schemars_derive", @@ -1457,9 +1245,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.10" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4d7e1b012cb3d9129567661a63755ea4b8a7386d339dc945ae187e403c6743" +checksum = "41ae4dce13e8614c46ac3c38ef1c0d668b101df6ac39817aebdaa26642ddae9b" dependencies = [ "proc-macro2", "quote", @@ -1469,13 +1257,12 @@ dependencies = [ [[package]] name = "sec1" -version = "0.3.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +checksum = "08da66b8b0965a5555b6bd6639e68ccba85e1e2506f5fbb089e93f8a04e1a2d1" dependencies = [ - "base16ct", "der", - "generic-array 0.14.6", + "generic-array 0.14.5", "pkcs8", "subtle 2.4.1", "zeroize", @@ -1483,15 +1270,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.13" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f6841e709003d68bb2deee8c343572bf446003ec20a583e76f7b15cebf3711" +checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" [[package]] name = "serde" -version = "1.0.144" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a" dependencies = [ "serde_derive", ] @@ -1507,9 +1294,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.144" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537" dependencies = [ "proc-macro2", "quote", @@ -1518,9 +1305,9 @@ dependencies = [ [[package]] name = "serde_derive_internals" -version = "0.26.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +checksum = "1dbab34ca63057a1f15280bdf3c39f2b1eb1b54c17e98360e511637aef7418c6" dependencies = [ "proc-macro2", "quote", @@ -1529,9 +1316,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.85" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" +checksum = "ee2bb9cd061c5865d345bb02ca49fcef1391741b672b54a0bf7b679badec3142" dependencies = [ "itoa", "ryu", @@ -1540,57 +1327,35 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.9" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" +checksum = "98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5" dependencies = [ "proc-macro2", "quote", "syn", ] -[[package]] -name = "sha1" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "006769ba83e921b3085caa8334186b00cf92b4cb1a6cf4632fbccc8eff5c7549" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.3", -] - [[package]] name = "sha2" version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ - "block-buffer 0.9.0", + "block-buffer", "cfg-if", "cpufeatures", "digest 0.9.0", "opaque-debug 0.3.0", ] -[[package]] -name = "sha2" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9db03534dff993187064c4e0c05a5708d2a9728ace9a8959b77bedf415dac5" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.3", -] - [[package]] name = "signature" -version = "1.6.0" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0ea32af43239f0d353a7dd75a22d94c329c8cdaafdcb4c1c1335aa10c298a4a" +checksum = "f2807892cfa58e081aa1f1111391c7a0649d4fa127a4ffbe34bcbfb35a1171a4" dependencies = [ - "digest 0.10.3", + "digest 0.9.0", "rand_core 0.6.3", ] @@ -1608,20 +1373,20 @@ dependencies = [ "curve25519-dalek", "digest 0.9.0", "hkdf", - "hmac 0.11.0", + "hmac", "lioness", "log", "rand", "rand_distr", - "sha2 0.9.9", + "sha2", "subtle 2.4.1", ] [[package]] name = "spki" -version = "0.6.0" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" dependencies = [ "base64ct", "der", @@ -1656,13 +1421,13 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.99" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7" dependencies = [ "proc-macro2", "quote", - "unicode-ident", + "unicode-xid", ] [[package]] @@ -1679,18 +1444,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.34" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c1b05ca9d106ba7d2e31a9dab4a64e7be2cce415321966ea3132c49a656e252" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.34" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8f2591983642de85c921015f3f070c665a197ed69e417af436115e3a1407487" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", @@ -1699,20 +1464,19 @@ dependencies = [ [[package]] name = "time" -version = "0.1.44" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" dependencies = [ "libc", - "wasi 0.10.0+wasi-snapshot-preview1", "winapi", ] [[package]] name = "time" -version = "0.3.14" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" +checksum = "c8d54b9298e05179c335de2b9645d061255bcd5155f843b3e328d2cfe0a5b413" dependencies = [ "itoa", "libc", @@ -1722,15 +1486,15 @@ dependencies = [ [[package]] name = "time-macros" -version = "0.2.4" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" +checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6" [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" dependencies = [ "tinyvec_macros", ] @@ -1741,27 +1505,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" -[[package]] -name = "toml" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" -dependencies = [ - "serde", -] - [[package]] name = "typenum" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" -[[package]] -name = "ucd-trie" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" - [[package]] name = "uint" version = "0.9.3" @@ -1776,39 +1525,34 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.8" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" - -[[package]] -name = "unicode-ident" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" [[package]] name = "unicode-normalization" -version = "0.1.21" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" dependencies = [ "tinyvec", ] [[package]] name = "unicode-xid" -version = "0.2.3" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] name = "url" -version = "2.3.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" dependencies = [ "form_urlencoded", "idna", + "matches", "percent-encoding", ] @@ -1843,7 +1587,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vesting-contract" -version = "1.0.2" +version = "1.1.0" dependencies = [ "cosmwasm-std", "cw-storage-plus", @@ -1872,21 +1616,15 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" +version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "wasm-bindgen" -version = "0.2.82" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1894,13 +1632,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.82" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" dependencies = [ "bumpalo", + "lazy_static", "log", - "once_cell", "proc-macro2", "quote", "syn", @@ -1909,9 +1647,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.82" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1919,9 +1657,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.82" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" dependencies = [ "proc-macro2", "quote", @@ -1932,9 +1670,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.82" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" [[package]] name = "winapi" @@ -1960,9 +1698,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "x25519-dalek" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" +checksum = "2392b6b94a576b4e2bf3c5b2757d63f10ada8020a2e4d08ac849ebcf6ea8e077" dependencies = [ "curve25519-dalek", "rand_core 0.5.1", @@ -1971,9 +1709,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.5.7" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" +checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" dependencies = [ "zeroize_derive", ] @@ -1988,4 +1726,4 @@ dependencies = [ "quote", "syn", "synstructure", -] \ No newline at end of file +] diff --git a/contracts/mixnet/Cargo.toml b/contracts/mixnet/Cargo.toml index 8a6f83b8c4..237e9c38ca 100644 --- a/contracts/mixnet/Cargo.toml +++ b/contracts/mixnet/Cargo.toml @@ -18,25 +18,22 @@ crate-type = ["cdylib", "rlib"] [dependencies] mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract" } -config = { path = "../../common/config"} +#config = { path = "../../common/config"} cosmwasm-std = "1.0.0" cosmwasm-storage = "1.0.0" cw-storage-plus = "0.13.4" -az = "1.2.0" bs58 = "0.4.0" schemars = "0.8" serde = { version = "1.0.103", default-features = false, features = ["derive"] } thiserror = { version = "1.0.23" } time = { version = "0.3", features = ["macros"] } -hex = "0.4.3" [dev-dependencies] cosmwasm-schema = "1.0.0" -fixed = "1.1" rand_chacha = "0.2" -rand = "0.7" +#rand = "0.7" crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } [build-dependencies] diff --git a/contracts/mixnet/src/constants.rs b/contracts/mixnet/src/constants.rs index a19aa4fef3..3d3ed82caf 100644 --- a/contracts/mixnet/src/constants.rs +++ b/contracts/mixnet/src/constants.rs @@ -1,17 +1,71 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -// approximately 1 epoch (assuming 5s per block) -pub const MINIMUM_BLOCK_AGE_FOR_REWARDING: u64 = 720; +use cosmwasm_std::Uint128; -pub const INTERVAL_REWARD_PERCENT: u8 = 2; // Used to calculate interval reward pool -pub const SYBIL_RESISTANCE_PERCENT: u8 = 30; -pub const ACTIVE_SET_WORK_FACTOR: u8 = 10; +/// Constant specifying minimum of coin amount required to bond a gateway +pub const INITIAL_GATEWAY_PLEDGE_AMOUNT: Uint128 = Uint128::new(100_000_000); -// TODO: this, in theory, represents "epoch" length. -// However, since the blocktime is not EXACTLY 5s, we can't really guarantee 720 epochs in interval -// and we can't change this easily to `Duration`, because then the entire rewarded set storage -// would be messed up... (as we look up stuff "by blocks") -pub const REWARDED_SET_REFRESH_BLOCKS: u64 = 720; // with blocktime being approximately 5s, it should be roughly 1h -pub const INTERVAL_SECONDS: u64 = 86400 * 30; // 30 days -pub const DEFAULT_OPERATOR_INTERVAL_COST: u64 = 40_000_000; +/// Constant specifying minimum of coin amount required to bond a mixnode +pub const INITIAL_MIXNODE_PLEDGE_AMOUNT: Uint128 = Uint128::new(100_000_000); + +// retrieval limits +// TODO: those would need to be empirically verified whether they're not way too small or way too high +pub const GATEWAY_BOND_DEFAULT_RETRIEVAL_LIMIT: u32 = 100; +pub const GATEWAY_BOND_MAX_RETRIEVAL_LIMIT: u32 = 150; + +pub const MIXNODE_BOND_DEFAULT_RETRIEVAL_LIMIT: u32 = 100; +pub const MIXNODE_BOND_MAX_RETRIEVAL_LIMIT: u32 = 150; + +pub const MIXNODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT: u32 = 75; +pub const MIXNODE_DETAILS_MAX_RETRIEVAL_LIMIT: u32 = 100; + +pub const UNBONDED_MIXNODES_DEFAULT_RETRIEVAL_LIMIT: u32 = 250; +pub const UNBONDED_MIXNODES_MAX_RETRIEVAL_LIMIT: u32 = 300; + +pub const DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT: u32 = 250; +pub const DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT: u32 = 300; + +pub const EPOCH_EVENTS_DEFAULT_RETRIEVAL_LIMIT: u32 = 200; +pub const EPOCH_EVENTS_MAX_RETRIEVAL_LIMIT: u32 = 250; + +pub const INTERVAL_EVENTS_DEFAULT_RETRIEVAL_LIMIT: u32 = 200; +pub const INTERVAL_EVENTS_MAX_RETRIEVAL_LIMIT: u32 = 250; + +pub const REWARDED_SET_DEFAULT_RETRIEVAL_LIMIT: u32 = 500; +pub const REWARDED_SET_MAX_RETRIEVAL_LIMIT: u32 = 1000; + +// storage keys +pub(crate) const DELEGATION_PK_NAMESPACE: &str = "dl"; +pub(crate) const DELEGATION_OWNER_IDX_NAMESPACE: &str = "dlo"; +pub(crate) const DELEGATION_MIXNODE_IDX_NAMESPACE: &str = "dlm"; + +pub(crate) const GATEWAYS_PK_NAMESPACE: &str = "gt"; +pub(crate) const GATEWAYS_OWNER_IDX_NAMESPACE: &str = "gto"; + +pub(crate) const REWARDED_SET_KEY: &str = "rs"; +pub(crate) const CURRENT_INTERVAL_KEY: &str = "ci"; +pub(crate) const EPOCH_EVENT_ID_COUNTER_KEY: &str = "eic"; +pub(crate) const INTERVAL_EVENT_ID_COUNTER_KEY: &str = "iic"; +pub(crate) const PENDING_EPOCH_EVENTS_NAMESPACE: &str = "pee"; +pub(crate) const PENDING_INTERVAL_EVENTS_NAMESPACE: &str = "pie"; + +pub(crate) const LAST_EPOCH_EVENT_ID_KEY: &str = "lee"; +pub(crate) const LAST_INTERVAL_EVENT_ID_KEY: &str = "lie"; + +pub(crate) const CONTRACT_STATE_KEY: &str = "state"; + +pub(crate) const LAYER_DISTRIBUTION_KEY: &str = "layers"; +pub(crate) const NODE_ID_COUNTER_KEY: &str = "nic"; +pub(crate) const MIXNODES_PK_NAMESPACE: &str = "mnn"; +pub(crate) const MIXNODES_OWNER_IDX_NAMESPACE: &str = "mno"; +pub(crate) const MIXNODES_IDENTITY_IDX_NAMESPACE: &str = "mni"; +pub(crate) const MIXNODES_SPHINX_IDX_NAMESPACE: &str = "mns"; + +pub(crate) const UNBONDED_MIXNODES_PK_NAMESPACE: &str = "ubm"; +pub(crate) const UNBONDED_MIXNODES_OWNER_IDX_NAMESPACE: &str = "umo"; +pub(crate) const UNBONDED_MIXNODES_IDENTITY_IDX_NAMESPACE: &str = "umi"; + +pub(crate) const REWARDING_PARAMS_KEY: &str = "rparams"; +pub(crate) const PENDING_REWARD_POOL_KEY: &str = "prp"; +pub(crate) const MIXNODES_REWARDING_PK_NAMESPACE: &str = "mnr"; diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index bf99614939..60c51aa603 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -1,82 +1,40 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::constants::{ACTIVE_SET_WORK_FACTOR, INTERVAL_REWARD_PERCENT, SYBIL_RESISTANCE_PERCENT}; -use crate::delegations::queries::query_delegator_delegations_paged; -use crate::delegations::queries::query_mixnode_delegation; -use crate::delegations::queries::{ - query_mixnode_delegations_paged, query_pending_delegation_events, -}; -use crate::error::ContractError; -use crate::gateways::queries::query_owns_gateway; -use crate::gateways::queries::{query_gateway_bond, query_gateways_paged}; -use crate::interval::queries::query_current_epoch; -use crate::interval::queries::{ - query_current_rewarded_set_height, query_rewarded_set, - query_rewarded_set_refresh_minimum_blocks, query_rewarded_set_update_details, -}; -use crate::interval::transactions::{init_epoch, try_init_epoch}; -use crate::mixnet_contract_settings::models::ContractState; -use crate::mixnet_contract_settings::queries::{ - query_contract_settings_params, query_contract_version, query_rewarding_validator_address, -}; +use crate::constants::{INITIAL_GATEWAY_PLEDGE_AMOUNT, INITIAL_MIXNODE_PLEDGE_AMOUNT}; +use crate::interval::storage as interval_storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage; -use crate::mixnet_contract_settings::transactions::try_update_rewarding_validator_address; -use crate::mixnodes::bonding_queries as mixnode_queries; -use crate::mixnodes::bonding_queries::{ - query_checkpoints_for_mixnode, query_mixnode_at_height, query_mixnodes_paged, -}; -use crate::mixnodes::layer_queries::query_layer_distribution; -use crate::mixnodes::transactions::_try_remove_mixnode; -use crate::rewards::queries::{ - query_circulating_supply, query_reward_pool, query_rewarding_status, query_staking_supply, -}; +use crate::mixnodes::storage as mixnode_storage; use crate::rewards::storage as rewards_storage; use cosmwasm_std::{ - entry_point, to_binary, Addr, Api, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, - Storage, Uint128, + entry_point, to_binary, Addr, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, }; +use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::{ - ContractStateParams, ExecuteMsg, InstantiateMsg, MigrateMsg, NodeToRemove, QueryMsg, + ContractState, ContractStateParams, ExecuteMsg, InstantiateMsg, Interval, MigrateMsg, QueryMsg, }; -use time::OffsetDateTime; - -/// Constant specifying minimum of coin required to bond a gateway -pub const INITIAL_GATEWAY_PLEDGE: Uint128 = Uint128::new(100_000_000); - -/// Constant specifying minimum of coin required to bond a mixnode -pub const INITIAL_MIXNODE_PLEDGE: Uint128 = Uint128::new(100_000_000); - -pub const INITIAL_MIXNODE_REWARDED_SET_SIZE: u32 = 200; -pub const INITIAL_MIXNODE_ACTIVE_SET_SIZE: u32 = 100; - -pub const INITIAL_REWARD_POOL: u128 = 250_000_000_000_000; -pub const INITIAL_ACTIVE_SET_WORK_FACTOR: u8 = 10; - -pub const DEFAULT_FIRST_INTERVAL_START: OffsetDateTime = - time::macros::datetime!(2022-01-01 12:00 UTC); - -pub const INITIAL_STAKING_SUPPLY: Uint128 = Uint128::new(100_000_000_000_000); - -pub fn debug_with_visibility>(api: &dyn Api, msg: S) { - api.debug(&*format!("\n\n\n=========================================\n{}\n=========================================\n\n\n", msg.into())); -} fn default_initial_state( owner: Addr, - mix_denom: String, rewarding_validator_address: Addr, + rewarding_denom: String, + vesting_contract_address: Addr, ) -> ContractState { ContractState { owner, - mix_denom, rewarding_validator_address, + vesting_contract_address, + rewarding_denom: rewarding_denom.clone(), params: ContractStateParams { - minimum_mixnode_pledge: INITIAL_MIXNODE_PLEDGE, - minimum_gateway_pledge: INITIAL_GATEWAY_PLEDGE, - mixnode_rewarded_set_size: INITIAL_MIXNODE_REWARDED_SET_SIZE, - mixnode_active_set_size: INITIAL_MIXNODE_ACTIVE_SET_SIZE, - staking_supply: INITIAL_STAKING_SUPPLY, + minimum_mixnode_delegation: None, + minimum_mixnode_pledge: Coin { + denom: rewarding_denom.clone(), + amount: INITIAL_MIXNODE_PLEDGE_AMOUNT, + }, + minimum_gateway_pledge: Coin { + denom: rewarding_denom, + amount: INITIAL_GATEWAY_PLEDGE_AMOUNT, + }, }, } } @@ -92,14 +50,25 @@ pub fn instantiate( env: Env, info: MessageInfo, msg: InstantiateMsg, -) -> Result { +) -> Result { let rewarding_validator_address = deps.api.addr_validate(&msg.rewarding_validator_address)?; - let state = default_initial_state(info.sender, msg.mixnet_denom, rewarding_validator_address); - init_epoch(deps.storage, env)?; + let vesting_contract_address = deps.api.addr_validate(&msg.vesting_contract_address)?; + let state = default_initial_state( + info.sender, + rewarding_validator_address, + msg.rewarding_denom, + vesting_contract_address, + ); + let starting_interval = + Interval::init_interval(msg.epochs_in_interval, msg.epoch_duration, &env); + let reward_params = msg + .initial_rewarding_params + .into_rewarding_params(msg.epochs_in_interval); - mixnet_params_storage::CONTRACT_STATE.save(deps.storage, &state)?; - mixnet_params_storage::LAYERS.save(deps.storage, &Default::default())?; - rewards_storage::REWARD_POOL.save(deps.storage, &Uint128::new(INITIAL_REWARD_POOL))?; + interval_storage::initialise_storage(deps.storage, starting_interval)?; + mixnet_params_storage::initialise_storage(deps.storage, state)?; + mixnode_storage::initialise_storage(deps.storage)?; + rewards_storage::initialise_storage(deps.storage, reward_params)?; Ok(Response::default()) } @@ -111,56 +80,118 @@ pub fn execute( env: Env, info: MessageInfo, msg: ExecuteMsg, -) -> Result { +) -> Result { match msg { - ExecuteMsg::CompoundReward { - operator, - delegator, - mix_identity, - proxy, - } => crate::rewards::transactions::try_compound_reward( + // state/sys-params-related + ExecuteMsg::UpdateRewardingValidatorAddress { address } => { + crate::mixnet_contract_settings::transactions::try_update_rewarding_validator_address( + deps, info, address, + ) + } + ExecuteMsg::UpdateContractStateParams { updated_parameters } => { + crate::mixnet_contract_settings::transactions::try_update_contract_settings( + deps, + info, + updated_parameters, + ) + } + ExecuteMsg::UpdateActiveSetSize { + active_set_size, + force_immediately, + } => crate::rewards::transactions::try_update_active_set_size( deps, env, - operator, - delegator, - mix_identity, - proxy, + info, + active_set_size, + force_immediately, ), - ExecuteMsg::UpdateRewardingValidatorAddress { address } => { - try_update_rewarding_validator_address(deps, info, address) + ExecuteMsg::UpdateRewardingParams { + updated_params, + force_immediately, + } => crate::rewards::transactions::try_update_rewarding_params( + deps, + env, + info, + updated_params, + force_immediately, + ), + ExecuteMsg::UpdateIntervalConfig { + epochs_in_interval, + epoch_duration_secs, + force_immediately, + } => crate::interval::transactions::try_update_interval_config( + deps, + env, + info, + epochs_in_interval, + epoch_duration_secs, + force_immediately, + ), + ExecuteMsg::AdvanceCurrentEpoch { + new_rewarded_set, + expected_active_set_size, + } => crate::interval::transactions::try_advance_epoch( + deps, + env, + info, + new_rewarded_set, + expected_active_set_size, + ), + ExecuteMsg::ReconcileEpochEvents { limit } => { + crate::interval::transactions::try_reconcile_epoch_events(deps, env, limit) } - ExecuteMsg::InitEpoch {} => try_init_epoch(info, deps.storage, env), + + // mixnode-related: ExecuteMsg::BondMixnode { mix_node, + cost_params, owner_signature, } => crate::mixnodes::transactions::try_add_mixnode( deps, env, info, mix_node, + 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::UnbondMixnode {} => { - crate::mixnodes::transactions::try_remove_mixnode(&env, deps.storage, deps.api, info) + crate::mixnodes::transactions::try_remove_mixnode(deps, info) } - ExecuteMsg::UpdateMixnodeConfig { - profit_margin_percent, - } => crate::mixnodes::transactions::try_update_mixnode_config( - deps, - env, - info, - profit_margin_percent, - ), - ExecuteMsg::UpdateMixnodeConfigOnBehalf { - profit_margin_percent, - owner, - } => crate::mixnodes::transactions::try_update_mixnode_config_on_behalf( - deps, - env, - info, - profit_margin_percent, - owner, - ), + ExecuteMsg::UnbondMixnodeOnBehalf { owner } => { + crate::mixnodes::transactions::try_remove_mixnode_on_behalf(deps, info, owner) + } + ExecuteMsg::UpdateMixnodeCostParams { new_costs } => { + crate::mixnodes::transactions::try_update_mixnode_cost_params(deps, info, new_costs) + } + ExecuteMsg::UpdateMixnodeCostParamsOnBehalf { new_costs, owner } => { + crate::mixnodes::transactions::try_update_mixnode_cost_params_on_behalf( + deps, 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 { gateway, owner_signature, @@ -171,78 +202,6 @@ pub fn execute( gateway, owner_signature, ), - ExecuteMsg::UnbondGateway {} => { - crate::gateways::transactions::try_remove_gateway(deps, info) - } - ExecuteMsg::UpdateContractStateParams(params) => { - crate::mixnet_contract_settings::transactions::try_update_contract_settings( - deps, info, params, - ) - } - ExecuteMsg::RewardMixnode { identity, params } => { - crate::rewards::transactions::try_reward_mixnode(deps, env, info, identity, params) - } - ExecuteMsg::DelegateToMixnode { mix_identity } => { - crate::delegations::transactions::try_delegate_to_mixnode(deps, env, info, mix_identity) - } - ExecuteMsg::UndelegateFromMixnode { mix_identity } => { - crate::delegations::transactions::try_remove_delegation_from_mixnode( - deps, - env, - info, - mix_identity, - ) - } - // ExecuteMsg::RewardNextMixDelegators { - // mix_identity, - // interval_id, - // } => crate::rewards::transactions::try_reward_next_mixnode_delegators( - // deps, - // info, - // mix_identity, - // interval_id, - // ), - ExecuteMsg::DelegateToMixnodeOnBehalf { - mix_identity, - delegate, - } => crate::delegations::transactions::try_delegate_to_mixnode_on_behalf( - deps, - env, - info, - mix_identity, - delegate, - ), - ExecuteMsg::UndelegateFromMixnodeOnBehalf { - mix_identity, - delegate, - } => crate::delegations::transactions::try_remove_delegation_from_mixnode_on_behalf( - deps, - env, - info, - mix_identity, - delegate, - ), - ExecuteMsg::BondMixnodeOnBehalf { - mix_node, - owner, - owner_signature, - } => crate::mixnodes::transactions::try_add_mixnode_on_behalf( - deps, - env, - info, - mix_node, - owner, - owner_signature, - ), - ExecuteMsg::UnbondMixnodeOnBehalf { owner } => { - crate::mixnodes::transactions::try_remove_mixnode_on_behalf( - &env, - deps.storage, - deps.api, - info, - owner, - ) - } ExecuteMsg::BondGatewayOnBehalf { gateway, owner, @@ -255,303 +214,341 @@ pub fn execute( 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::WriteRewardedSet { - rewarded_set, - expected_active_set_size, - } => crate::interval::transactions::try_write_rewarded_set( - deps, - env, - info, - rewarded_set, - expected_active_set_size, - ), - ExecuteMsg::AdvanceCurrentEpoch {} => crate::interval::transactions::try_advance_epoch( - env, - deps.storage, - info.sender.to_string(), - ), - ExecuteMsg::CompoundDelegatorReward { mix_identity } => { - crate::rewards::transactions::try_compound_delegator_reward( - deps, - env, - info, - mix_identity, + + // delegation-related: + ExecuteMsg::DelegateToMixnode { mix_id } => { + crate::delegations::transactions::try_delegate_to_mixnode(deps, info, mix_id) + } + ExecuteMsg::DelegateToMixnodeOnBehalf { mix_id, delegate } => { + crate::delegations::transactions::try_delegate_to_mixnode_on_behalf( + deps, info, mix_id, delegate, ) } - ExecuteMsg::CompoundOperatorReward {} => { - crate::rewards::transactions::try_compound_operator_reward(deps, env, info) + ExecuteMsg::UndelegateFromMixnode { mix_id } => { + crate::delegations::transactions::try_remove_delegation_from_mixnode(deps, info, mix_id) } - ExecuteMsg::CompoundDelegatorRewardOnBehalf { - owner, - mix_identity, - } => crate::rewards::transactions::try_compound_delegator_reward_on_behalf( - deps, - env, - info, - owner, - mix_identity, - ), - ExecuteMsg::CompoundOperatorRewardOnBehalf { owner } => { - crate::rewards::transactions::try_compound_operator_reward_on_behalf( - deps, env, info, owner, + ExecuteMsg::UndelegateFromMixnodeOnBehalf { mix_id, delegate } => { + crate::delegations::transactions::try_remove_delegation_from_mixnode_on_behalf( + deps, info, mix_id, delegate, ) } - ExecuteMsg::ReconcileDelegations {} => { - crate::delegations::transactions::try_reconcile_all_delegation_events(deps) + + // reward-related + ExecuteMsg::RewardMixnode { + mix_id, + performance, + } => crate::rewards::transactions::try_reward_mixnode(deps, env, info, mix_id, performance), + + ExecuteMsg::WithdrawOperatorReward {} => { + crate::rewards::transactions::try_withdraw_operator_reward(deps, info) } - ExecuteMsg::CheckpointMixnodes {} => { - crate::mixnodes::transactions::try_checkpoint_mixnodes( - deps.storage, - env.block.height, - 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, ) } - ExecuteMsg::ClaimOperatorReward {} => { - crate::rewards::transactions::try_claim_operator_reward(deps, &env, &info) - } - ExecuteMsg::ClaimOperatorRewardOnBehalf { owner } => { - crate::rewards::transactions::try_claim_operator_reward_on_behalf( - deps, &env, &info, owner, - ) - } - ExecuteMsg::ClaimDelegatorReward { mix_identity } => { - crate::rewards::transactions::try_claim_delegator_reward( - deps, - &env, - &info, - &mix_identity, - ) - } - ExecuteMsg::ClaimDelegatorRewardOnBehalf { - mix_identity, - owner, - } => crate::rewards::transactions::try_claim_delegator_reward_on_behalf( - deps, - &env, - &info, - owner, - &mix_identity, - ), } } #[entry_point] -pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result { +pub fn query( + deps: Deps<'_>, + env: Env, + msg: QueryMsg, +) -> Result { let query_res = match msg { - QueryMsg::GetBlacklistedNodes {} => to_binary( - &crate::mixnodes::bonding_queries::get_blacklisted_nodes(deps), + QueryMsg::GetContractVersion {} => { + to_binary(&crate::mixnet_contract_settings::queries::query_contract_version()) + } + QueryMsg::GetStateParams {} => to_binary( + &crate::mixnet_contract_settings::queries::query_contract_settings_params(deps)?, ), - QueryMsg::GetRewardingValidatorAddress {} => { - to_binary(&query_rewarding_validator_address(deps)?) + QueryMsg::GetRewardingValidatorAddress {} => to_binary( + &crate::mixnet_contract_settings::queries::query_rewarding_validator_address(deps)?, + ), + QueryMsg::GetState {} => { + to_binary(&crate::mixnet_contract_settings::queries::query_contract_state(deps)?) } - QueryMsg::GetContractVersion {} => to_binary(&query_contract_version()), - QueryMsg::GetMixNodes { start_after, limit } => { - to_binary(&query_mixnodes_paged(deps, start_after, limit)?) + QueryMsg::GetRewardingParams {} => { + to_binary(&crate::rewards::queries::query_rewarding_params(deps)?) } - QueryMsg::GetGateways { limit, start_after } => { - to_binary(&query_gateways_paged(deps, start_after, limit)?) + QueryMsg::GetCurrentIntervalDetails {} => to_binary( + &crate::interval::queries::query_current_interval_details(deps, env)?, + ), + QueryMsg::GetRewardedSet { limit, start_after } => to_binary( + &crate::interval::queries::query_rewarded_set_paged(deps, start_after, limit)?, + ), + + // mixnode-related: + QueryMsg::GetMixNodeBonds { start_after, limit } => to_binary( + &crate::mixnodes::queries::query_mixnode_bonds_paged(deps, start_after, limit)?, + ), + QueryMsg::GetMixNodesDetailed { start_after, limit } => to_binary( + &crate::mixnodes::queries::query_mixnodes_details_paged(deps, start_after, limit)?, + ), + QueryMsg::GetUnbondedMixNodes { limit, start_after } => to_binary( + &crate::mixnodes::queries::query_unbonded_mixnodes_paged(deps, start_after, limit)?, + ), + QueryMsg::GetUnbondedMixNodesByOwner { + owner, + limit, + start_after, + } => to_binary( + &crate::mixnodes::queries::query_unbonded_mixnodes_by_owner_paged( + deps, + owner, + start_after, + limit, + )?, + ), + QueryMsg::GetUnbondedMixNodesByIdentityKey { + identity_key, + limit, + start_after, + } => to_binary( + &crate::mixnodes::queries::query_unbonded_mixnodes_by_identity_paged( + deps, + identity_key, + start_after, + limit, + )?, + ), + QueryMsg::GetOwnedMixnode { address } => to_binary( + &crate::mixnodes::queries::query_owned_mixnode(deps, address)?, + ), + QueryMsg::GetMixnodeDetails { mix_id } => to_binary( + &crate::mixnodes::queries::query_mixnode_details(deps, mix_id)?, + ), + QueryMsg::GetMixnodeRewardingDetails { mix_id } => to_binary( + &crate::mixnodes::queries::query_mixnode_rewarding_details(deps, mix_id)?, + ), + QueryMsg::GetStakeSaturation { mix_id } => to_binary( + &crate::mixnodes::queries::query_stake_saturation(deps, mix_id)?, + ), + QueryMsg::GetUnbondedMixNodeInformation { mix_id } => to_binary( + &crate::mixnodes::queries::query_unbonded_mixnode(deps, mix_id)?, + ), + QueryMsg::GetBondedMixnodeDetailsByIdentity { mix_identity } => to_binary( + &crate::mixnodes::queries::query_mixnode_details_by_identity(deps, mix_identity)?, + ), + QueryMsg::GetLayerDistribution {} => { + to_binary(&crate::mixnodes::queries::query_layer_distribution(deps)?) } - QueryMsg::OwnsMixnode { address } => { - to_binary(&mixnode_queries::query_owns_mixnode(deps, address)?) - } - QueryMsg::GetMixnodeBond { identity } => { - to_binary(&mixnode_queries::query_mixnode_bond(deps, identity)?) - } - QueryMsg::GetGatewayBond { identity } => to_binary(&query_gateway_bond(deps, identity)?), - QueryMsg::OwnsGateway { address } => to_binary(&query_owns_gateway(deps, address)?), - QueryMsg::StateParams {} => to_binary(&query_contract_settings_params(deps)?), - QueryMsg::LayerDistribution {} => to_binary(&query_layer_distribution(deps)?), + + // gateway-related: + QueryMsg::GetGateways { limit, start_after } => to_binary( + &crate::gateways::queries::query_gateways_paged(deps, start_after, limit)?, + ), + QueryMsg::GetGatewayBond { identity } => to_binary( + &crate::gateways::queries::query_gateway_bond(deps, identity)?, + ), + QueryMsg::GetOwnedGateway { address } => to_binary( + &crate::gateways::queries::query_owned_gateway(deps, address)?, + ), + + // delegation-related: QueryMsg::GetMixnodeDelegations { - mix_identity, + mix_id, start_after, limit, - } => to_binary(&query_mixnode_delegations_paged( - deps, - mix_identity, - start_after, - limit, - )?), + } => to_binary( + &crate::delegations::queries::query_mixnode_delegations_paged( + deps, + mix_id, + start_after, + limit, + )?, + ), QueryMsg::GetDelegatorDelegations { - delegator: delegation_owner, + delegator, start_after, limit, - } => to_binary(&query_delegator_delegations_paged( - deps, - delegation_owner, - start_after, - limit, - )?), + } => to_binary( + &crate::delegations::queries::query_delegator_delegations_paged( + deps, + delegator, + start_after, + limit, + )?, + ), QueryMsg::GetDelegationDetails { - mix_identity, - delegator, - proxy, - } => to_binary(&query_mixnode_delegation( - deps.storage, - deps.api, - mix_identity, + mix_id, delegator, proxy, + } => to_binary(&crate::delegations::queries::query_mixnode_delegation( + deps, mix_id, delegator, proxy, )?), - QueryMsg::GetRewardPool {} => to_binary(&query_reward_pool(deps)?), - QueryMsg::GetCirculatingSupply {} => to_binary(&query_circulating_supply(deps)?), - QueryMsg::GetStakingSupply {} => to_binary(&query_staking_supply(deps)?), - QueryMsg::GetIntervalRewardPercent {} => to_binary(&INTERVAL_REWARD_PERCENT), - QueryMsg::GetSybilResistancePercent {} => to_binary(&SYBIL_RESISTANCE_PERCENT), - QueryMsg::GetActiveSetWorkFactor {} => to_binary(&ACTIVE_SET_WORK_FACTOR), - QueryMsg::GetRewardingStatus { - mix_identity, - interval_id, - } => to_binary(&query_rewarding_status(deps, mix_identity, interval_id)?), - QueryMsg::GetRewardedSet { - height, - start_after, - limit, - } => to_binary(&query_rewarded_set( - deps.storage, - height, - start_after, - limit, - )?), - QueryMsg::GetRewardedSetUpdateDetails {} => { - to_binary(&query_rewarded_set_update_details(env, deps.storage)?) - } - QueryMsg::GetCurrentRewardedSetHeight {} => { - to_binary(&query_current_rewarded_set_height(deps.storage)?) - } - // QueryMsg::GetCurrentInterval {} => to_binary(&query_current_interval(deps.storage)?), - QueryMsg::GetRewardedSetRefreshBlocks {} => { - to_binary(&query_rewarded_set_refresh_minimum_blocks()) - } - QueryMsg::GetEpochsInInterval {} => { - to_binary(&crate::support::helpers::epochs_in_interval(deps.storage)?) - } - QueryMsg::GetCurrentOperatorCost {} => to_binary( - &crate::support::helpers::current_operator_epoch_cost(deps.storage)?, + QueryMsg::GetAllDelegations { start_after, limit } => to_binary( + &crate::delegations::queries::query_all_delegations_paged(deps, start_after, limit)?, ), - QueryMsg::GetCurrentEpoch {} => to_binary(&query_current_epoch(deps.storage)?), - QueryMsg::QueryOperatorReward { address } => to_binary( - &crate::rewards::queries::query_operator_reward(deps, address)?, + + // rewards related + QueryMsg::GetPendingOperatorReward { address } => to_binary( + &crate::rewards::queries::query_pending_operator_reward(deps, address)?, ), - QueryMsg::QueryDelegatorReward { + QueryMsg::GetPendingMixNodeOperatorReward { mix_id } => to_binary( + &crate::rewards::queries::query_pending_mixnode_operator_reward(deps, mix_id)?, + ), + QueryMsg::GetPendingDelegatorReward { address, - mix_identity, + mix_id, proxy, - } => to_binary(&crate::rewards::queries::query_delegator_reward( - deps, + } => to_binary(&crate::rewards::queries::query_pending_delegator_reward( + deps, address, mix_id, proxy, + )?), + QueryMsg::GetEstimatedCurrentEpochOperatorReward { + mix_id, + estimated_performance, + } => to_binary( + &crate::rewards::queries::query_estimated_current_epoch_operator_reward( + deps, + mix_id, + estimated_performance, + )?, + ), + QueryMsg::GetEstimatedCurrentEpochDelegatorReward { address, - mix_identity, + mix_id, proxy, - )?), - QueryMsg::GetPendingDelegationEvents { - owner_address, - proxy_address, - } => to_binary(&query_pending_delegation_events( - deps, - owner_address, - proxy_address, - )?), - QueryMsg::GetAllDelegationKeys {} => to_binary( - &crate::delegations::queries::query_all_delegation_keys(deps.storage)?, + estimated_performance, + } => to_binary( + &crate::rewards::queries::query_estimated_current_epoch_delegator_reward( + deps, + address, + mix_id, + proxy, + estimated_performance, + )?, ), - QueryMsg::DebugGetAllDelegationValues {} => to_binary( - &crate::delegations::queries::debug_query_all_delegation_values(deps.storage)?, - ), - QueryMsg::GetCheckpointsForMixnode { mix_identity } => { - to_binary(&query_checkpoints_for_mixnode(deps, mix_identity)?) + + // interval-related + QueryMsg::GetPendingEpochEvents { limit, start_after } => { + to_binary(&crate::interval::queries::query_pending_epoch_events_paged( + deps, + env, + start_after, + limit, + )?) } - QueryMsg::GetMixnodeAtHeight { - mix_identity, - height, - } => to_binary(&query_mixnode_at_height(deps, mix_identity, height)?), + QueryMsg::GetPendingIntervalEvents { limit, start_after } => to_binary( + &crate::interval::queries::query_pending_interval_events_paged( + deps, + env, + start_after, + limit, + )?, + ), }; Ok(query_res?) } -#[allow(unused)] -fn blacklist_malicious_node(storage: &mut dyn Storage, owner: &Addr) -> Result<(), ContractError> { - let mixnode_bond = match crate::mixnodes::storage::mixnodes() - .idx - .owner - .item(storage, owner.clone())? - { - Some(record) => record.1, - None => { - return Err(ContractError::NoAssociatedMixNodeBond { - owner: owner.to_owned(), - }) - } - }; - - crate::mixnodes::storage::MIXNODES_BOND_BLACKLIST.save(storage, mixnode_bond.identity(), &0)?; - - Ok(()) -} - -// Removes nodes we've deemed malicious, returns the pledge to the owners, but does not send any rewards -#[allow(unused)] -fn remove_malicious_node( - storage: &mut dyn Storage, - api: &dyn Api, - env: &Env, - node: &NodeToRemove, -) -> Result { - let proxy = node.proxy().map(|p| { - api.addr_validate(p) - .unwrap_or_else(|_| panic!("Invalid address: {}", p)) - }); - let owner_addr = api.addr_validate(node.owner())?; - blacklist_malicious_node(storage, &owner_addr)?; - _try_remove_mixnode(env, storage, api, node.owner(), proxy, false) -} - #[entry_point] -pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result { - Ok(Response::default()) +pub fn migrate( + _deps: DepsMut<'_>, + _env: Env, + _msg: MigrateMsg, +) -> Result { + Ok(Default::default()) } #[cfg(test)] -pub mod tests { +mod tests { use super::*; - use crate::support::tests; - use crate::support::tests::fixtures::{TEST_COIN_DENOM, TEST_REWARDING_VALIDATOR_ADDRESS}; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - use cosmwasm_std::{coins, from_binary}; - use mixnet_contract_common::PagedMixnodeResponse; + use cosmwasm_std::Decimal; + use mixnet_contract_common::reward_params::{IntervalRewardParams, RewardingParams}; + use mixnet_contract_common::{InitialRewardingParams, Percent}; + use std::time::Duration; #[test] fn initialize_contract() { let mut deps = mock_dependencies(); let env = mock_env(); - let msg = InstantiateMsg { - rewarding_validator_address: TEST_REWARDING_VALIDATOR_ADDRESS.to_string(), - mixnet_denom: TEST_COIN_DENOM.to_string(), - }; - let info = mock_info("creator", &[]); - let res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); - assert_eq!(0, res.messages.len()); - - // mix_node_bonds should be empty after initialization - let res = query( - deps.as_ref(), - env.clone(), - QueryMsg::GetMixNodes { - start_after: None, - limit: Option::from(2), + let init_msg = InstantiateMsg { + rewarding_validator_address: "foomp123".to_string(), + vesting_contract_address: "bar456".to_string(), + rewarding_denom: "uatom".to_string(), + epochs_in_interval: 1234, + epoch_duration: Duration::from_secs(4321), + initial_rewarding_params: InitialRewardingParams { + initial_reward_pool: Decimal::from_atomics(100_000_000_000_000u128, 0).unwrap(), + initial_staking_supply: Decimal::from_atomics(123_456_000_000_000u128, 0).unwrap(), + sybil_resistance: Percent::from_percentage_value(23).unwrap(), + active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), + interval_pool_emission: Percent::from_percentage_value(1).unwrap(), + rewarded_set_size: 543, + active_set_size: 123, }, - ) - .unwrap(); - let page: PagedMixnodeResponse = from_binary(&res).unwrap(); - assert_eq!(0, page.nodes.len()); // there are no mixnodes in the list when it's just been initialized + }; - // Contract balance should match what we initialized it as - assert_eq!( - coins(0, TEST_COIN_DENOM), - tests::queries::query_contract_balance(env.contract.address, deps) - ); + let sender = mock_info("sender", &[]); + let res = instantiate(deps.as_mut(), env, sender, init_msg); + assert!(res.is_ok()); + + let expected_state = ContractState { + owner: Addr::unchecked("sender"), + rewarding_validator_address: Addr::unchecked("foomp123"), + vesting_contract_address: Addr::unchecked("bar456"), + rewarding_denom: "uatom".into(), + params: ContractStateParams { + minimum_mixnode_delegation: None, + minimum_mixnode_pledge: Coin { + denom: "uatom".into(), + amount: INITIAL_MIXNODE_PLEDGE_AMOUNT, + }, + minimum_gateway_pledge: Coin { + denom: "uatom".into(), + amount: INITIAL_GATEWAY_PLEDGE_AMOUNT, + }, + }, + }; + + let expected_epoch_reward_budget = + Decimal::from_ratio(100_000_000_000_000u128, 1234u32) * Decimal::percent(1); + let expected_stake_saturation_point = Decimal::from_ratio(123_456_000_000_000u128, 543u32); + + let expected_rewarding_params = RewardingParams { + interval: IntervalRewardParams { + reward_pool: Decimal::from_atomics(100_000_000_000_000u128, 0).unwrap(), + staking_supply: Decimal::from_atomics(123_456_000_000_000u128, 0).unwrap(), + epoch_reward_budget: expected_epoch_reward_budget, + stake_saturation_point: expected_stake_saturation_point, + sybil_resistance: Percent::from_percentage_value(23).unwrap(), + active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), + interval_pool_emission: Percent::from_percentage_value(1).unwrap(), + }, + rewarded_set_size: 543, + active_set_size: 123, + }; + + let state = mixnet_params_storage::CONTRACT_STATE + .load(deps.as_ref().storage) + .unwrap(); + assert_eq!(state, expected_state); + + let rewarding_params = rewards_storage::REWARDING_PARAMS + .load(deps.as_ref().storage) + .unwrap(); + assert_eq!(rewarding_params, expected_rewarding_params); + + let interval = interval_storage::current_interval(deps.as_ref().storage).unwrap(); + assert_eq!(interval.epochs_in_interval(), 1234); + assert_eq!(interval.epoch_length(), Duration::from_secs(4321)); + assert_eq!(interval.current_interval_id(), 0); + assert_eq!(interval.current_epoch_id(), 0); } } diff --git a/contracts/mixnet/src/delegations/helpers.rs b/contracts/mixnet/src/delegations/helpers.rs new file mode 100644 index 0000000000..5a639d6c05 --- /dev/null +++ b/contracts/mixnet/src/delegations/helpers.rs @@ -0,0 +1,65 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::delegations::storage; +use crate::rewards::storage as rewards_storage; +use cosmwasm_std::{Coin, Storage}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::mixnode::MixNodeRewarding; +use mixnet_contract_common::Delegation; + +pub(crate) fn undelegate( + store: &mut dyn Storage, + delegation: Delegation, + mut mix_rewarding: MixNodeRewarding, +) -> Result { + let tokens = mix_rewarding.undelegate(&delegation)?; + + rewards_storage::MIXNODE_REWARDING.save(store, delegation.node_id, &mix_rewarding)?; + storage::delegations().replace(store, delegation.storage_key(), None, Some(&delegation))?; + + Ok(tokens) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::support::tests::test_helpers::{performance, TestSetup}; + use cosmwasm_std::{Addr, Decimal, Uint128}; + use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; + + #[test] + fn undelegation_updates_mix_rewarding_storage_and_deletes_delegation() { + let mut test = TestSetup::new(); + + let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(100_000_000_000))); + let delegator = "delegator"; + let og_amount = Uint128::new(200_000_000); + test.add_immediate_delegation(delegator, og_amount, mix_id); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + let dist1 = test.reward_with_distribution(mix_id, performance(100.0)); + test.skip_to_next_epoch_end(); + let dist2 = test.reward_with_distribution(mix_id, performance(100.0)); + + let mix_rewarding = test.mix_rewarding(mix_id); + let delegation = test.delegation(mix_id, delegator, &None); + + let expected_amount = og_amount + truncate_reward_amount(dist1.delegates + dist2.delegates); + + let res = undelegate(test.deps_mut().storage, delegation, mix_rewarding).unwrap(); + assert_eq!(res.amount, expected_amount); + + let mix_rewarding = test.mix_rewarding(mix_id); + assert_eq!(mix_rewarding.delegates, Decimal::zero()); + assert_eq!(mix_rewarding.unique_delegations, 0); + + let storage_key = + Delegation::generate_storage_key(mix_id, &Addr::unchecked(delegator), None); + assert!(storage::delegations() + .may_load(test.deps().storage, storage_key) + .unwrap() + .is_none()); + } +} diff --git a/contracts/mixnet/src/delegations/mod.rs b/contracts/mixnet/src/delegations/mod.rs index 32fd774de7..026c0d13ec 100644 --- a/contracts/mixnet/src/delegations/mod.rs +++ b/contracts/mixnet/src/delegations/mod.rs @@ -1,6 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +pub(crate) mod helpers; pub(crate) mod queries; pub(crate) mod storage; pub(crate) mod transactions; diff --git a/contracts/mixnet/src/delegations/queries.rs b/contracts/mixnet/src/delegations/queries.rs index 8b60091e4f..d921c22b26 100644 --- a/contracts/mixnet/src/delegations/queries.rs +++ b/contracts/mixnet/src/delegations/queries.rs @@ -1,69 +1,80 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use super::storage; -use crate::error::ContractError; +use crate::constants::{ + DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT, DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT, +}; +use crate::mixnodes::storage as mixnodes_storage; +use cosmwasm_std::Deps; use cosmwasm_std::Order; use cosmwasm_std::StdResult; -use cosmwasm_std::{Api, Deps, Storage}; -use cw_storage_plus::{Bound, PrimaryKey}; -use mixnet_contract_common::mixnode::DelegationEvent; +use cw_storage_plus::Bound; +use mixnet_contract_common::delegation::{MixNodeDelegationResponse, OwnerProxySubKey}; use mixnet_contract_common::{ - delegation, Delegation, IdentityKey, PagedDelegatorDelegationsResponse, - PagedMixDelegationsResponse, + delegation, Delegation, NodeId, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, + PagedMixNodeDelegationsResponse, }; -pub(crate) fn query_pending_delegation_events( +pub(crate) fn query_mixnode_delegations_paged( deps: Deps<'_>, - owner_address: String, - proxy_address: Option, -) -> Result, ContractError> { - let validated_owner = deps.api.addr_validate(&owner_address)?; - let validated_proxy = proxy_address - .map(|proxy| deps.api.addr_validate(&proxy)) - .transpose()?; + mix_id: NodeId, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT) + .min(DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT) as usize; - let key_prefix = delegation::generate_storage_key(&validated_owner, validated_proxy.as_ref()); + let start = start_after.map(|subkey| { + Bound::exclusive(Delegation::generate_storage_key_with_subkey(mix_id, subkey)) + }); - Ok(storage::PENDING_DELEGATION_EVENTS - .sub_prefix(key_prefix) - .range(deps.storage, None, None, Order::Ascending) - .filter_map(|r| r.ok()) - .map(|(_key, delegation_event)| delegation_event) - .collect::>()) + let delegations = storage::delegations() + .idx + .mixnode + .prefix(mix_id) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|record| record.map(|r| r.1)) + .collect::>>()?; + + let start_next_after = delegations.last().map(|del| del.proxy_storage_key()); + + Ok(PagedMixNodeDelegationsResponse::new( + delegations, + start_next_after, + )) } pub(crate) fn query_delegator_delegations_paged( deps: Deps<'_>, delegation_owner: String, - start_after: Option, + start_after: Option<(NodeId, OwnerProxySubKey)>, limit: Option, ) -> StdResult { let validated_owner = deps.api.addr_validate(&delegation_owner)?; let limit = limit - .unwrap_or(storage::DELEGATION_PAGE_DEFAULT_LIMIT) - .min(storage::DELEGATION_PAGE_MAX_LIMIT) as usize; - let start = start_after.map(|mix_identity| { - Bound::ExclusiveRaw((mix_identity, validated_owner.clone()).joined_key()) + .unwrap_or(DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT) + .min(DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(|(mix_id, subkey)| { + Bound::exclusive(Delegation::generate_storage_key_with_subkey(mix_id, subkey)) }); let delegations = storage::delegations() .idx .owner .prefix(validated_owner) - .range_raw(deps.storage, start, None, Order::Ascending) + .range(deps.storage, start, None, Order::Ascending) .take(limit) .map(|record| record.map(|r| r.1)) .collect::>>()?; - let start_next_after = if delegations.len() < limit { - None - } else { - delegations - .last() - .map(|delegation| delegation.node_identity()) - }; + let start_next_after = delegations + .last() + .map(|del| (del.node_id, del.proxy_storage_key())); Ok(PagedDelegatorDelegationsResponse::new( delegations, @@ -71,540 +82,655 @@ pub(crate) fn query_delegator_delegations_paged( )) } -pub fn query_all_delegation_keys(storage: &dyn Storage) -> Result, ContractError> { - Ok(storage::delegations() - .keys_raw(storage, None, None, Order::Ascending) - .map(hex::encode) - .collect()) -} - -use std::collections::HashSet; - -// This should only be exposed directly on the contract via nymd binary, not through the nymd clients -pub fn debug_query_all_delegation_values( - storage: &dyn Storage, -) -> Result, ContractError> { - use crate::delegations::storage::{ - DelegationIndex, DELEGATION_MIXNODE_IDX_NAMESPACE, DELEGATION_OWNER_IDX_NAMESPACE, - DELEGATION_PK_NAMESPACE, - }; - - use cw_storage_plus::{IndexedMap, MultiIndex}; - - type PrimaryKey = Vec; - - fn all_delegations<'a>() -> IndexedMap<'a, PrimaryKey, Delegation, DelegationIndex<'a>> { - let indexes = DelegationIndex { - owner: MultiIndex::new( - |d| d.owner.clone(), - DELEGATION_PK_NAMESPACE, - DELEGATION_OWNER_IDX_NAMESPACE, - ), - mixnode: MultiIndex::new( - |d| d.node_identity.clone(), - DELEGATION_PK_NAMESPACE, - DELEGATION_MIXNODE_IDX_NAMESPACE, - ), - }; - - IndexedMap::new(DELEGATION_PK_NAMESPACE, indexes) - } - - let all_delegations = all_delegations() - .range(storage, None, None, Order::Ascending) - .filter_map(|r| r.ok()) - .map(|(_key, delegation)| delegation) - .collect::>(); - - Ok(all_delegations) -} - // queries for delegation value of given address for particular node pub(crate) fn query_mixnode_delegation( - storage: &dyn Storage, - api: &dyn Api, - mix_identity: IdentityKey, - delegator: String, + deps: Deps<'_>, + mix_id: NodeId, + delegation_owner: String, proxy: Option, -) -> Result, ContractError> { - let validated_delegator = api.addr_validate(&delegator)?; - let proxy = proxy.map(|p| api.addr_validate(&p)).transpose()?; - let storage_key = ( - mix_identity.clone(), - mixnet_contract_common::delegation::generate_storage_key( - &validated_delegator, - proxy.as_ref(), - ), - ); +) -> StdResult { + let validated_owner = deps.api.addr_validate(&delegation_owner)?; + let validated_proxy = proxy + .map(|proxy| deps.api.addr_validate(&proxy)) + .transpose()?; + let storage_key = + Delegation::generate_storage_key(mix_id, &validated_owner, validated_proxy.as_ref()); - let delegations = storage::delegations() - .prefix(storage_key) - .range(storage, None, None, Order::Ascending) - .filter_map(|d| d.ok()) - .map(|r| r.1) - .collect::>(); + let delegation = storage::delegations().may_load(deps.storage, storage_key)?; - if delegations.is_empty() { - Err(ContractError::NoMixnodeDelegationFound { - identity: mix_identity, - address: delegator, - }) - } else { - Ok(delegations) - } + let mixnode_still_bonded = mixnodes_storage::mixnode_bonds() + .may_load(deps.storage, mix_id)? + .map(|bond| !bond.is_unbonding) + .unwrap_or_default(); + + Ok(MixNodeDelegationResponse::new( + delegation, + mixnode_still_bonded, + )) } -pub(crate) fn query_mixnode_delegations_paged( +pub(crate) fn query_all_delegations_paged( deps: Deps<'_>, - mix_identity: IdentityKey, - start_after: Option<(String, u64)>, + start_after: Option, limit: Option, -) -> StdResult { +) -> StdResult { let limit = limit - .unwrap_or(storage::DELEGATION_PAGE_DEFAULT_LIMIT) - .min(storage::DELEGATION_PAGE_MAX_LIMIT) as usize; + .unwrap_or(DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT) + .min(DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT) as usize; - let start = start_after.map(|(addr, height)| { - Bound::exclusive(( - hex::decode(addr).expect("Could not hex decode proxy_storage_key"), - height, - )) - }); + let start = start_after.map(Bound::exclusive); let delegations = storage::delegations() - .sub_prefix(mix_identity) .range(deps.storage, start, None, Order::Ascending) .take(limit) - .filter_map(|r| r.ok()) - .map(|record| record.1) - .collect::>(); + .map(|res| res.map(|item| item.1)) + .collect::>>()?; - let start_next_after = if delegations.len() < limit { - None - } else { - delegations.last().map(|delegation| { - ( - hex::encode(delegation.proxy_storage_key()), - delegation.block_height(), - ) - }) - }; + let start_next_after = delegations.last().map(|del| del.storage_key()); - Ok(PagedMixDelegationsResponse::new( + Ok(PagedAllDelegationsResponse::new( delegations, start_next_after, )) } #[cfg(test)] -pub(crate) mod tests { +mod tests { use super::*; - use crate::support::tests::{fixtures::TEST_COIN_DENOM, test_helpers}; - use cosmwasm_std::{coin, Addr, Storage}; - use rand::Rng; + use crate::support::tests::test_helpers::TestSetup; - pub fn store_n_mix_delegations(n: u32, storage: &mut dyn Storage, node_identity: &str) { - for i in 0..n { - let address = format!("address{}", i); - test_helpers::save_dummy_delegation(storage, node_identity, address, 1); + fn add_dummy_mixes_with_delegations(test: &mut TestSetup, delegators: usize, mixes: usize) { + for i in 0..mixes { + let mix_id = test.add_dummy_mixnode(&format!("mix-owner{}", i), None); + for delegator in 0..delegators { + let name = &format!("delegator{}", delegator); + test.add_immediate_delegation(name, 100_000_000u32, mix_id) + } } } #[cfg(test)] - mod querying_for_mixnode_delegations_paged { - use std::collections::HashSet; - + mod mixnode_delegations { use super::*; - use mixnet_contract_common::IdentityKey; - use rand::{distributions::Alphanumeric, SeedableRng}; + use crate::support::tests::test_helpers; #[test] - fn retrieval_obeys_limits() { - let mut deps = test_helpers::init_contract(); - let limit = 2; - let node_identity: IdentityKey = "foo".into(); - store_n_mix_delegations(100, &mut deps.storage, &node_identity); + fn obeys_limits() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); - let page1 = query_mixnode_delegations_paged( - deps.as_ref(), - node_identity, - None, - Option::from(limit), - ) - .unwrap(); + let env = test.env(); + test_helpers::add_dummy_delegations(test.deps_mut(), env, mix_id, 200); + + let limit = 2; + + let page1 = + query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(limit)).unwrap(); assert_eq!(limit, page1.delegations.len() as u32); } #[test] - fn retrieval_has_default_limit() { - let mut deps = test_helpers::init_contract(); - let node_identity: IdentityKey = "foo".into(); - store_n_mix_delegations( - storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10, - &mut deps.storage, - &node_identity, - ); + fn has_default_limit() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let env = test.env(); + test_helpers::add_dummy_delegations(test.deps_mut(), env, mix_id, 500); // query without explicitly setting a limit - let page1 = - query_mixnode_delegations_paged(deps.as_ref(), node_identity, None, None).unwrap(); + let page1 = query_mixnode_delegations_paged(test.deps(), mix_id, None, None).unwrap(); + assert_eq!( - storage::DELEGATION_PAGE_DEFAULT_LIMIT, + DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT, page1.delegations.len() as u32 ); } #[test] - fn retrieval_has_max_limit() { - let mut deps = test_helpers::init_contract(); - let node_identity: IdentityKey = "foo".into(); - store_n_mix_delegations( - storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10, - &mut deps.storage, - &node_identity, - ); + fn has_max_limit() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let env = test.env(); + test_helpers::add_dummy_delegations(test.deps_mut(), env, mix_id, 5000); // query with a crazily high limit in an attempt to use too many resources - let crazy_limit = 1000 * storage::DELEGATION_PAGE_DEFAULT_LIMIT; - let page1 = query_mixnode_delegations_paged( - deps.as_ref(), - node_identity, - None, - Option::from(crazy_limit), - ) - .unwrap(); + let crazy_limit = 10000; + let page1 = + query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(crazy_limit)) + .unwrap(); - // we default to a decent sized upper bound instead - let expected_limit = storage::DELEGATION_PAGE_MAX_LIMIT; - assert_eq!(expected_limit, page1.delegations.len() as u32); + assert_eq!( + DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT, + page1.delegations.len() as u32 + ); } #[test] fn pagination_works() { - let dummy_seed = [42u8; 32]; - let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let mut test = TestSetup::new(); - let mut deps = test_helpers::init_contract(); - let node_identity: IdentityKey = "foo".into(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + test.add_immediate_delegation("addr1", 1000u32, mix_id); - let mut delegation_test_data = vec![]; - let mut returned_delegation_data = HashSet::new(); + let per_page = 2; + let page1 = + query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(per_page)).unwrap(); - // Crete a bunch of randomly ordered (in storage) delegations - for _ in 0..200 { - delegation_test_data.push(( - rng.clone() - .sample_iter(&Alphanumeric) - .take(30) - .map(char::from) - .collect::(), - rng.gen::() as u64, - )) - } + // page should have 1 result on it + assert_eq!(1, page1.delegations.len()); - for (address, block_height) in delegation_test_data.iter() { - test_helpers::save_dummy_delegation( - &mut deps.storage, - &node_identity, - address, - *block_height, - ); - } + // save another + test.add_immediate_delegation("addr2", 1000u32, mix_id); - let per_page = 100; + // page1 should have 2 results on it + let page1 = + query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(per_page)).unwrap(); + assert_eq!(2, page1.delegations.len()); - // page1 still has 2 results - let page1 = query_mixnode_delegations_paged( - deps.as_ref(), - node_identity.clone(), - None, - Option::from(per_page), - ) - .unwrap(); + test.add_immediate_delegation("addr3", 1000u32, mix_id); - let start_after = page1.start_next_after.unwrap(); - assert_eq!(100, page1.delegations.len()); - assert_eq!( - (( - "5874735a724c52587679656777795a446a754a746c59694735423165694a".to_string(), - 1594717548 - )), - start_after - ); - - for delegation in page1.delegations { - returned_delegation_data.insert(delegation.owner().to_string()); - } + // page1 still has the same 2 results + let another_page1 = + query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(per_page)).unwrap(); + assert_eq!(2, another_page1.delegations.len()); + assert_eq!(page1, another_page1); // retrieving the next page should start after the last key on this page + let start_after = page1.start_next_after.unwrap(); + let page2 = query_mixnode_delegations_paged( + test.deps(), + mix_id, + Some(start_after.clone()), + Some(per_page), + ) + .unwrap(); + + assert_eq!(1, page2.delegations.len()); + + // save another one + test.add_immediate_delegation("addr4", 1000u32, mix_id); let page2 = query_mixnode_delegations_paged( - deps.as_ref(), - node_identity.clone(), - Option::from(start_after.clone()), - Option::from(per_page), + test.deps(), + mix_id, + Some(start_after), + Some(per_page), ) .unwrap(); - let start_after = page2.start_next_after.unwrap(); - assert_eq!( - ( - "7a6b48546c63674f57417948384e6f494a326c6b5a63767668597346696b".to_string(), - 3448133410 - ), - start_after - ); - - for delegation in page2.delegations { - returned_delegation_data.insert(delegation.owner().to_string()); - } - - let page3 = query_mixnode_delegations_paged( - deps.as_ref(), - node_identity.clone(), - Option::from(start_after), - Option::from(per_page), - ) - .unwrap(); - - assert!(page3.start_next_after.is_none()); - - for delegation in delegation_test_data { - assert!(returned_delegation_data.contains(&*delegation.0)); - } - } - } - - #[test] - fn mix_deletion_query_returns_current_delegation_value() { - let mut deps = test_helpers::init_contract(); - let node_identity: IdentityKey = "foo".into(); - let delegation_owner = Addr::unchecked("bar"); - - let delegation = Delegation::new( - delegation_owner.clone(), - node_identity.clone(), - coin(1234, TEST_COIN_DENOM), - 1234, - None, - ); - - storage::delegations() - .save(deps.as_mut().storage, delegation.storage_key(), &delegation) - .unwrap(); - - assert_eq!( - Ok(vec![delegation]), - query_mixnode_delegation( - &deps.storage, - &deps.api, - node_identity, - delegation_owner.to_string(), - None - ) - ) - } - - #[test] - fn mix_deletion_query_returns_error_if_delegation_doesnt_exist() { - let mut deps = test_helpers::init_contract(); - - let node_identity1: IdentityKey = "foo1".into(); - let node_identity2: IdentityKey = "foo2".into(); - let delegation_owner1 = Addr::unchecked("bar"); - let delegation_owner2 = Addr::unchecked("bar2"); - - assert_eq!( - Err(ContractError::NoMixnodeDelegationFound { - identity: node_identity1.clone(), - address: delegation_owner1.to_string(), - }), - query_mixnode_delegation( - &deps.storage, - &deps.api, - node_identity1.clone(), - delegation_owner1.to_string(), - None - ) - ); - - // add delegation from a different address - let delegation = Delegation::new( - delegation_owner2, - node_identity1.clone(), - coin(1234, TEST_COIN_DENOM), - 1234, - None, - ); - - storage::delegations() - .save(deps.as_mut().storage, delegation.storage_key(), &delegation) - .unwrap(); - - assert_eq!( - Err(ContractError::NoMixnodeDelegationFound { - identity: node_identity1.clone(), - address: delegation_owner1.to_string(), - }), - query_mixnode_delegation( - &deps.storage, - &deps.api, - node_identity1.clone(), - delegation_owner1.to_string(), - None - ) - ); - - // add delegation for a different node - let delegation = Delegation::new( - delegation_owner1.clone(), - node_identity2, - coin(1234, TEST_COIN_DENOM), - 1234, - None, - ); - - storage::delegations() - .save(deps.as_mut().storage, delegation.storage_key(), &delegation) - .unwrap(); - - assert_eq!( - Err(ContractError::NoMixnodeDelegationFound { - identity: node_identity1.clone(), - address: delegation_owner1.to_string() - }), - query_mixnode_delegation( - &deps.storage, - &deps.api, - node_identity1, - delegation_owner1.to_string(), - None - ) - ) - } - - #[cfg(test)] - mod querying_for_reverse_mixnode_delegations_paged { - use super::*; - - fn store_n_reverse_delegations(n: u32, storage: &mut dyn Storage, delegation_owner: &str) { - for i in 0..n { - let node_identity = format!("node{}", i); - test_helpers::save_dummy_delegation(storage, node_identity, delegation_owner, 1); - } + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.delegations.len()); } #[test] - fn retrieval_obeys_limits() { - let mut deps = test_helpers::init_contract(); + fn all_retrieved_delegations_are_towards_specified_mixnode() { + let mut test = TestSetup::new(); + let mix_id1 = test.add_dummy_mixnode("mix-owner1", None); + let mix_id2 = test.add_dummy_mixnode("mix-owner2", None); + let mix_id3 = test.add_dummy_mixnode("mix-owner3", None); + let mix_id4 = test.add_dummy_mixnode("mix-owner4", None); + + let env = test.env(); + // add other "out of order" delegations manually + test.add_immediate_delegation("random-delegator1", 1000u32, mix_id2); + test.add_immediate_delegation("random-delegator2", 1000u32, mix_id2); + test_helpers::add_dummy_delegations(test.deps_mut(), env.clone(), mix_id1, 10); + test_helpers::add_dummy_delegations(test.deps_mut(), env.clone(), mix_id2, 10); + test_helpers::add_dummy_delegations(test.deps_mut(), env.clone(), mix_id3, 10); + test.add_immediate_delegation("random-delegator3", 1000u32, mix_id2); + test_helpers::add_dummy_delegations(test.deps_mut(), env, mix_id4, 10); + test.add_immediate_delegation("random-delegator4", 1000u32, mix_id2); + + let res1 = query_mixnode_delegations_paged(test.deps(), mix_id1, None, None).unwrap(); + assert_eq!(res1.delegations.len(), 10); + assert!(res1.delegations.into_iter().all(|d| d.node_id == mix_id1)); + + let res2 = query_mixnode_delegations_paged(test.deps(), mix_id2, None, None).unwrap(); + assert_eq!(res2.delegations.len(), 14); + assert!(res2.delegations.into_iter().all(|d| d.node_id == mix_id2)); + + let res3 = query_mixnode_delegations_paged(test.deps(), mix_id3, None, None).unwrap(); + assert_eq!(res3.delegations.len(), 10); + assert!(res3.delegations.into_iter().all(|d| d.node_id == mix_id3)); + + let res4 = query_mixnode_delegations_paged(test.deps(), mix_id4, None, None).unwrap(); + assert_eq!(res4.delegations.len(), 10); + assert!(res4.delegations.into_iter().all(|d| d.node_id == mix_id4)); + } + } + + 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}; + + #[test] + fn obeys_limits() { + let mut test = TestSetup::new(); + + // 50 mixnodes with 500 delegations each; + add_dummy_mixes_with_delegations(&mut test, 500, 50); + let limit = 2; - let delegation_owner = "foo".to_string(); - store_n_reverse_delegations(100, &mut deps.storage, &delegation_owner); let page1 = query_delegator_delegations_paged( - deps.as_ref(), - delegation_owner, + test.deps(), + "delegator1".into(), None, - Option::from(limit), + Some(limit), ) .unwrap(); assert_eq!(limit, page1.delegations.len() as u32); } #[test] - fn retrieval_has_default_limit() { - let mut deps = test_helpers::init_contract(); - let delegation_owner = "foo".to_string(); - store_n_reverse_delegations( - storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10, - &mut deps.storage, - &delegation_owner, - ); + fn has_default_limit() { + let mut test = TestSetup::new(); + add_dummy_mixes_with_delegations(&mut test, 10, 500); // query without explicitly setting a limit let page1 = - query_delegator_delegations_paged(deps.as_ref(), delegation_owner, None, None) + query_delegator_delegations_paged(test.deps(), "delegator1".into(), None, None) .unwrap(); + assert_eq!( - storage::DELEGATION_PAGE_DEFAULT_LIMIT, + DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT, page1.delegations.len() as u32 ); } #[test] - fn retrieval_has_max_limit() { - let mut deps = test_helpers::init_contract(); - let delegation_owner = "foo".to_string(); - store_n_reverse_delegations( - storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10, - &mut deps.storage, - &delegation_owner, - ); + fn has_max_limit() { + let mut test = TestSetup::new(); + add_dummy_mixes_with_delegations(&mut test, 10, 500); - // query with a crazy high limit in an attempt to use too many resources - let crazy_limit = 1000 * storage::DELEGATION_PAGE_DEFAULT_LIMIT; + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 10000; let page1 = query_delegator_delegations_paged( - deps.as_ref(), - delegation_owner, + test.deps(), + "delegator1".into(), None, - Option::from(crazy_limit), + Some(crazy_limit), ) .unwrap(); - // we default to a decent sized upper bound instead - let expected_limit = storage::DELEGATION_PAGE_MAX_LIMIT; - assert_eq!(expected_limit, page1.delegations.len() as u32); + assert_eq!( + DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT, + page1.delegations.len() as u32 + ); } #[test] fn pagination_works() { - let mut deps = test_helpers::init_contract(); - let delegation_owner = "bar".to_string(); + let mut test = TestSetup::new(); - for j in 0..20 { - for i in 0..10 { - test_helpers::save_dummy_delegation( - &mut deps.storage, - format!("{}-{}", j, i), - delegation_owner.clone(), - i, - ); - } + // note that mix_ids are monotonically increasing + let mix_id1 = test.add_dummy_mixnode("mix-owner1", None); + let mix_id2 = test.add_dummy_mixnode("mix-owner2", None); + let mix_id3 = test.add_dummy_mixnode("mix-owner3", None); + let mix_id4 = test.add_dummy_mixnode("mix-owner4", None); + let mix_id5 = test.add_dummy_mixnode("mix-owner5", None); + + // add few delegations from unrelated delegators + for mix_id in [mix_id1, mix_id2, mix_id3, mix_id4, mix_id5] { + test.add_immediate_delegation("random1", 1000u32, mix_id); + test.add_immediate_delegation("random2", 1000u32, mix_id); + test.add_immediate_delegation("random1", 1000u32, mix_id); } - let per_page = 100; + let delegator = "delegator"; + + test.add_immediate_delegation(delegator, 1000u32, mix_id1); + + let per_page = 2; let page1 = query_delegator_delegations_paged( - deps.as_ref(), - delegation_owner.clone(), + test.deps(), + delegator.into(), None, - Option::from(per_page), + Some(per_page), ) .unwrap(); + // page should have 1 result on it + assert_eq!(1, page1.delegations.len()); + + // save another + test.add_immediate_delegation(delegator, 1000u32, mix_id2); + + // page1 should have 2 results on it + let page1 = query_delegator_delegations_paged( + test.deps(), + delegator.into(), + None, + Some(per_page), + ) + .unwrap(); + assert_eq!(2, page1.delegations.len()); + + test.add_immediate_delegation(delegator, 1000u32, mix_id3); + + // page1 still has the same 2 results + let another_page1 = query_delegator_delegations_paged( + test.deps(), + delegator.into(), + None, + Some(per_page), + ) + .unwrap(); + assert_eq!(2, another_page1.delegations.len()); + assert_eq!(page1, another_page1); + + // retrieving the next page should start after the last key on this page let start_after = page1.start_next_after.unwrap(); - assert_eq!(per_page as usize, page1.delegations.len()); - assert_eq!(start_after, "9-9".to_string()); + let page2 = query_delegator_delegations_paged( + test.deps(), + delegator.into(), + Some(start_after.clone()), + Some(per_page), + ) + .unwrap(); + + assert_eq!(1, page2.delegations.len()); + + // save another one + test.add_immediate_delegation(delegator, 1000u32, mix_id4); let page2 = query_delegator_delegations_paged( - deps.as_ref(), - delegation_owner.clone(), + test.deps(), + delegator.into(), Some(start_after), - Option::from(per_page), + Some(per_page), ) .unwrap(); - let start_after = page2.start_next_after.unwrap(); - assert_eq!(start_after, "19-9".to_string()); + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.delegations.len()); + } - let page3 = query_delegator_delegations_paged( - deps.as_ref(), - delegation_owner.clone(), - Some(start_after), - Option::from(per_page), - ) - .unwrap(); + #[test] + fn all_retrieved_delegations_are_from_the_specified_delegator() { + let mut test = TestSetup::new(); + // 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); - assert!(page3.start_next_after.is_none()); + // 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(), + 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) + .unwrap(); + assert_eq!(res1.delegations.len(), 100); + assert!(res1 + .delegations + .into_iter() + .all(|d| d.owner == Addr::unchecked("delegator2"))); + + let res2 = + query_delegator_delegations_paged(test.deps(), "delegator35".into(), None, None) + .unwrap(); + assert_eq!(res2.delegations.len(), 100); + assert!(res2 + .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) + } + } + + mod all_delegations { + use super::*; + + #[test] + fn obeys_limits() { + let mut test = TestSetup::new(); + + // 50 mixnodes with 500 delegations each; + add_dummy_mixes_with_delegations(&mut test, 500, 50); + + let limit = 2; + + let page1 = query_all_delegations_paged(test.deps(), None, Some(limit)).unwrap(); + assert_eq!(limit, page1.delegations.len() as u32); + } + + #[test] + fn has_default_limit() { + let mut test = TestSetup::new(); + add_dummy_mixes_with_delegations(&mut test, 10, 500); + + // query without explicitly setting a limit + let page1 = query_all_delegations_paged(test.deps(), None, None).unwrap(); + + assert_eq!( + DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT, + page1.delegations.len() as u32 + ); + } + + #[test] + fn has_max_limit() { + let mut test = TestSetup::new(); + add_dummy_mixes_with_delegations(&mut test, 10, 500); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 10000; + let page1 = query_all_delegations_paged(test.deps(), None, Some(crazy_limit)).unwrap(); + + assert_eq!( + DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT, + page1.delegations.len() as u32 + ); + } + + #[test] + fn pagination_works() { + let mut test = TestSetup::new(); + + // note that mix_ids are monotonically increasing and are the first chunk of all + // delegation storage keys, + let mix_id1 = test.add_dummy_mixnode("mix-owner1", None); + let mix_id2 = test.add_dummy_mixnode("mix-owner2", None); + + let delegator1 = "delegator1"; + let delegator2 = "delegator2"; + + test.add_immediate_delegation(delegator1, 1000u32, mix_id1); + + let per_page = 2; + let page1 = query_all_delegations_paged(test.deps(), None, Some(per_page)).unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.delegations.len()); + assert!( + page1.delegations[0].owner.as_str() == delegator1 + && page1.delegations[0].node_id == mix_id1 + ); + + test.add_immediate_delegation(delegator1, 1000u32, mix_id2); + + let page1 = query_all_delegations_paged(test.deps(), None, Some(per_page)).unwrap(); + + // page1 should have 2 results on it + assert_eq!(2, page1.delegations.len()); + assert!( + page1.delegations[0].owner.as_str() == delegator1 + && page1.delegations[0].node_id == mix_id1 + ); + assert!( + page1.delegations[1].owner.as_str() == delegator1 + && page1.delegations[1].node_id == mix_id2 + ); + + test.add_immediate_delegation(delegator2, 1000u32, mix_id1); + + // note that the order of results changed on page1 + let another_page1 = + query_all_delegations_paged(test.deps(), None, Some(per_page)).unwrap(); + assert_eq!(2, another_page1.delegations.len()); + assert!( + another_page1.delegations[0].owner.as_str() == delegator1 + && another_page1.delegations[0].node_id == mix_id1 + ); + assert!( + another_page1.delegations[1].owner.as_str() == delegator2 + && another_page1.delegations[1].node_id == mix_id1 + ); + + // retrieving the next page should start after the last key on this page + let start_after = another_page1.start_next_after.unwrap(); + let page2 = + query_all_delegations_paged(test.deps(), Some(start_after.clone()), Some(per_page)) + .unwrap(); + + assert_eq!(1, page2.delegations.len()); + assert!( + page2.delegations[0].owner.as_str() == delegator1 + && page2.delegations[0].node_id == mix_id2 + ); + + // save another one + test.add_immediate_delegation(delegator2, 1000u32, mix_id2); + + let page2 = query_all_delegations_paged(test.deps(), Some(start_after), Some(per_page)) + .unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.delegations.len()); + assert!( + page2.delegations[0].owner.as_str() == delegator1 + && page2.delegations[0].node_id == mix_id2 + ); + assert!( + page2.delegations[1].owner.as_str() == delegator2 + && page2.delegations[1].node_id == mix_id2 + ); + } + } + + #[cfg(test)] + mod querying_for_specific_mixnode_delegation { + use super::*; + + #[test] + fn when_delegation_doesnt_exist() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + let owner = "owner"; + + let res = query_mixnode_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); + assert!(res.delegation.is_none()); + assert!(res.mixnode_still_bonded); + } + + #[test] + fn when_delegation_exists_but_mixnode_has_unbonded() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + let owner = "owner"; + + test.add_immediate_delegation(owner, 1000u32, mix_id); + test.immediately_unbond_mixnode(mix_id); + + let res = query_mixnode_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); + assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); + assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); + assert!(!res.mixnode_still_bonded); + } + + #[test] + fn when_delegation_exists_but_mixnode_is_unbonding() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + let owner = "owner"; + + test.add_immediate_delegation(owner, 1000u32, mix_id); + test.start_unbonding_mixnode(mix_id); + + let res = query_mixnode_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); + assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); + assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); + assert!(!res.mixnode_still_bonded); + } + + #[test] + fn when_delegation_exists_with_fully_bonded_node() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + let owner = "owner"; + + test.add_immediate_delegation(owner, 1000u32, mix_id); + + let res = query_mixnode_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); + assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); + assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); + assert!(res.mixnode_still_bonded); } } } diff --git a/contracts/mixnet/src/delegations/storage.rs b/contracts/mixnet/src/delegations/storage.rs index 5715bc2c27..f33fa97d3d 100644 --- a/contracts/mixnet/src/delegations/storage.rs +++ b/contracts/mixnet/src/delegations/storage.rs @@ -1,32 +1,20 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cw_storage_plus::{Index, IndexList, IndexedMap, Map, MultiIndex}; -use mixnet_contract_common::{mixnode::DelegationEvent, Addr, Delegation, IdentityKey}; +use crate::constants::{ + DELEGATION_MIXNODE_IDX_NAMESPACE, DELEGATION_OWNER_IDX_NAMESPACE, DELEGATION_PK_NAMESPACE, +}; +use cw_storage_plus::{Index, IndexList, IndexedMap, MultiIndex}; +use mixnet_contract_common::delegation::OwnerProxySubKey; +use mixnet_contract_common::{Addr, Delegation, NodeId}; -// storage prefixes -pub const DELEGATION_PK_NAMESPACE: &str = "dl"; -pub const DELEGATION_OWNER_IDX_NAMESPACE: &str = "dlo"; -pub const DELEGATION_MIXNODE_IDX_NAMESPACE: &str = "dlm"; - -pub const PENDING_DELEGATION_EVENTS: Map< - (OwnerAddress, BlockHeight, IdentityKey), - DelegationEvent, -> = Map::new("pend2"); - -// paged retrieval limits for all queries and transactions -pub(crate) const DELEGATION_PAGE_MAX_LIMIT: u32 = 500; -pub(crate) const DELEGATION_PAGE_DEFAULT_LIMIT: u32 = 250; - -type BlockHeight = u64; -type OwnerAddress = Vec; -// It's a composite key on node's identity, delegator address, and block height -type PrimaryKey = (IdentityKey, OwnerAddress, BlockHeight); +// It's a composite key on node's id and delegator address +type PrimaryKey = (NodeId, OwnerProxySubKey); pub(crate) struct DelegationIndex<'a> { pub(crate) owner: MultiIndex<'a, Addr, Delegation, PrimaryKey>, - pub(crate) mixnode: MultiIndex<'a, IdentityKey, Delegation, PrimaryKey>, + pub(crate) mixnode: MultiIndex<'a, NodeId, Delegation, PrimaryKey>, } impl<'a> IndexList for DelegationIndex<'a> { @@ -36,20 +24,6 @@ impl<'a> IndexList for DelegationIndex<'a> { } } -// I was really going back and forth about the data stored on the disk vs primary key duplication. -// It was basically between convenience and bloat, but in the end I decided the convenience wins. -// -// Basically I had 2 approaches. a) store delegator address and mixnode identity only as primary key of delegation or -// b) store it both as primary key AND inside delegation data. -// For the longest time I was in favour of a), since that removed any data duplication. However..., -// that also required that during index creation I recovered delegator address and mixnode identity -// from the Vec. That doesn't sound that terrible. However, even though I'm 99.99% certain that -// conversion would be impossible to fail, I'd still have to call an `unwrap` here due to required -// type signature and I didn't feel super comfortable doing that in our smart contract... -// So to get rid of this uncertainty I went with the b) approach. Even though each stored delegation -// takes over ~250B (since the key has to be duplicated), in the grand blockchain scheme of things -// it's not that terrible. Say we had 100_000_000 delegations -> that's still only 25GB of data -// and as a nice by-product it cleans up code a little bit by only having a single Delegation type. pub(crate) fn delegations<'a>() -> IndexedMap<'a, PrimaryKey, Delegation, DelegationIndex<'a>> { let indexes = DelegationIndex { owner: MultiIndex::new( @@ -58,7 +32,7 @@ pub(crate) fn delegations<'a>() -> IndexedMap<'a, PrimaryKey, Delegation, Delega DELEGATION_OWNER_IDX_NAMESPACE, ), mixnode: MultiIndex::new( - |d| d.node_identity.clone(), + |d| d.node_id, DELEGATION_PK_NAMESPACE, DELEGATION_MIXNODE_IDX_NAMESPACE, ), @@ -66,130 +40,3 @@ pub(crate) fn delegations<'a>() -> IndexedMap<'a, PrimaryKey, Delegation, Delega IndexedMap::new(DELEGATION_PK_NAMESPACE, indexes) } - -#[cfg(test)] -mod tests { - use crate::delegations::storage; - use cosmwasm_std::Addr; - use mixnet_contract_common::IdentityKey; - - #[cfg(test)] - mod reverse_mix_delegations { - use super::*; - use crate::support::tests::fixtures::TEST_COIN_DENOM; - use crate::support::tests::test_helpers; - use cosmwasm_std::testing::mock_env; - use cosmwasm_std::{coin, Order}; - use mixnet_contract_common::Delegation; - - #[test] - fn reverse_mix_delegation_exists() { - let mut deps = test_helpers::init_contract(); - let node_identity: IdentityKey = "foo".into(); - let delegation_owner = Addr::unchecked("bar"); - let delegation = coin(12345, TEST_COIN_DENOM); - - let dummy_data = Delegation::new( - delegation_owner.clone(), - node_identity.clone(), - delegation, - mock_env().block.height, - None, - ); - - storage::delegations() - .save( - &mut deps.storage, - (node_identity, delegation_owner.as_bytes().to_vec(), 0), - &dummy_data, - ) - .unwrap(); - - let read = storage::delegations() - .idx - .owner - .prefix(delegation_owner) - .range(&deps.storage, None, None, Order::Ascending) - .map(|record| record.unwrap().1) - .collect::>(); - - assert_eq!(1, read.len()); - assert_eq!(dummy_data, read[0]); - } - - #[test] - fn reverse_mix_delegation_returns_none_if_delegation_doesnt_exist() { - let mut deps = test_helpers::init_contract(); - - let node_identity1: IdentityKey = "foo1".into(); - let node_identity2: IdentityKey = "foo2".into(); - let delegation_owner1 = Addr::unchecked("bar"); - let delegation_owner2 = Addr::unchecked("bar2"); - let delegation = coin(12345, TEST_COIN_DENOM); - - assert!(test_helpers::read_delegation( - deps.as_ref().storage, - &node_identity1, - delegation_owner1.as_bytes(), - mock_env().block.height - ) - .is_none()); - - // add delegation for a different node - let dummy_data = Delegation::new( - delegation_owner1.clone(), - node_identity2, - delegation.clone(), - mock_env().block.height, - None, - ); - storage::delegations() - .save( - &mut deps.storage, - ( - node_identity1.clone(), - delegation_owner1.as_bytes().to_vec(), - 0, - ), - &dummy_data, - ) - .unwrap(); - - storage::delegations() - .idx - .owner - .prefix(delegation_owner1.clone()) - .range(&deps.storage, None, None, Order::Ascending) - .map(|record| record.unwrap().1) - .for_each(|delegation| assert_ne!(delegation.node_identity, node_identity1)); - - // add delegation from a different owner - let dummy_data = Delegation::new( - delegation_owner2.clone(), - node_identity1.clone(), - delegation, - mock_env().block.height, - None, - ); - storage::delegations() - .save( - &mut deps.storage, - ( - node_identity1.clone(), - delegation_owner2.as_bytes().to_vec(), - 0, - ), - &dummy_data, - ) - .unwrap(); - - storage::delegations() - .idx - .owner - .prefix(delegation_owner1.clone()) - .range(&deps.storage, None, None, Order::Ascending) - .map(|record| record.unwrap().1) - .for_each(|delegation| assert_ne!(delegation.node_identity, node_identity1)); - } - } -} diff --git a/contracts/mixnet/src/delegations/transactions.rs b/contracts/mixnet/src/delegations/transactions.rs index c5e462929e..7018c65790 100644 --- a/contracts/mixnet/src/delegations/transactions.rs +++ b/contracts/mixnet/src/delegations/transactions.rs @@ -1,1193 +1,411 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::storage::{self, PENDING_DELEGATION_EVENTS}; -// use crate::contract::debug_with_visibility; -// use crate::contract::debug_with_visibility; -use crate::error::ContractError; -use crate::mixnet_contract_settings::storage::{self as mixnet_params_storage}; + +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 cosmwasm_std::{ - coins, wasm_execute, Addr, Api, BankMsg, Coin, DepsMut, Env, Event, MessageInfo, Order, - Response, Storage, Uint128, WasmMsg, -}; +use crate::support::helpers::validate_delegation_stake; +use cosmwasm_std::{Addr, Coin, DepsMut, MessageInfo, Response}; +use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ - new_error_event, new_pending_delegation_event, new_pending_undelegation_event, - new_undelegation_event, + new_pending_delegation_event, new_pending_undelegation_event, }; -use mixnet_contract_common::mixnode::{DelegationEvent, PendingUndelegate}; -use mixnet_contract_common::{Delegation, IdentityKey}; -use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; -use vesting_contract_common::one_ucoin; - -pub fn try_reconcile_all_delegation_events(deps: DepsMut<'_>) -> Result { - _try_reconcile_all_delegation_events(deps.storage, deps.api) -} - -// TODO: Error handling? -pub(crate) fn _try_reconcile_all_delegation_events( - storage: &mut dyn Storage, - api: &dyn Api, -) -> Result { - let pending_delegation_events = PENDING_DELEGATION_EVENTS - .range(storage, None, None, Order::Ascending) - .filter_map(|r| r.ok()) - .collect::, u64, String), DelegationEvent)>>(); - - let mut response = Response::new(); - - // debug_with_visibility(api, "Reconciling delegation events"); - - for (key, delegation_event) in pending_delegation_events { - match delegation_event { - DelegationEvent::Delegate(delegation) => { - // if for some reason the delegation is zero, don't do anything since it should be a no-op anyway - if delegation.amount.amount == Uint128::zero() { - continue; - } - let event = try_reconcile_delegation(storage, delegation)?; - response = response.add_event(event); - } - DelegationEvent::Undelegate(pending_undelegate) => { - let undelegate_response = - try_reconcile_undelegation(storage, api, &pending_undelegate)?; - response = response.add_event(undelegate_response.event); - if let Some(msg) = undelegate_response.bank_msg { - response = response.add_message(msg); - } - if let Some(msg) = undelegate_response.wasm_msg { - response = response.add_message(msg); - } - } - } - PENDING_DELEGATION_EVENTS.remove(storage, key); - } - Ok(response) -} - -fn validate_delegation_stake( - mut delegation: Vec, - mix_denom: String, -) -> Result { - // check if anything was put as delegation - if delegation.is_empty() { - return Err(ContractError::EmptyDelegation); - } - - if delegation.len() > 1 { - return Err(ContractError::MultipleDenoms); - } - - // check that the denomination is correct - if delegation[0].denom != mix_denom { - return Err(ContractError::WrongDenom { mix_denom }); - } - - // check that we have provided a non-zero amount in the delegation - if delegation[0].amount.is_zero() { - return Err(ContractError::EmptyDelegation); - } - - Ok(delegation.pop().unwrap()) -} +use mixnet_contract_common::pending_events::PendingEpochEventData; +use mixnet_contract_common::{Delegation, NodeId}; pub(crate) fn try_delegate_to_mixnode( deps: DepsMut<'_>, - env: Env, info: MessageInfo, - mix_identity: IdentityKey, -) -> Result { - // check if the delegation contains any funds of the appropriate denomination - let amount = - validate_delegation_stake(info.funds, mixnet_params_storage::mix_denom(deps.storage)?)?; - - _try_delegate_to_mixnode( - deps, - env.block.height, - &mix_identity, - info.sender.as_str(), - amount, - None, - ) + mix_id: NodeId, +) -> Result { + _try_delegate_to_mixnode(deps, mix_id, info.sender, info.funds, None) } pub(crate) fn try_delegate_to_mixnode_on_behalf( deps: DepsMut<'_>, - env: Env, info: MessageInfo, - mix_identity: IdentityKey, + mix_id: NodeId, delegate: String, -) -> Result { - // check if the delegation contains any funds of the appropriate denomination - let amount = - validate_delegation_stake(info.funds, mixnet_params_storage::mix_denom(deps.storage)?)?; - - _try_delegate_to_mixnode( - deps, - env.block.height, - &mix_identity, - &delegate, - amount, - Some(info.sender), - ) -} - -pub(crate) fn try_reconcile_delegation( - storage: &mut dyn Storage, - delegation: Delegation, -) -> Result { - // update total_delegation of this node - mixnodes_storage::TOTAL_DELEGATION.update::<_, ContractError>( - storage, - &delegation.node_identity, - |total_delegation| { - // since we know that the target node exists and because the total_delegation bucket - // entry is created whenever the node itself is added, the unwrap here is fine - // as the entry MUST exist - Ok(total_delegation.unwrap() + delegation.amount.amount) - }, - )?; - - // update [or create new] pending delegation of this delegator - storage::delegations().update::<_, ContractError>( - storage, - delegation.storage_key(), - |existing_delegation| { - Ok(match existing_delegation { - Some(mut existing_delegation) => { - existing_delegation - .increment_amount(delegation.amount.amount, Some(delegation.block_height)); - existing_delegation - } - None => delegation.clone(), - }) - }, - )?; - - Ok(new_pending_delegation_event( - &delegation.owner, - &delegation.proxy, - &delegation.amount, - &delegation.node_identity, - )) +) -> Result { + let delegate = deps.api.addr_validate(&delegate)?; + _try_delegate_to_mixnode(deps, mix_id, delegate, info.funds, Some(info.sender)) } pub(crate) fn _try_delegate_to_mixnode( deps: DepsMut<'_>, - block_height: u64, - mix_identity: &str, - delegate: &str, - amount: Coin, + mix_id: NodeId, + delegate: Addr, + amount: Vec, proxy: Option, -) -> Result { - let delegate = deps.api.addr_validate(delegate)?; - - // check if the target node actually exists - if mixnodes_storage::mixnodes() - .may_load(deps.storage, mix_identity)? - .is_none() - { - return Err(ContractError::MixNodeBondNotFound { - identity: mix_identity.to_string(), - }); - } - - let delegation = Delegation::new( - delegate.to_owned(), - mix_identity.to_string(), - amount.clone(), - block_height, - proxy.clone(), - ); - - if storage::PENDING_DELEGATION_EVENTS - .may_load(deps.storage, delegation.event_storage_key())? - .is_some() - { - return Err(ContractError::DelegationEventAlreadyPending { - block_height, - identity: mix_identity.to_string(), - kind: "delgation".to_string(), - }); - } - - storage::PENDING_DELEGATION_EVENTS.save( - deps.storage, - delegation.event_storage_key(), - &DelegationEvent::Delegate(delegation), +) -> Result { + // 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, + contract_state.params.minimum_mixnode_delegation, + contract_state.rewarding_denom, )?; - Ok(Response::new().add_event(new_pending_delegation_event( - &delegate, - &proxy, - &amount, - mix_identity, - ))) + // check if the target node actually exists and is still bonded + match mixnodes_storage::mixnode_bonds().may_load(deps.storage, mix_id)? { + None => return Err(MixnetContractError::MixNodeBondNotFound { id: mix_id }), + Some(bond) if bond.is_unbonding => { + return Err(MixnetContractError::MixnodeIsUnbonding { node_id: mix_id }) + } + _ => (), + } + + // push the event onto the queue and wait for it to be picked up at the end of the epoch + let cosmos_event = new_pending_delegation_event(&delegate, &proxy, &delegation, mix_id); + + let epoch_event = PendingEpochEventData::Delegate { + owner: delegate, + mix_id, + amount: delegation, + proxy, + }; + interval_storage::push_new_epoch_event(deps.storage, &epoch_event)?; + + Ok(Response::new().add_event(cosmos_event)) } pub(crate) fn try_remove_delegation_from_mixnode( deps: DepsMut<'_>, - env: Env, info: MessageInfo, - mix_identity: IdentityKey, -) -> Result { - _try_remove_delegation_from_mixnode(deps, env, mix_identity, info.sender.as_str(), None) + mix_id: NodeId, +) -> Result { + _try_remove_delegation_from_mixnode(deps, mix_id, info.sender, None) } pub(crate) fn try_remove_delegation_from_mixnode_on_behalf( deps: DepsMut<'_>, - env: Env, info: MessageInfo, - mix_identity: IdentityKey, + mix_id: NodeId, delegate: String, -) -> Result { - _try_remove_delegation_from_mixnode(deps, env, mix_identity, &delegate, Some(info.sender)) -} - -pub struct ReconcileUndelegateResponse { - bank_msg: Option, - wasm_msg: Option, - event: Event, -} - -pub(crate) fn try_reconcile_undelegation( - storage: &mut dyn Storage, - api: &dyn Api, - pending_undelegate: &PendingUndelegate, -) -> Result { - let delegation_map = storage::delegations(); - let mix_denom = mixnet_params_storage::mix_denom(storage)?; - - // debug_with_visibility(api, "Reconciling undelegations"); - - let any_delegations = delegation_map - .prefix(pending_undelegate.storage_key()) - .keys(storage, None, None, cosmwasm_std::Order::Ascending) - .filter_map(|v| v.ok()) - .next() - .is_some(); - - if !any_delegations { - return Ok(ReconcileUndelegateResponse { - bank_msg: None, - wasm_msg: None, - event: new_error_event( - ContractError::NoMixnodeDelegationFound { - identity: pending_undelegate.mix_identity(), - address: pending_undelegate.delegate().to_string(), - } - .to_string(), - ), - }); - } - - let reward = crate::rewards::transactions::calculate_delegator_reward( - storage, - api, - pending_undelegate.proxy_storage_key(), - &pending_undelegate.mix_identity(), - )?; - - // debug_with_visibility(api, format!("Delegator reward: {}", reward)); - - // Might want to introduce paging here - let delegation_heights = delegation_map - .prefix(pending_undelegate.storage_key()) - .keys(storage, None, None, cosmwasm_std::Order::Ascending) - .filter_map(|v| v.ok()) - .collect::>(); - - if delegation_heights.is_empty() { - return Ok(ReconcileUndelegateResponse { - bank_msg: None, - wasm_msg: None, - event: new_error_event( - ContractError::NoMixnodeDelegationFound { - identity: pending_undelegate.mix_identity(), - address: pending_undelegate.delegate().to_string(), - } - .to_string(), - ), - }); - } - - let mut total_delegation = Uint128::zero(); - - // debug_with_visibility(api, "Reducing accumulated rewards"); - - { - if let Some(mut bond) = crate::mixnodes::storage::mixnodes() - .may_load(storage, &pending_undelegate.mix_identity())? - { - let remaining = bond.accumulated_rewards().saturating_sub(reward); - // debug_with_visibility(api, format!("Remaining accumulated rewards: {}", remaining)); - bond.accumulated_rewards = Some(remaining); - - crate::mixnodes::storage::mixnodes().save( - storage, - &pending_undelegate.mix_identity(), - &bond, - pending_undelegate.block_height(), - )?; - } - } - - for h in delegation_heights { - let delegation = - delegation_map.load(storage, pending_undelegate.delegation_key(h).clone())?; - total_delegation += delegation.amount.amount; - delegation_map.replace( - storage, - pending_undelegate.delegation_key(h), - None, - Some(&delegation), - )?; - } - - mixnodes_storage::TOTAL_DELEGATION.update::<_, ContractError>( - storage, - &pending_undelegate.mix_identity(), - |total_node_delegation| { - // debug_with_visibility(api, "Setting total delegation"); - let remaining = match total_node_delegation.unwrap().checked_sub(total_delegation) { - Ok(remaining) => remaining, - Err(_) => { - // debug_with_visibility( - // api, - // format!( - // "Overflowed delegation subsctraction, {} - {}", - // total_node_delegation.unwrap(), - // total_delegation - // ), - // ); - return Err(ContractError::TotalDelegationSubOverflow { - mix_identity: pending_undelegate.mix_identity(), - total_node_delegation: total_node_delegation.unwrap().u128(), - to_subtract: total_delegation.u128(), - }); - } - }; - // debug_with_visibility(api, format!("Remaining total delegation: {}", remaining)); - // the first unwrap is fine because the delegation information MUST exist, otherwise we would - // have never gotten here in the first place - // the second unwrap is also fine because we should NEVER underflow here, - // if we do, it means we have some serious error in our logic - Ok(remaining) - }, - )?; - - let total_funds = total_delegation + reward; - - // don't add a bank message if it would have resulted in attempting to send 0 tokens - let bank_msg = if total_delegation != Uint128::zero() { - Some(BankMsg::Send { - to_address: pending_undelegate - .proxy() - .as_ref() - .unwrap_or(&pending_undelegate.delegate()) - .to_string(), - amount: coins(total_funds.u128(), mix_denom.clone()), - }) - } else { - None - }; - - let mut wasm_msg = None; - - if let Some(proxy) = &pending_undelegate.proxy() { - let msg = Some(VestingContractExecuteMsg::TrackUndelegation { - owner: pending_undelegate.delegate().as_str().to_string(), - mix_identity: pending_undelegate.mix_identity(), - amount: Coin::new(total_funds.u128(), mix_denom.clone()), - }); - - wasm_msg = Some(wasm_execute(proxy, &msg, vec![one_ucoin(mix_denom)])?); - } - - let event = new_undelegation_event( - &pending_undelegate.delegate(), - &pending_undelegate.proxy(), - &pending_undelegate.mix_identity(), - total_funds, - ); - - // debug_with_visibility(api, "Done"); - - Ok(ReconcileUndelegateResponse { - bank_msg, - wasm_msg, - event, - }) +) -> Result { + let delegate = deps.api.addr_validate(&delegate)?; + _try_remove_delegation_from_mixnode(deps, mix_id, delegate, Some(info.sender)) } pub(crate) fn _try_remove_delegation_from_mixnode( deps: DepsMut<'_>, - env: Env, - mix_identity: IdentityKey, - delegate: &str, + mix_id: NodeId, + delegate: Addr, proxy: Option, -) -> Result { - let delegate = deps.api.addr_validate(delegate)?; +) -> Result { + // see if the delegation even exists + let storage_key = Delegation::generate_storage_key(mix_id, &delegate, proxy.as_ref()); - let event = PendingUndelegate::new( - mix_identity.to_string(), - delegate.clone(), - proxy.clone(), - env.block.height, - ); - - if storage::PENDING_DELEGATION_EVENTS - .may_load(deps.storage, event.event_storage_key())? - .is_some() + if storage::delegations() + .may_load(deps.storage, storage_key)? + .is_none() { - return Err(ContractError::DelegationEventAlreadyPending { - block_height: event.block_height(), - identity: mix_identity, - kind: "undelgation".to_string(), + return Err(MixnetContractError::NoMixnodeDelegationFound { + mix_id, + address: delegate.into_string(), + proxy: proxy.map(Addr::into_string), }); } - PENDING_DELEGATION_EVENTS.save( - deps.storage, - event.event_storage_key(), - &DelegationEvent::Undelegate(event), - )?; + // 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); - Ok(Response::new().add_event(new_pending_undelegation_event( - &delegate, - &proxy, - &mix_identity, - ))) + let epoch_event = PendingEpochEventData::Undelegate { + owner: delegate, + mix_id, + proxy, + }; + interval_storage::push_new_epoch_event(deps.storage, &epoch_event)?; + + Ok(Response::new().add_event(cosmos_event)) } #[cfg(test)] mod tests { - use cosmwasm_std::coins; - - use crate::support::tests; - use crate::support::tests::fixtures::TEST_COIN_DENOM; - use crate::support::tests::test_helpers; - use super::*; #[cfg(test)] - mod delegation_stake_validation { - use cosmwasm_std::coin; - - use super::*; - - #[test] - fn stake_cant_be_empty() { - assert_eq!( - Err(ContractError::EmptyDelegation), - validate_delegation_stake(vec![], TEST_COIN_DENOM.to_string()) - ) - } - - #[test] - fn stake_must_have_single_coin_type() { - assert_eq!( - Err(ContractError::MultipleDenoms), - validate_delegation_stake( - vec![ - coin(123, TEST_COIN_DENOM), - coin(123, "BTC"), - coin(123, "DOGE") - ], - TEST_COIN_DENOM.to_string() - ) - ) - } - - #[test] - fn stake_coin_must_be_of_correct_type() { - assert_eq!( - Err(ContractError::WrongDenom { - mix_denom: TEST_COIN_DENOM.to_string() - }), - validate_delegation_stake(coins(123, "DOGE"), TEST_COIN_DENOM.to_string()) - ) - } - - #[test] - fn stake_coin_must_have_value_greater_than_zero() { - assert_eq!( - Err(ContractError::EmptyDelegation), - validate_delegation_stake(coins(0, TEST_COIN_DENOM), TEST_COIN_DENOM.to_string()) - ) - } - - #[test] - fn stake_can_have_any_positive_value() { - // this might change in the future, but right now an arbitrary (positive) value can be delegated - assert!(validate_delegation_stake( - coins(1, TEST_COIN_DENOM), - TEST_COIN_DENOM.to_string() - ) - .is_ok()); - assert!(validate_delegation_stake( - coins(123, TEST_COIN_DENOM), - TEST_COIN_DENOM.to_string() - ) - .is_ok()); - assert!(validate_delegation_stake( - coins(10000000000, TEST_COIN_DENOM), - TEST_COIN_DENOM.to_string() - ) - .is_ok()); - } - } - - #[cfg(test)] - mod mix_stake_delegation { + mod delegating_to_mixnode { use super::*; use crate::mixnodes::transactions::try_remove_mixnode; - use cosmwasm_std::coin; - use cosmwasm_std::testing::mock_env; + use crate::rewards::storage as rewards_storage; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::testing::mock_info; - use cosmwasm_std::Addr; + use cosmwasm_std::{coin, Decimal}; #[test] - fn fails_if_node_doesnt_exist() { - let mut deps = test_helpers::init_contract(); + fn can_only_be_done_towards_an_existing_mixnode() { + let mut test = TestSetup::new(); + let owner = "delegator"; + let sender = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); + + let res = try_delegate_to_mixnode(test.deps_mut(), sender, 42); assert_eq!( - Err(ContractError::MixNodeBondNotFound { - identity: "non-existent-mix-identity".into() - }), - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info("sender", &coins(123, TEST_COIN_DENOM)), - "non-existent-mix-identity".into(), - ) - ); - } - - #[test] - fn succeeds_for_existing_node() { - let mut deps = test_helpers::init_contract(); - let mixnode_owner = "bob"; - let identity = test_helpers::add_mixnode( - mixnode_owner, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - let delegation_owner = Addr::unchecked("sender"); - let delegation = coin(123, TEST_COIN_DENOM); - assert!(try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info(delegation_owner.as_str(), &[delegation.clone()]), - identity.clone(), - ) - .is_ok()); - - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); - - let expected = Delegation::new( - delegation_owner.clone(), - identity.clone(), - delegation.clone(), - mock_env().block.height, - None, - ); - - assert_eq!( - expected, - test_helpers::read_delegation( - &deps.storage, - &identity, - delegation_owner.as_bytes(), - mock_env().block.height - ) - .unwrap() - ); - - // node's "total_delegation" is increased - assert_eq!( - delegation.amount, - mixnodes_storage::TOTAL_DELEGATION - .load(&deps.storage, &identity) - .unwrap() + res, + Err(MixnetContractError::MixNodeBondNotFound { id: 42 }) ) } #[test] - fn fails_if_node_unbonded() { - let mut deps = test_helpers::init_contract(); - let mixnode_owner = "bob"; - let identity = test_helpers::add_mixnode( - mixnode_owner, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), + fn must_contain_non_zero_amount_of_coins() { + let mut test = TestSetup::new(); + let owner = "delegator"; + let mix_id = test.add_dummy_mixnode("mix-owner", None); + let sender1 = mock_info(owner, &[coin(0, TEST_COIN_DENOM)]); + let sender2 = mock_info(owner, &[]); + let sender3 = mock_info(owner, &[coin(1000, "some-weird-coin")]); + + let res = try_delegate_to_mixnode(test.deps_mut(), sender1, mix_id); + assert_eq!(res, Err(MixnetContractError::EmptyDelegation)); + let res = try_delegate_to_mixnode(test.deps_mut(), sender2, mix_id); + assert_eq!(res, Err(MixnetContractError::EmptyDelegation)); + let res = try_delegate_to_mixnode(test.deps_mut(), sender3, mix_id); + assert_eq!( + res, + Err(MixnetContractError::WrongDenom { + received: "some-weird-coin".to_string(), + expected: TEST_COIN_DENOM.to_string() + }) ); - let delegation_owner = Addr::unchecked("sender"); - let api = deps.api.clone(); + } + + #[test] + fn if_applicable_must_contain_at_least_the_minimum_pledge() { + let mut test = TestSetup::new(); + let owner = "delegator"; + let mix_id = test.add_dummy_mixnode("mix-owner", None); + let sender1 = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); + let sender2 = mock_info(owner, &[coin(150_000_000, TEST_COIN_DENOM)]); + + let min_delegation = coin(150_000_000, TEST_COIN_DENOM); + let mut contract_state = mixnet_params_storage::CONTRACT_STATE + .load(test.deps().storage) + .unwrap(); + contract_state.params.minimum_mixnode_delegation = Some(min_delegation.clone()); + mixnet_params_storage::CONTRACT_STATE + .save(test.deps_mut().storage, &contract_state) + .unwrap(); + + let res = try_delegate_to_mixnode(test.deps_mut(), sender1, mix_id); + assert_eq!( + res, + Err(MixnetContractError::InsufficientDelegation { + received: coin(100_000_000, TEST_COIN_DENOM), + minimum: min_delegation + }) + ); + + let res = try_delegate_to_mixnode(test.deps_mut(), sender2, mix_id); + assert!(res.is_ok()) + } + + #[test] + fn can_only_be_done_towards_fully_bonded_mixnode() { + let mut test = TestSetup::new(); + let owner = "delegator"; + let sender = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); + + let mix_id_unbonding = test.add_dummy_mixnode("mix-owner-unbonding", None); + let mix_id_unbonded = test.add_dummy_mixnode("mix-owner-unbonded", None); + let mix_id_unbonded_leftover = + test.add_dummy_mixnode("mix-owner-unbonded-leftover", None); + + // manually adjust delegation info as to indicate the rewarding information shouldnt get removed + let mut rewarding_details = rewards_storage::MIXNODE_REWARDING + .load(test.deps().storage, mix_id_unbonded_leftover) + .unwrap(); + rewarding_details.delegates = Decimal::raw(12345); + rewarding_details.unique_delegations = 1; + rewards_storage::MIXNODE_REWARDING + .save( + test.deps_mut().storage, + mix_id_unbonded_leftover, + &rewarding_details, + ) + .unwrap(); + + try_remove_mixnode(test.deps_mut(), mock_info("mix-owner-unbonded", &[])).unwrap(); try_remove_mixnode( - &mock_env(), - deps.as_mut().storage, - &api, - mock_info(mixnode_owner, &[]), + test.deps_mut(), + mock_info("mix-owner-unbonded-leftover", &[]), ) .unwrap(); + + test.execute_all_pending_events(); + try_remove_mixnode(test.deps_mut(), mock_info("mix-owner-unbonding", &[])).unwrap(); + + let res = try_delegate_to_mixnode(test.deps_mut(), sender.clone(), mix_id_unbonding); assert_eq!( - Err(ContractError::MixNodeBondNotFound { - identity: identity.clone() - }), - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info(delegation_owner.as_str(), &coins(123, TEST_COIN_DENOM)), - identity, - ) + res, + Err(MixnetContractError::MixnodeIsUnbonding { + node_id: mix_id_unbonding + }) + ); + + let res = try_delegate_to_mixnode(test.deps_mut(), sender.clone(), mix_id_unbonded); + assert_eq!( + res, + Err(MixnetContractError::MixNodeBondNotFound { + id: mix_id_unbonded + }) + ); + + let res = try_delegate_to_mixnode(test.deps_mut(), sender, mix_id_unbonded_leftover); + assert_eq!( + res, + Err(MixnetContractError::MixNodeBondNotFound { + id: mix_id_unbonded_leftover + }) ); } #[test] - fn succeeds_if_node_rebonded() { - let mut deps = test_helpers::init_contract(); - let mixnode_owner = "bob"; - test_helpers::add_mixnode( - mixnode_owner, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - let api = deps.api.clone(); - try_remove_mixnode( - &mock_env(), - deps.as_mut().storage, - &api, - mock_info(mixnode_owner, &[]), - ) - .unwrap(); - let identity = test_helpers::add_mixnode( - mixnode_owner, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - let delegation = coin(123, TEST_COIN_DENOM); - let delegation_owner = Addr::unchecked("sender"); - assert!(try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info(delegation_owner.as_str(), &[delegation.clone()]), - identity.clone(), - ) - .is_ok()); + fn can_still_be_done_if_prior_delegation_exists() { + let mut test = TestSetup::new(); + let owner = "delegator"; + let mix_id = test.add_dummy_mixnode("mix-owner", None); + let sender1 = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); + let sender2 = mock_info(owner, &[coin(50_000_000, TEST_COIN_DENOM)]); - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); + let res = try_delegate_to_mixnode(test.deps_mut(), sender1, mix_id); + assert!(res.is_ok()); - let expected = Delegation::new( - delegation_owner.clone(), - identity.clone(), - delegation.clone(), - mock_env().block.height, - None, - ); - - assert_eq!( - expected, - test_helpers::read_delegation( - &deps.storage, - &identity, - delegation_owner.as_bytes(), - mock_env().block.height - ) - .unwrap() - ); - - // node's "total_delegation" is increased - assert_eq!( - delegation.amount, - mixnodes_storage::TOTAL_DELEGATION - .load(&deps.storage, &identity) - .unwrap() - ) + // still OK + let res = try_delegate_to_mixnode(test.deps_mut(), sender2, mix_id); + assert!(res.is_ok()) } #[test] - fn is_possible_for_an_already_delegated_node() { - let mut deps = test_helpers::init_contract(); - let mixnode_owner = "bob"; - let identity = test_helpers::add_mixnode( - mixnode_owner, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - let delegation_owner = Addr::unchecked("sender"); - let delegation1 = coin(100, TEST_COIN_DENOM); - let delegation2 = coin(50, TEST_COIN_DENOM); + fn correctly_pushes_appropriate_epoch_event() { + let mut test = TestSetup::new(); + let owner = "delegator"; + let mix_id = test.add_dummy_mixnode("mix-owner", None); - let mut env = mock_env(); + let amount1 = coin(100_000_000, TEST_COIN_DENOM); + let amount2 = coin(50_000_000, TEST_COIN_DENOM); - try_delegate_to_mixnode( - deps.as_mut(), - env.clone(), - mock_info(delegation_owner.as_str(), &[delegation1.clone()]), - identity.clone(), - ) - .unwrap(); + let sender1 = mock_info(owner, &[amount1.clone()]); + let sender2 = mock_info(test.vesting_contract().as_str(), &[amount2.clone()]); - env.block.height += 1; + try_delegate_to_mixnode(test.deps_mut(), sender1, mix_id).unwrap(); + try_delegate_to_mixnode_on_behalf(test.deps_mut(), sender2, mix_id, owner.into()) + .unwrap(); - try_delegate_to_mixnode( - deps.as_mut(), - env, - mock_info(delegation_owner.as_str(), &[delegation2.clone()]), - identity.clone(), - ) - .unwrap(); - - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); - - // let expected = Delegation::new( - // delegation_owner.clone(), - // identity.clone(), - // coin(delegation1.amount.u128() + delegation2.amount.u128(), TEST_COIN_DENOM), - // mock_env().block.height, - // None, - // ); - - // assert_eq!( - // expected, - // test_helpers::read_delegation( - // &deps.storage, - // &identity, - // delegation_owner.as_bytes(), - // mock_env().block.height - // ) - // .unwrap() - // ); - - // node's "total_delegation" is sum of both - assert_eq!( - delegation1.amount + delegation2.amount, - mixnodes_storage::TOTAL_DELEGATION - .load(&deps.storage, &identity) - .unwrap() - ) - } - - #[test] - fn block_height_is_updated_on_new_delegation() { - let mut deps = test_helpers::init_contract(); - let mixnode_owner = "bob"; - let identity = test_helpers::add_mixnode( - mixnode_owner, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - let delegation_owner = Addr::unchecked("sender"); - let delegation = coin(100, TEST_COIN_DENOM); - let env1 = mock_env(); - let mut env2 = mock_env(); - let initial_height = env1.block.height; - let updated_height = initial_height + 42; - // second env has grown in block height - env2.block.height = updated_height; - try_delegate_to_mixnode( - deps.as_mut(), - env1.clone(), - mock_info(delegation_owner.as_str(), &[delegation.clone()]), - identity.clone(), - ) - .unwrap(); - - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); + let events = test.pending_epoch_events(); assert_eq!( - initial_height, - test_helpers::read_delegation( - &deps.storage, - &identity, - delegation_owner.as_bytes(), - env1.block.height - ) - .unwrap() - .block_height - ); - try_delegate_to_mixnode( - deps.as_mut(), - env2, - mock_info(delegation_owner.as_str(), &[delegation.clone()]), - identity.clone(), - ) - .unwrap(); - - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); - - let delegations = crate::delegations::queries::query_mixnode_delegation( - &deps.storage, - &deps.api, - identity, - delegation_owner.to_string(), - None, - ) - .unwrap(); - - let total_delegation = delegations - .iter() - .fold(Uint128::zero(), |acc, d| acc + d.amount.amount); - - assert_eq!(delegation.amount + delegation.amount, total_delegation); - } - - #[test] - fn block_height_is_not_updated_on_different_delegator() { - let mut deps = test_helpers::init_contract(); - let mixnode_owner = "bob"; - let identity = test_helpers::add_mixnode( - mixnode_owner, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - let delegation_owner1 = Addr::unchecked("sender1"); - let delegation_owner2 = Addr::unchecked("sender2"); - let delegation1 = coin(100, TEST_COIN_DENOM); - let delegation2 = coin(120, TEST_COIN_DENOM); - let env1 = mock_env(); - let mut env2 = mock_env(); - let initial_height = env1.block.height; - let second_height = initial_height + 42; - // second env has grown in block height - env2.block.height = second_height; - try_delegate_to_mixnode( - deps.as_mut(), - env1.clone(), - mock_info(delegation_owner1.as_str(), &[delegation1]), - identity.clone(), - ) - .unwrap(); - - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); - - assert_eq!( - initial_height, - test_helpers::read_delegation( - &deps.storage, - &identity, - delegation_owner1.as_bytes(), - env1.block.height - ) - .unwrap() - .block_height - ); - try_delegate_to_mixnode( - deps.as_mut(), - env2.clone(), - mock_info(delegation_owner2.as_str(), &[delegation2]), - identity.clone(), - ) - .unwrap(); - - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); - - assert_eq!( - initial_height, - test_helpers::read_delegation( - &deps.storage, - &identity, - delegation_owner1.as_bytes(), - env1.block.height - ) - .unwrap() - .block_height - ); - assert_eq!( - second_height, - test_helpers::read_delegation( - &deps.storage, - identity, - delegation_owner2.as_bytes(), - env2.block.height - ) - .unwrap() - .block_height - ); - } - - #[test] - fn is_disallowed_for_already_delegated_node_if_it_unbonded() { - let mut deps = test_helpers::init_contract(); - let mixnode_owner = "bob"; - let identity = test_helpers::add_mixnode( - mixnode_owner, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - let delegation_owner = Addr::unchecked("sender"); - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info(delegation_owner.as_str(), &coins(100, TEST_COIN_DENOM)), - identity.clone(), - ) - .unwrap(); - let api = deps.api.clone(); - try_remove_mixnode( - &mock_env(), - deps.as_mut().storage, - &api, - mock_info(mixnode_owner, &[]), - ) - .unwrap(); - assert_eq!( - Err(ContractError::MixNodeBondNotFound { - identity: identity.clone() - }), - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info(delegation_owner.as_str(), &coins(50, TEST_COIN_DENOM)), - identity, - ) - ); - } - - #[test] - fn is_allowed_for_multiple_nodes() { - let mut deps = test_helpers::init_contract(); - let mixnode_owner1 = "bob"; - let mixnode_owner2 = "fred"; - let identity1 = test_helpers::add_mixnode( - mixnode_owner1, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - let identity2 = test_helpers::add_mixnode( - mixnode_owner2, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - let delegation_owner = Addr::unchecked("sender"); - assert!(try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info(delegation_owner.as_str(), &coins(123, TEST_COIN_DENOM)), - identity1.clone(), - ) - .is_ok()); - assert!(try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info(delegation_owner.as_str(), &coins(42, TEST_COIN_DENOM)), - identity2.clone(), - ) - .is_ok()); - - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); - - let expected1 = Delegation::new( - delegation_owner.clone(), - identity1.clone(), - coin(123, TEST_COIN_DENOM), - mock_env().block.height, - None, - ); - - let expected2 = Delegation::new( - delegation_owner.clone(), - identity2.clone(), - coin(42, TEST_COIN_DENOM), - mock_env().block.height, - None, + events[0], + PendingEpochEventData::Delegate { + owner: Addr::unchecked(owner), + mix_id, + amount: amount1, + proxy: None + } ); assert_eq!( - expected1, - test_helpers::read_delegation( - &deps.storage, - identity1, - delegation_owner.as_bytes(), - mock_env().block.height - ) - .unwrap() + events[1], + PendingEpochEventData::Delegate { + owner: Addr::unchecked(owner), + mix_id, + amount: amount2, + proxy: Some(test.vesting_contract()) + } ); - assert_eq!( - expected2, - test_helpers::read_delegation( - &deps.storage, - identity2, - delegation_owner.as_bytes(), - mock_env().block.height - ) - .unwrap() - ); - } - - #[test] - fn is_allowed_by_multiple_users() { - let mut deps = test_helpers::init_contract(); - let mixnode_owner = "bob"; - let identity = test_helpers::add_mixnode( - mixnode_owner, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - let delegation1 = coin(123, TEST_COIN_DENOM); - let delegation2 = coin(234, TEST_COIN_DENOM); - assert!(try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info("sender1", &[delegation1.clone()]), - identity.clone(), - ) - .is_ok()); - assert!(try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info("sender2", &[delegation2.clone()]), - identity.clone(), - ) - .is_ok()); - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); - - // node's "total_delegation" is sum of both - assert_eq!( - delegation1.amount + delegation2.amount, - mixnodes_storage::TOTAL_DELEGATION - .load(&deps.storage, &identity) - .unwrap() - ) - } - - #[test] - fn delegation_is_not_removed_if_node_unbonded() { - let mut deps = test_helpers::init_contract(); - let mixnode_owner = "bob"; - let identity = test_helpers::add_mixnode( - mixnode_owner, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - let delegation_owner = Addr::unchecked("sender"); - let delegation_amount = coin(100, TEST_COIN_DENOM); - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info(delegation_owner.as_str(), &[delegation_amount.clone()]), - identity.clone(), - ) - .unwrap(); - - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); - let api = deps.api.clone(); - try_remove_mixnode( - &mock_env(), - deps.as_mut().storage, - &api, - mock_info(mixnode_owner, &[]), - ) - .unwrap(); - - let expected = Delegation::new( - delegation_owner.clone(), - identity.clone(), - delegation_amount, - mock_env().block.height, - None, - ); - - assert_eq!( - expected, - test_helpers::read_delegation( - &deps.storage, - identity, - delegation_owner.as_bytes(), - mock_env().block.height - ) - .unwrap() - ) } } #[cfg(test)] - mod removing_mix_stake_delegation { + mod removing_mixnode_delegation { use super::*; - use crate::support::tests; + use crate::mixnodes::transactions::try_remove_mixnode; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::coin; - use cosmwasm_std::testing::mock_env; use cosmwasm_std::testing::mock_info; - use cosmwasm_std::Addr; #[test] - fn total_delegation_is_preserved_if_only_some_undelegate() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let mixnode_owner = "bob"; - let identity = test_helpers::add_mixnode( - mixnode_owner, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - let delegation_owner1 = Addr::unchecked("sender1"); - let delegation_owner2 = Addr::unchecked("sender2"); - let delegation1 = coin(123, TEST_COIN_DENOM); - let delegation2 = coin(234, TEST_COIN_DENOM); - assert!(try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info(delegation_owner1.as_str(), &[delegation1]), - identity.clone(), + fn cannot_be_performed_if_delegation_never_existed() { + let mut test = TestSetup::new(); + let owner = "delegator"; + let sender = mock_info(owner, &[]); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let res = try_remove_delegation_from_mixnode(test.deps_mut(), sender, mix_id); + assert_eq!( + res, + Err(MixnetContractError::NoMixnodeDelegationFound { + mix_id, + address: owner.to_string(), + proxy: None + }) ) - .is_ok()); + } - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); + #[test] + fn cannot_be_performed_if_the_delegation_is_still_pending() { + let mut test = TestSetup::new(); + let owner = "delegator"; + let mix_id = test.add_dummy_mixnode("mix-owner", None); + let sender1 = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); + let sender2 = mock_info(owner, &[]); - assert!(try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info(delegation_owner2.as_str(), &[delegation2.clone()]), - identity.clone(), + try_delegate_to_mixnode(test.deps_mut(), sender1, mix_id).unwrap(); + + let res = try_remove_delegation_from_mixnode(test.deps_mut(), sender2, mix_id); + assert_eq!( + res, + Err(MixnetContractError::NoMixnodeDelegationFound { + mix_id, + address: owner.to_string(), + proxy: None + }) ) - .is_ok()); + } - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); + #[test] + fn as_long_as_delegation_exists_can_always_be_performed() { + let mut test = TestSetup::new(); + let owner = "delegator"; + let sender = mock_info(owner, &[]); - // sender1 undelegates - try_remove_delegation_from_mixnode( - deps.as_mut(), - env, - mock_info(delegation_owner1.as_str(), &[]), - identity.clone(), + let normal_mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id_unbonding = test.add_dummy_mixnode("mix-owner-unbonding", None); + let mix_id_unbonded_leftover = + test.add_dummy_mixnode("mix-owner-unbonded-leftover", None); + + test.add_immediate_delegation(owner, 10000u32, normal_mix_id); + test.add_immediate_delegation(owner, 10000u32, mix_id_unbonding); + test.add_immediate_delegation(owner, 10000u32, mix_id_unbonded_leftover); + + try_remove_mixnode( + test.deps_mut(), + mock_info("mix-owner-unbonded-leftover", &[]), ) .unwrap(); - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); - // but total delegation should still equal to what sender2 sent - // node's "total_delegation" is sum of both - assert_eq!( - delegation2.amount, - mixnodes_storage::TOTAL_DELEGATION - .load(&deps.storage, &identity) - .unwrap() - ) + test.execute_all_pending_events(); + try_remove_mixnode(test.deps_mut(), mock_info("mix-owner-unbonding", &[])).unwrap(); + + let res = + try_remove_delegation_from_mixnode(test.deps_mut(), sender.clone(), normal_mix_id); + assert!(res.is_ok()); + + let res = try_remove_delegation_from_mixnode( + test.deps_mut(), + sender.clone(), + mix_id_unbonding, + ); + assert!(res.is_ok()); + + let res = try_remove_delegation_from_mixnode( + test.deps_mut(), + sender, + mix_id_unbonded_leftover, + ); + assert!(res.is_ok()); } } - - // #[cfg(test)] - // mod multi_delegations { - // use super::*; - // use crate::delegations::helpers; - // use crate::delegations::queries::tests::store_n_mix_delegations; - // use crate::support::tests::test_helpers; - // use mixnet_contract::IdentityKey; - // use mixnet_contract::RawDelegationData; - // - // #[test] - // fn multiple_page_delegations() { - // let mut deps = test_helpers::init_contract(); - // let node_identity: IdentityKey = "foo".into(); - // store_n_mix_delegations( - // storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10, - // &mut deps.storage, - // &node_identity, - // ); - // let mix_bucket = storage::all_mix_delegations_read::(&deps.storage); - // let mix_delegations = helpers::Delegations::new(mix_bucket); - // assert_eq!( - // storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10, - // mix_delegations.count() as u32 - // ); - // } - // } } diff --git a/contracts/mixnet/src/error.rs b/contracts/mixnet/src/error.rs deleted file mode 100644 index e1d3a22335..0000000000 --- a/contracts/mixnet/src/error.rs +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use cosmwasm_std::{Addr, StdError}; -use mixnet_contract_common::{error::MixnetContractError, IdentityKey}; -use thiserror::Error; - -/// Custom errors for contract failure conditions. -/// -/// Add any other custom errors you like here. -/// Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details. -#[derive(Error, Debug, PartialEq)] -pub enum ContractError { - #[error("MIXNET ({}): {0}", line!())] - Std(#[from] StdError), - - #[error("MIXNET ({}): Not enough funds sent for mixnode bond. (received {received}, minimum {minimum})", line!())] - InsufficientMixNodeBond { received: u128, minimum: u128 }, - - #[error("MIXNET ({}): Mixnode ({identity}) does not exist", line!())] - MixNodeBondNotFound { identity: IdentityKey }, - - #[error("MIXNET ({}): Not enough funds sent for gateway bond. (received {received}, minimum {minimum})", line!())] - InsufficientGatewayBond { received: u128, minimum: u128 }, - - #[error("MIXNET ({}): {owner} does not seem to own any mixnodes", line!())] - NoAssociatedMixNodeBond { owner: Addr }, - - #[error("MIXNET ({}): {owner} does not seem to own any gateways", line!())] - NoAssociatedGatewayBond { owner: Addr }, - - #[error("MIXNET ({}): Unauthorized", line!())] - Unauthorized, - - #[error("MIXNET ({}): Wrong coin denomination, you must send {mix_denom}", line!())] - WrongDenom { mix_denom: String }, - - #[error("MIXNET ({}): Received multiple coin types during staking", line!())] - MultipleDenoms, - - #[error("MIXNET ({}): No coin was sent for the bonding, you must send {mix_denom}", line!())] - NoBondFound { mix_denom: String }, - - #[error("MIXNET ({}): Provided active set size is bigger than the rewarded set", line!())] - InvalidActiveSetSize, - - #[error("MIXNET ({}): Provided active set size is zero", line!())] - ZeroActiveSet, - - #[error("MIXNET ({}): Provided rewarded set size is zero", line!())] - ZeroRewardedSet, - - #[error("MIXNET ({}): This address has already bonded a mixnode", line!())] - AlreadyOwnsMixnode, - - #[error("MIXNET ({}): This address has already bonded a gateway", line!())] - AlreadyOwnsGateway, - - #[error("MIXNET ({}): Mixnode with this identity already exists. Its owner is {owner}", line!())] - DuplicateMixnode { owner: Addr }, - - #[error("MIXNET ({}): Gateway with this identity already exists. Its owner is {owner}", line!())] - DuplicateGateway { owner: Addr }, - - #[error("MIXNET ({}): No funds were provided for the delegation", line!())] - EmptyDelegation, - - #[error("MIXNET ({}): Could not find any delegation information associated with mixnode {identity} for {address}", line!())] - NoMixnodeDelegationFound { - identity: IdentityKey, - address: String, - }, - - #[error("MIXNET ({}): We tried to remove more funds then are available in the Reward pool. Wanted to remove {to_remove}, but have only {reward_pool}", line!())] - OutOfFunds { to_remove: u128, reward_pool: u128 }, - - #[error("MIXNET ({}): Received invalid interval id. Expected {expected}, received {received}", line!())] - InvalidIntervalId { received: u32, expected: u32 }, - - #[error("MIXNET ({}): Mixnode {identity} has already been rewarded during the current rewarding interval", line!())] - MixnodeAlreadyRewarded { identity: IdentityKey }, - - #[error("MIXNET ({}): Some of mixnodes {identity} delegators are still pending reward", line!())] - DelegatorsPendingReward { identity: IdentityKey }, - - #[error("MIXNET ({}): Mixnode's {identity} operator has not been rewarded yet - cannot perform delegator rewarding until that happens", line!())] - MixnodeOperatorNotRewarded { identity: IdentityKey }, - - #[error("MIXNET ({}): Proxy address mismatch, expected {existing}, got {incoming}", line!())] - ProxyMismatch { existing: String, incoming: String }, - - #[error("MIXNET ({}): Failed to recover ed25519 public key from its base58 representation - {0}", line!())] - MalformedEd25519IdentityKey(String), - - #[error("MIXNET ({}): Failed to recover ed25519 signature from its base58 representation - {0}", line!())] - MalformedEd25519Signature(String), - - #[error("MIXNET ({}): Provided ed25519 signature did not verify correctly", line!())] - InvalidEd25519Signature, - - #[error("MIXNET ({}): Profit margin percent needs to be an integer in range [0, 100], received {0}", line!())] - InvalidProfitMarginPercent(u8), - - #[error("MIXNET ({}): Rewarded set height not set, was rewarding set determined?", line!())] - RewardSetHeightMapEmpty, - - #[error("MIXNET ({}): Received unexpected value for the active set. Got: {received}, expected: {expected}", line!())] - UnexpectedActiveSetSize { received: u32, expected: u32 }, - - #[error("MIXNET ({}): Received unexpected value for the rewarded set. Got: {received}, expected at most: {expected}", line!())] - UnexpectedRewardedSetSize { received: u32, expected: u32 }, - - #[error("MIXNET ({}): There hasn't been sufficient delay since last rewarded set update. It was last updated at height {last_update}. The delay is {minimum_delay}. The current block height is {current_height}", line!())] - TooFrequentRewardedSetUpdate { - last_update: u64, - minimum_delay: u64, - current_height: u64, - }, - - #[error("MIXNET ({}): Can't change to the desired interval as it's not in progress yet. It starts at {interval_start} and finishes at {interval_end}, while the current block time is {current_block_time}", line!())] - IntervalNotInProgress { - current_block_time: u64, - interval_start: i64, - interval_end: i64, - }, - - #[error("MIXNET ({}): Can't change to the desired interval as it's not in progress yet. It starts at {epoch_start} and finishes at {epoch_end}, while the current block time is {current_block_time}", line!())] - EpochNotInProgress { - current_block_time: u64, - epoch_start: i64, - epoch_end: i64, - }, - - #[error("MIXNET ({}): Can't change to the desired interval as it hasn't started yet. It starts at {epoch_start} and finishes at {epoch_end}, while the current block time is {current_block_time}", line!())] - EpochNotStarted { - current_block_time: u64, - epoch_start: i64, - epoch_end: i64, - }, - - #[error("MIXNET ({}): Can't change to the desired interval as it's in progress. It starts at {epoch_start} and finishes at {epoch_end}, while the current block time is {current_block_time}", line!())] - EpochInProgress { - current_block_time: u64, - epoch_start: i64, - epoch_end: i64, - }, - - #[error("Could not cast reward to a u128, this should be impossible, at {}", line!())] - CastError, - #[error("{source}")] - MixnetCommonError { - #[from] - source: MixnetContractError, - }, - #[error("No rewards to claim for mixnode {identity} for {address}")] - NoRewardsToClaim { identity: String, address: String }, - - #[error("Epoch not initialized yet!")] - EpochNotInitialized, - - #[error("Invalid address: {0}")] - InvalidAddress(String), - - #[error("Pending {kind} event already exists at block {block_height} for mixnode {identity}")] - DelegationEventAlreadyPending { - block_height: u64, - identity: String, - kind: String, // delegation | undelegation - }, - #[error("Attempted to subsctract more then the total delegation, this MUST never happen! mix: {mix_identity}, total_node_delegation {total_node_delegation}, to_subtract {to_subtract}")] - TotalDelegationSubOverflow { - mix_identity: String, - total_node_delegation: u128, - to_subtract: u128, - }, - #[error("Profit margin can be updated only once during a rolling 30 day interval, last update was at {last_update_time} and current block time is {current_block_time}")] - UpdatePMTooSoon { - last_update_time: u64, - current_block_time: u64, - }, - #[error("`mix_identity` is required when `delegator` is set")] - MissingMixIdentity, - #[error("Compounding has been disabled temporarily")] - CompoundDisabled, - - #[error("Mixnode {identity} has been blacklisted on the network")] - MixnodeBlacklisted { identity: String }, -} diff --git a/contracts/mixnet/src/gateways/queries.rs b/contracts/mixnet/src/gateways/queries.rs index 87237f39ec..cc58b58944 100644 --- a/contracts/mixnet/src/gateways/queries.rs +++ b/contracts/mixnet/src/gateways/queries.rs @@ -1,8 +1,8 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use super::storage; -use crate::mixnodes::storage::{BOND_PAGE_DEFAULT_LIMIT, BOND_PAGE_MAX_LIMIT}; // Keeps gateway and mixnode retrieval in sync by re-using the constant. Could be split into its own constant. +use crate::constants::{GATEWAY_BOND_DEFAULT_RETRIEVAL_LIMIT, GATEWAY_BOND_MAX_RETRIEVAL_LIMIT}; // Keeps gateway and mixnode retrieval in sync by re-using the constant. Could be split into its own constant. use cosmwasm_std::{Deps, Order, StdResult}; use cw_storage_plus::Bound; use mixnet_contract_common::{ @@ -15,8 +15,8 @@ pub(crate) fn query_gateways_paged( limit: Option, ) -> StdResult { let limit = limit - .unwrap_or(BOND_PAGE_DEFAULT_LIMIT) - .min(BOND_PAGE_MAX_LIMIT) as usize; + .unwrap_or(GATEWAY_BOND_DEFAULT_RETRIEVAL_LIMIT) + .min(GATEWAY_BOND_MAX_RETRIEVAL_LIMIT) as usize; let start = start_after.as_deref().map(Bound::exclusive); @@ -31,7 +31,7 @@ pub(crate) fn query_gateways_paged( Ok(PagedGatewayResponse::new(nodes, limit, start_next_after)) } -pub(crate) fn query_owns_gateway( +pub(crate) fn query_owned_gateway( deps: Deps<'_>, address: String, ) -> StdResult { @@ -74,11 +74,11 @@ pub(crate) mod tests { #[test] fn gateways_paged_retrieval_obeys_limits() { let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + let limit = 2; - for n in 0..1000 { - let key = format!("bond{}", n); - test_helpers::add_gateway(&key, tests::fixtures::good_gateway_pledge(), deps.as_mut()); - } + test_helpers::add_dummy_gateways(&mut rng, deps.as_mut(), env, 1000); let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(limit)).unwrap(); assert_eq!(limit, page1.nodes.len() as u32); @@ -87,50 +87,55 @@ pub(crate) mod tests { #[test] fn gateways_paged_retrieval_has_default_limit() { let mut deps = test_helpers::init_contract(); - for n in 0..1000 { - let key = format!("bond{}", n); - test_helpers::add_gateway(&key, tests::fixtures::good_gateway_pledge(), deps.as_mut()); - } + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + test_helpers::add_dummy_gateways(&mut rng, deps.as_mut(), env, 1000); // query without explicitly setting a limit let page1 = query_gateways_paged(deps.as_ref(), None, None).unwrap(); - assert_eq!(BOND_PAGE_DEFAULT_LIMIT, page1.nodes.len() as u32); + assert_eq!( + GATEWAY_BOND_DEFAULT_RETRIEVAL_LIMIT, + page1.nodes.len() as u32 + ); } #[test] fn gateways_paged_retrieval_has_max_limit() { let mut deps = test_helpers::init_contract(); - for n in 0..1000 { - let key = format!("bond{}", n); - test_helpers::add_gateway(&key, tests::fixtures::good_gateway_pledge(), deps.as_mut()); - } + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + test_helpers::add_dummy_gateways(&mut rng, deps.as_mut(), env, 1000); // query with a crazily high limit in an attempt to use too many resources - let crazy_limit = 1000 * BOND_PAGE_DEFAULT_LIMIT; + let crazy_limit = 1000 * GATEWAY_BOND_DEFAULT_RETRIEVAL_LIMIT; let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(crazy_limit)).unwrap(); // we default to a decent sized upper bound instead - let expected_limit = BOND_PAGE_MAX_LIMIT; + let expected_limit = GATEWAY_BOND_MAX_RETRIEVAL_LIMIT; assert_eq!(expected_limit, page1.nodes.len() as u32); } #[test] fn gateway_pagination_works() { + let mut deps = test_helpers::init_contract(); + let _env = mock_env(); + let mut rng = test_helpers::test_rng(); + // prepare 4 messages and identities that are sorted by the generated identities // (because we query them in an ascended manner) let mut exec_data = (0..4) .map(|i| { let sender = format!("nym-addr{}", i); - let (msg, identity) = tests::messages::valid_bond_gateway_msg(&sender); + let (msg, identity) = tests::messages::valid_bond_gateway_msg(&mut rng, &sender); (msg, (sender, identity)) }) .collect::>(); exec_data.sort_by(|(_, (_, id1)), (_, (_, id2))| id1.cmp(id2)); let (messages, sender_identities): (Vec<_>, Vec<_>) = exec_data.into_iter().unzip(); - let mut deps = test_helpers::init_contract(); - let info = mock_info( &sender_identities[0].0.clone(), &tests::fixtures::good_gateway_pledge(), @@ -196,32 +201,42 @@ pub(crate) mod tests { #[test] fn query_for_gateway_owner_works() { let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); // "fred" does not own a mixnode if there are no mixnodes - let res = query_owns_gateway(deps.as_ref(), "fred".to_string()).unwrap(); + let res = query_owned_gateway(deps.as_ref(), "fred".to_string()).unwrap(); assert!(res.gateway.is_none()); // mixnode was added to "bob", "fred" still does not own one - test_helpers::add_gateway("bob", tests::fixtures::good_gateway_pledge(), deps.as_mut()); + test_helpers::add_gateway( + &mut rng, + deps.as_mut(), + env.clone(), + "bob", + tests::fixtures::good_gateway_pledge(), + ); - let res = query_owns_gateway(deps.as_ref(), "fred".to_string()).unwrap(); + let res = query_owned_gateway(deps.as_ref(), "fred".to_string()).unwrap(); assert!(res.gateway.is_none()); // "fred" now owns a gateway! test_helpers::add_gateway( + &mut rng, + deps.as_mut(), + env, "fred", tests::fixtures::good_gateway_pledge(), - deps.as_mut(), ); - let res = query_owns_gateway(deps.as_ref(), "fred".to_string()).unwrap(); + let res = query_owned_gateway(deps.as_ref(), "fred".to_string()).unwrap(); assert!(res.gateway.is_some()); // but after unbonding it, he doesn't own one anymore crate::gateways::transactions::try_remove_gateway(deps.as_mut(), mock_info("fred", &[])) .unwrap(); - let res = query_owns_gateway(deps.as_ref(), "fred".to_string()).unwrap(); + let res = query_owned_gateway(deps.as_ref(), "fred".to_string()).unwrap(); assert!(res.gateway.is_none()); } } diff --git a/contracts/mixnet/src/gateways/storage.rs b/contracts/mixnet/src/gateways/storage.rs index 89786a3019..9b5892844a 100644 --- a/contracts/mixnet/src/gateways/storage.rs +++ b/contracts/mixnet/src/gateways/storage.rs @@ -1,14 +1,11 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::constants::{GATEWAYS_OWNER_IDX_NAMESPACE, GATEWAYS_PK_NAMESPACE}; use cosmwasm_std::Addr; use cw_storage_plus::{Index, IndexList, IndexedMap, UniqueIndex}; use mixnet_contract_common::{GatewayBond, IdentityKeyRef}; -// storage prefixes -const GATEWAYS_PK_NAMESPACE: &str = "gt"; -const GATEWAYS_OWNER_IDX_NAMESPACE: &str = "gto"; - pub(crate) struct GatewayBondIndex<'a> { pub(crate) owner: UniqueIndex<'a, Addr, GatewayBond>, } @@ -30,77 +27,3 @@ pub(crate) fn gateways<'a>() -> IndexedMap<'a, IdentityKeyRef<'a>, GatewayBond, }; IndexedMap::new(GATEWAYS_PK_NAMESPACE, indexes) } - -// currently not used outside tests -#[cfg(test)] -mod tests { - use super::super::storage; - use crate::support::tests; - use crate::support::tests::fixtures::TEST_COIN_DENOM; - use cosmwasm_std::testing::MockStorage; - use cosmwasm_std::StdResult; - use cosmwasm_std::Storage; - use cosmwasm_std::{coin, Addr, Uint128}; - use mixnet_contract_common::{Gateway, GatewayBond, IdentityKey, IdentityKeyRef}; - - // currently this is only used in tests but may become useful later on - pub(crate) fn read_gateway_pledge_amount( - storage: &dyn Storage, - identity: IdentityKeyRef<'_>, - ) -> StdResult { - let node = storage::gateways().load(storage, identity)?; - Ok(node.pledge_amount.amount) - } - - #[test] - fn gateway_single_read_retrieval() { - let mut storage = MockStorage::new(); - let bond1 = tests::fixtures::gateway_bond_fixture("owner1"); - let bond2 = tests::fixtures::gateway_bond_fixture("owner2"); - storage::gateways() - .save(&mut storage, "bond1", &bond1) - .unwrap(); - storage::gateways() - .save(&mut storage, "bond2", &bond2) - .unwrap(); - - let res1 = storage::gateways().load(&storage, "bond1").unwrap(); - let res2 = storage::gateways().load(&storage, "bond2").unwrap(); - assert_eq!(bond1, res1); - assert_eq!(bond2, res2); - } - - #[test] - fn reading_gateway_bond() { - let mut mock_storage = MockStorage::new(); - let node_owner: Addr = Addr::unchecked("node-owner"); - let node_identity: IdentityKey = "nodeidentity".into(); - - // produces an error if target gateway doesn't exist - let res = read_gateway_pledge_amount(&mock_storage, &node_identity); - assert!(res.is_err()); - - // returns appropriate value otherwise - let pledge_amount = 1000; - - let gateway_bond = GatewayBond { - pledge_amount: coin(pledge_amount, TEST_COIN_DENOM), - owner: node_owner, - block_height: 12_345, - gateway: Gateway { - identity_key: node_identity.clone(), - ..tests::fixtures::gateway_fixture() - }, - proxy: None, - }; - - storage::gateways() - .save(&mut mock_storage, &node_identity, &gateway_bond) - .unwrap(); - - assert_eq!( - Uint128::new(pledge_amount), - read_gateway_pledge_amount(&mock_storage, &node_identity).unwrap() - ); - } -} diff --git a/contracts/mixnet/src/gateways/transactions.rs b/contracts/mixnet/src/gateways/transactions.rs index 6315a2b2e2..3b28394e46 100644 --- a/contracts/mixnet/src/gateways/transactions.rs +++ b/contracts/mixnet/src/gateways/transactions.rs @@ -1,17 +1,16 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use super::storage; -use crate::error::ContractError; use crate::mixnet_contract_settings::storage as mixnet_params_storage; -use crate::support::helpers::{ensure_no_existing_bond, validate_node_identity_signature}; -use cosmwasm_std::{ - wasm_execute, Addr, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, Uint128, +use crate::support::helpers::{ + ensure_no_existing_bond, validate_node_identity_signature, validate_pledge, }; +use cosmwasm_std::{wasm_execute, Addr, BankMsg, Coin, DepsMut, Env, MessageInfo, Response}; +use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{new_gateway_bonding_event, new_gateway_unbonding_event}; -use mixnet_contract_common::{Gateway, GatewayBond, Layer}; +use mixnet_contract_common::{Gateway, GatewayBond}; use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; -use vesting_contract_common::one_ucoin; pub fn try_add_gateway( deps: DepsMut<'_>, @@ -19,21 +18,13 @@ pub fn try_add_gateway( info: MessageInfo, gateway: Gateway, owner_signature: String, -) -> Result { - // check if the pledge contains any funds of the appropriate denomination - let minimum_pledge = mixnet_params_storage::CONTRACT_STATE - .load(deps.storage)? - .params - .minimum_mixnode_pledge; - let mix_denom = mixnet_params_storage::mix_denom(deps.storage)?; - let pledge = validate_gateway_pledge(info.funds, minimum_pledge, mix_denom)?; - +) -> Result { _try_add_gateway( deps, env, gateway, - pledge, - info.sender.as_str(), + info.funds, + info.sender, owner_signature, None, ) @@ -46,22 +37,15 @@ pub fn try_add_gateway_on_behalf( gateway: Gateway, owner: String, owner_signature: String, -) -> Result { - // check if the pledge contains any funds of the appropriate denomination - let minimum_pledge = mixnet_params_storage::CONTRACT_STATE - .load(deps.storage)? - .params - .minimum_mixnode_pledge; - let mix_denom = mixnet_params_storage::mix_denom(deps.storage)?; - let pledge = validate_gateway_pledge(info.funds, minimum_pledge, mix_denom)?; - +) -> Result { let proxy = info.sender; + let owner = deps.api.addr_validate(&owner)?; _try_add_gateway( deps, env, gateway, - pledge, - &owner, + info.funds, + owner, owner_signature, Some(proxy), ) @@ -71,12 +55,14 @@ pub(crate) fn _try_add_gateway( deps: DepsMut<'_>, env: Env, gateway: Gateway, - pledge: Coin, - owner: &str, + pledge: Vec, + owner: Addr, owner_signature: String, proxy: Option, -) -> Result { - let owner = deps.api.addr_validate(owner)?; +) -> 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)?; // if the client has an active bonded mixnode or gateway, don't allow bonding ensure_no_existing_bond(deps.storage, &owner)?; @@ -86,7 +72,7 @@ pub(crate) fn _try_add_gateway( storage::gateways().may_load(deps.storage, &gateway.identity_key)? { if existing_bond.owner != owner { - return Err(ContractError::DuplicateGateway { + return Err(MixnetContractError::DuplicateGateway { owner: existing_bond.owner, }); } @@ -110,7 +96,6 @@ pub(crate) fn _try_add_gateway( ); storage::gateways().save(deps.storage, bond.identity(), &bond)?; - mixnet_params_storage::increment_layer_count(deps.storage, Layer::Gateway)?; Ok(Response::new().add_event(new_gateway_bonding_event( &owner, @@ -124,22 +109,24 @@ pub fn try_remove_gateway_on_behalf( deps: DepsMut<'_>, info: MessageInfo, owner: String, -) -> Result { +) -> Result { let proxy = info.sender; - _try_remove_gateway(deps, &owner, Some(proxy)) + 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.as_ref(), None) +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: &str, + owner: Addr, proxy: Option, -) -> Result { - let owner = deps.api.addr_validate(owner)?; - let mix_denom = mixnet_params_storage::mix_denom(deps.storage)?; +) -> Result { // try to find the node of the sender let gateway_bond = match storage::gateways() .idx @@ -147,11 +134,11 @@ pub(crate) fn _try_remove_gateway( .item(deps.storage, owner.clone())? { Some(record) => record.1, - None => return Err(ContractError::NoAssociatedGatewayBond { owner }), + None => return Err(MixnetContractError::NoAssociatedGatewayBond { owner }), }; if proxy != gateway_bond.proxy { - return Err(ContractError::ProxyMismatch { + return Err(MixnetContractError::ProxyMismatch { existing: gateway_bond .proxy .map_or_else(|| "None".to_string(), |a| a.as_str().to_string()), @@ -168,9 +155,6 @@ pub(crate) fn _try_remove_gateway( // remove the bond storage::gateways().remove(deps.storage, gateway_bond.identity())?; - // decrement layer count - mixnet_params_storage::decrement_layer_count(deps.storage, Layer::Gateway)?; - let mut response = Response::new().add_message(return_tokens); if let Some(proxy) = &proxy { @@ -179,7 +163,7 @@ pub(crate) fn _try_remove_gateway( amount: gateway_bond.pledge_amount(), }; - let track_unbond_message = wasm_execute(proxy, &msg, vec![one_ucoin(mix_denom)])?; + let track_unbond_message = wasm_execute(proxy, &msg, vec![])?; response = response.add_message(track_unbond_message); } @@ -191,235 +175,116 @@ pub(crate) fn _try_remove_gateway( ))) } -fn validate_gateway_pledge( - mut pledge: Vec, - minimum_pledge: Uint128, - mix_denom: String, -) -> Result { - // check if anything was put as bond - if pledge.is_empty() { - return Err(ContractError::NoBondFound { mix_denom }); - } - - if pledge.len() > 1 { - return Err(ContractError::MultipleDenoms); - } - - // check that the denomination is correct - if pledge[0].denom != mix_denom { - return Err(ContractError::WrongDenom { mix_denom }); - } - - // check that we have at least 100 coins in our pledge - if pledge[0].amount < minimum_pledge { - return Err(ContractError::InsufficientGatewayBond { - received: pledge[0].amount.into(), - minimum: minimum_pledge.into(), - }); - } - - Ok(pledge.pop().unwrap()) -} - #[cfg(test)] pub mod tests { - use super::*; - use crate::contract::{execute, query, INITIAL_GATEWAY_PLEDGE}; - use crate::error::ContractError; - use crate::gateways::transactions::validate_gateway_pledge; + use crate::contract::execute; + use crate::gateways::transactions::try_add_gateway; + use crate::interval::pending_events; + use crate::mixnet_contract_settings::storage::minimum_gateway_pledge; use crate::support::tests; use crate::support::tests::fixtures::TEST_COIN_DENOM; - use crate::support::tests::test_helpers; + use crate::support::tests::{fixtures, test_helpers}; use cosmwasm_std::testing::{mock_env, mock_info}; - use cosmwasm_std::{coins, BankMsg, Response}; - use cosmwasm_std::{from_binary, Addr, Uint128}; - use mixnet_contract_common::{ExecuteMsg, Gateway, PagedGatewayResponse, QueryMsg}; + use cosmwasm_std::{coin, Addr, BankMsg, Response, Uint128}; + use mixnet_contract_common::error::MixnetContractError; + use mixnet_contract_common::events::new_gateway_unbonding_event; + use mixnet_contract_common::ExecuteMsg; #[test] fn gateway_add() { let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); // if we fail validation (by say not sending enough funds - let insufficient_bond = Into::::into(INITIAL_GATEWAY_PLEDGE) - 1; - let info = mock_info("anyone", &coins(insufficient_bond, TEST_COIN_DENOM)); - let (msg, _) = tests::messages::valid_bond_gateway_msg("anyone"); + let sender = "alice"; + let minimum_pledge = minimum_gateway_pledge(deps.as_ref().storage).unwrap(); + let mut insufficient_pledge = minimum_pledge.clone(); + insufficient_pledge.amount -= Uint128::new(1000); + + let info = mock_info(sender, &[insufficient_pledge.clone()]); + let (gateway, sig) = test_helpers::gateway_with_signature(&mut rng, sender); + + let result = try_add_gateway( + deps.as_mut(), + env.clone(), + info, + gateway.clone(), + sig.clone(), + ); // we are informed that we didn't send enough funds - let result = execute(deps.as_mut(), mock_env(), info, msg); assert_eq!( result, - Err(ContractError::InsufficientGatewayBond { - received: insufficient_bond, - minimum: INITIAL_GATEWAY_PLEDGE.into(), + Err(MixnetContractError::InsufficientPledge { + received: insufficient_pledge, + minimum: minimum_pledge.clone(), }) ); - // make sure no gateway was inserted into the topology - let res = query( - deps.as_ref(), - mock_env(), - QueryMsg::GetGateways { - start_after: None, - limit: Option::from(2), - }, - ) - .unwrap(); - let page: PagedGatewayResponse = from_binary(&res).unwrap(); - assert_eq!(0, page.nodes.len()); + // if the signature provided is invalid, the bonding also fails + let info = mock_info(sender, &[minimum_pledge]); - // if we send enough funds - let info = mock_info("anyone", &tests::fixtures::good_gateway_pledge()); - let (msg, identity) = tests::messages::valid_bond_gateway_msg("anyone"); - - // we get back a message telling us everything was OK - let execute_response = execute(deps.as_mut(), mock_env(), info, msg); - assert!(execute_response.is_ok()); - - // we can query topology and the new node is there - let query_response = query( - deps.as_ref(), - mock_env(), - QueryMsg::GetGateways { - start_after: None, - limit: Option::from(2), - }, - ) - .unwrap(); - let page: PagedGatewayResponse = from_binary(&query_response).unwrap(); - assert_eq!(1, page.nodes.len()); - assert_eq!( - &Gateway { - identity_key: identity, - ..tests::fixtures::gateway_fixture() - }, - page.nodes[0].gateway() + let result = try_add_gateway( + deps.as_mut(), + env.clone(), + info.clone(), + gateway.clone(), + "bad-signature".into(), ); + assert!(matches!( + result, + Err(MixnetContractError::MalformedEd25519Signature(..)) + )); // if there was already a gateway bonded by particular user - let info = mock_info("foomper", &tests::fixtures::good_gateway_pledge()); - let (msg, _) = tests::messages::valid_bond_gateway_msg("foomper"); - execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - let info = mock_info("foomper", &tests::fixtures::good_gateway_pledge()); - let (msg, _) = tests::messages::valid_bond_gateway_msg("foomper"); + test_helpers::add_gateway( + &mut rng, + deps.as_mut(), + env.clone(), + sender, + fixtures::good_gateway_pledge(), + ); // it fails - let execute_response = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(Err(ContractError::AlreadyOwnsGateway), execute_response); + let result = try_add_gateway(deps.as_mut(), env.clone(), info, gateway, sig); + assert_eq!(Err(MixnetContractError::AlreadyOwnsGateway), result); - // bonding fails if the user already owns a mixnode - test_helpers::add_mixnode( - "mixnode-owner", - tests::fixtures::good_mixnode_pledge(), + // the same holds if the user already owns a mixnode + let sender2 = "mixnode-owner"; + + let mix_id = test_helpers::add_mixnode( + &mut rng, deps.as_mut(), + env.clone(), + sender2, + vec![coin(100_000_000, TEST_COIN_DENOM)], ); - let info = mock_info("mixnode-owner", &tests::fixtures::good_gateway_pledge()); - let (msg, _) = tests::messages::valid_bond_gateway_msg("mixnode-owner"); + let info = mock_info(sender2, &fixtures::good_gateway_pledge()); + let (gateway, sig) = test_helpers::gateway_with_signature(&mut rng, sender2); - let execute_response = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(execute_response, Err(ContractError::AlreadyOwnsMixnode)); + let result = try_add_gateway( + deps.as_mut(), + env.clone(), + info.clone(), + gateway.clone(), + sig.clone(), + ); + assert_eq!(Err(MixnetContractError::AlreadyOwnsMixnode), result); // but after he unbonds it, it's all fine again - let info = mock_info("mixnode-owner", &[]); - let msg = ExecuteMsg::UnbondMixnode {}; - execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + pending_events::unbond_mixnode(deps.as_mut(), &env, mix_id).unwrap(); - let info = mock_info("mixnode-owner", &tests::fixtures::good_gateway_pledge()); - let (msg, _) = tests::messages::valid_bond_gateway_msg("mixnode-owner"); - - let execute_response = execute(deps.as_mut(), mock_env(), info, msg); - assert!(execute_response.is_ok()); - - // adding another node from another account, but with the same IP, should fail (or we would have a weird state). - // Is that right? Think about this, not sure yet. - } - - #[test] - fn adding_gateway_without_existing_owner() { - let mut deps = test_helpers::init_contract(); - - let info = mock_info("gateway-owner", &tests::fixtures::good_gateway_pledge()); - - // before the execution the node had no associated owner - assert!(storage::gateways() - .idx - .owner - .item(deps.as_ref().storage, Addr::unchecked("gateway-owner")) - .unwrap() - .is_none()); - - let (msg, identity) = tests::messages::valid_bond_gateway_msg("gateway-owner"); - - // it's all fine, owner is saved - let execute_response = execute(deps.as_mut(), mock_env(), info, msg); - assert!(execute_response.is_ok()); - - assert_eq!( - &identity, - storage::gateways() - .idx - .owner - .item(deps.as_ref().storage, Addr::unchecked("gateway-owner")) - .unwrap() - .unwrap() - .1 - .identity() - ); - } - - #[test] - fn adding_gateway_with_existing_owner() { - let mut deps = test_helpers::init_contract(); - - let identity = test_helpers::add_gateway( - "gateway-owner", - tests::fixtures::good_gateway_pledge(), - deps.as_mut(), - ); - - // request fails giving the existing owner address in the message - let info = mock_info( - "gateway-owner-pretender", - &tests::fixtures::good_gateway_pledge(), - ); - let msg = ExecuteMsg::BondGateway { - gateway: Gateway { - identity_key: identity, - ..tests::fixtures::gateway_fixture() - }, - owner_signature: "foomp".to_string(), - }; - - let execute_response = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!( - Err(ContractError::DuplicateGateway { - owner: Addr::unchecked("gateway-owner") - }), - execute_response - ); - } - - #[test] - fn adding_gateway_with_existing_unchanged_owner() { - let mut deps = test_helpers::init_contract(); - - test_helpers::add_gateway( - "gateway-owner", - tests::fixtures::good_gateway_pledge(), - deps.as_mut(), - ); - - let info = mock_info("gateway-owner", &tests::fixtures::good_gateway_pledge()); - let (msg, _) = tests::messages::valid_bond_gateway_msg("gateway-owner"); - - let res = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(Err(ContractError::AlreadyOwnsGateway), res); + let result = try_add_gateway(deps.as_mut(), env, info, gateway, sig); + assert!(result.is_ok()); } #[test] fn gateway_remove() { let mut deps = test_helpers::init_contract(); + let mut rng = test_helpers::test_rng(); + let env = mock_env(); // try unbond when no nodes exist yet let info = mock_info("anyone", &[]); @@ -429,13 +294,19 @@ pub mod tests { // we're told that there is no node for our address assert_eq!( result, - Err(ContractError::NoAssociatedGatewayBond { + Err(MixnetContractError::NoAssociatedGatewayBond { owner: Addr::unchecked("anyone") }) ); // let's add a node owned by bob - test_helpers::add_gateway("bob", tests::fixtures::good_gateway_pledge(), deps.as_mut()); + test_helpers::add_gateway( + &mut rng, + deps.as_mut(), + env.clone(), + "bob", + fixtures::good_gateway_pledge(), + ); // attempt to unbond fred's node, which doesn't exist let info = mock_info("fred", &[]); @@ -443,7 +314,7 @@ pub mod tests { let result = execute(deps.as_mut(), mock_env(), info, msg); assert_eq!( result, - Err(ContractError::NoAssociatedGatewayBond { + Err(MixnetContractError::NoAssociatedGatewayBond { owner: Addr::unchecked("fred") }) ); @@ -457,9 +328,11 @@ pub mod tests { // add a node owned by fred let fred_identity = test_helpers::add_gateway( + &mut rng, + deps.as_mut(), + env, "fred", tests::fixtures::good_gateway_pledge(), - deps.as_mut(), ); // let's make sure we now have 2 nodes: @@ -494,124 +367,4 @@ pub mod tests { assert_eq!(1, gateway_bonds.len()); assert_eq!(&Addr::unchecked("bob"), gateway_bonds[0].owner()); } - - #[test] - fn removing_gateway_clears_ownership() { - let mut deps = test_helpers::init_contract(); - - let info = mock_info("gateway-owner", &tests::fixtures::good_gateway_pledge()); - let (bond_msg, identity) = tests::messages::valid_bond_gateway_msg("gateway-owner"); - execute(deps.as_mut(), mock_env(), info, bond_msg.clone()).unwrap(); - - assert_eq!( - &identity, - storage::gateways() - .idx - .owner - .item(deps.as_ref().storage, Addr::unchecked("gateway-owner")) - .unwrap() - .unwrap() - .1 - .identity() - ); - - let info = mock_info("gateway-owner", &[]); - let msg = ExecuteMsg::UnbondGateway {}; - - assert!(execute(deps.as_mut(), mock_env(), info, msg).is_ok()); - - assert!(storage::gateways() - .idx - .owner - .item(deps.as_ref().storage, Addr::unchecked("gateway-owner")) - .unwrap() - .is_none()); - - // and since it's removed, it can be reclaimed - let info = mock_info("gateway-owner", &tests::fixtures::good_gateway_pledge()); - - assert!(execute(deps.as_mut(), mock_env(), info, bond_msg).is_ok()); - assert_eq!( - &identity, - storage::gateways() - .idx - .owner - .item(deps.as_ref().storage, Addr::unchecked("gateway-owner")) - .unwrap() - .unwrap() - .1 - .identity() - ); - } - - #[test] - fn validating_gateway_bond() { - // you must send SOME funds - let result = validate_gateway_pledge( - Vec::new(), - INITIAL_GATEWAY_PLEDGE, - TEST_COIN_DENOM.to_string(), - ); - assert_eq!( - result, - Err(ContractError::NoBondFound { - mix_denom: TEST_COIN_DENOM.to_string() - }) - ); - - // you must send at least 100 coins... - let mut bond = tests::fixtures::good_gateway_pledge(); - bond[0].amount = INITIAL_GATEWAY_PLEDGE.checked_sub(Uint128::new(1)).unwrap(); - let result = validate_gateway_pledge( - bond.clone(), - INITIAL_GATEWAY_PLEDGE, - TEST_COIN_DENOM.to_string(), - ); - assert_eq!( - result, - Err(ContractError::InsufficientGatewayBond { - received: Into::::into(INITIAL_GATEWAY_PLEDGE) - 1, - minimum: INITIAL_GATEWAY_PLEDGE.into(), - }) - ); - - // more than that is still fine - let mut bond = tests::fixtures::good_gateway_pledge(); - bond[0].amount = INITIAL_GATEWAY_PLEDGE + Uint128::new(1); - let result = validate_gateway_pledge( - bond.clone(), - INITIAL_GATEWAY_PLEDGE, - TEST_COIN_DENOM.to_string(), - ); - assert!(result.is_ok()); - - // it must be sent in the defined denom! - let mut bond = tests::fixtures::good_gateway_pledge(); - bond[0].denom = "baddenom".to_string(); - let result = validate_gateway_pledge( - bond.clone(), - INITIAL_GATEWAY_PLEDGE, - TEST_COIN_DENOM.to_string(), - ); - assert_eq!( - result, - Err(ContractError::WrongDenom { - mix_denom: TEST_COIN_DENOM.to_string() - }) - ); - - let mut bond = tests::fixtures::good_gateway_pledge(); - bond[0].denom = "foomp".to_string(); - let result = validate_gateway_pledge( - bond.clone(), - INITIAL_GATEWAY_PLEDGE, - TEST_COIN_DENOM.to_string(), - ); - assert_eq!( - result, - Err(ContractError::WrongDenom { - mix_denom: TEST_COIN_DENOM.to_string() - }) - ); - } } diff --git a/contracts/mixnet/src/interval/helpers.rs b/contracts/mixnet/src/interval/helpers.rs new file mode 100644 index 0000000000..5187943154 --- /dev/null +++ b/contracts/mixnet/src/interval/helpers.rs @@ -0,0 +1,96 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::interval::storage; +use crate::rewards::storage as rewards_storage; +use cosmwasm_std::{Response, Storage}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::events::new_interval_config_update_event; +use mixnet_contract_common::Interval; +use std::time::Duration; + +pub(crate) fn change_interval_config( + store: &mut dyn Storage, + mut current_interval: Interval, + epochs_in_interval: u32, + epoch_duration_secs: u64, +) -> Result { + current_interval.change_epoch_length(Duration::from_secs(epoch_duration_secs)); + + let mut rewarding_params = rewards_storage::REWARDING_PARAMS.load(store)?; + rewarding_params.apply_epochs_in_interval_change(epochs_in_interval); + rewards_storage::REWARDING_PARAMS.save(store, &rewarding_params)?; + + current_interval.force_change_epochs_in_interval(epochs_in_interval); + storage::save_interval(store, ¤t_interval)?; + + Ok(Response::new().add_event(new_interval_config_update_event( + epochs_in_interval, + epoch_duration_secs, + rewarding_params.interval, + ))) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::support::tests::test_helpers; + use cosmwasm_std::Decimal; + + #[test] + fn changing_interval_config() { + let two = Decimal::from_atomics(2u32, 0).unwrap(); + let mut deps = test_helpers::init_contract(); + + let initial_interval = storage::current_interval(&deps.storage).unwrap(); + let initial_params = rewards_storage::REWARDING_PARAMS + .load(&deps.storage) + .unwrap(); + + // if we half the number of epochs, the reward budget should get doubled + change_interval_config( + &mut deps.storage, + initial_interval, + initial_interval.epochs_in_interval() / 2, + initial_interval.epoch_length_secs(), + ) + .unwrap(); + let updated_interval = storage::current_interval(&deps.storage).unwrap(); + let updated_params = rewards_storage::REWARDING_PARAMS + .load(&deps.storage) + .unwrap(); + + assert_eq!( + updated_interval.epochs_in_interval(), + initial_interval.epochs_in_interval() / 2 + ); + assert_eq!( + updated_params.interval.epoch_reward_budget, + initial_params.interval.epoch_reward_budget * two + ); + + // and similarly when we double number of epochs, the reward budget should get halved + change_interval_config( + &mut deps.storage, + initial_interval, + initial_interval.epochs_in_interval() * 2, + initial_interval.epoch_length_secs(), + ) + .unwrap(); + + // if we half the number of epochs, the reward budget should get doubled + let updated_interval = storage::current_interval(&deps.storage).unwrap(); + let updated_params = rewards_storage::REWARDING_PARAMS + .load(&deps.storage) + .unwrap(); + + assert_eq!( + updated_interval.epochs_in_interval(), + initial_interval.epochs_in_interval() * 2 + ); + assert_eq!( + updated_params.interval.epoch_reward_budget, + initial_params.interval.epoch_reward_budget / two + ); + } +} diff --git a/contracts/mixnet/src/interval/mod.rs b/contracts/mixnet/src/interval/mod.rs index 11f07ae776..9f81da03d1 100644 --- a/contracts/mixnet/src/interval/mod.rs +++ b/contracts/mixnet/src/interval/mod.rs @@ -1,6 +1,8 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +pub mod helpers; +pub(crate) mod pending_events; pub mod queries; pub mod storage; pub mod transactions; diff --git a/contracts/mixnet/src/interval/pending_events.rs b/contracts/mixnet/src/interval/pending_events.rs new file mode 100644 index 0000000000..6fa599927c --- /dev/null +++ b/contracts/mixnet/src/interval/pending_events.rs @@ -0,0 +1,1253 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::delegations; +use crate::delegations::storage as delegations_storage; +use crate::interval::helpers::change_interval_config; +use crate::interval::storage; +use crate::mixnet_contract_settings::storage as mixnet_params_storage; +use crate::mixnodes::helpers::{cleanup_post_unbond_mixnode_storage, get_mixnode_details_by_id}; +use crate::rewards::storage as rewards_storage; +use crate::support::helpers::send_to_proxy_or_owner; +use cosmwasm_std::{wasm_execute, Addr, Coin, DepsMut, Env, Response}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::events::{ + new_active_set_update_event, new_delegation_event, new_delegation_on_unbonded_node_event, + new_mixnode_cost_params_update_event, new_mixnode_unbonding_event, + new_rewarding_params_update_event, new_undelegation_event, +}; +use mixnet_contract_common::mixnode::MixNodeCostParams; +use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData}; +use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate; +use mixnet_contract_common::{Delegation, NodeId}; +use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; + +pub(crate) trait ContractExecutableEvent { + // note: the error only means a HARD error like we failed to read from storage. + // if, for example, delegating fails because mixnode no longer exists, we return an Ok(()), + // because it's not a hard error and we don't want to fail the entire transaction + fn execute(self, deps: DepsMut<'_>, env: &Env) -> Result; +} + +pub(crate) fn delegate( + deps: DepsMut<'_>, + env: &Env, + owner: Addr, + mix_id: NodeId, + 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 + // unbonded at this very block (if the event was pending), but that's tough luck, then it's up + // to the delegator to click the undelegate button + let mixnode_details = match get_mixnode_details_by_id(deps.storage, mix_id)? { + Some(details) + if details.rewarding_details.still_bonded() + && !details.bond_information.is_unbonding => + { + details + } + _ => { + // if mixnode is no longer bonded or in the process of unbonding, return the tokens back to the + // delegator; + let return_tokens = send_to_proxy_or_owner(&proxy, &owner, vec![amount.clone()]); + let mut response = Response::new().add_message(return_tokens).add_event( + new_delegation_on_unbonded_node_event(&owner, &proxy, mix_id), + ); + + 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::TrackUndelegation { + owner: owner.into_string(), + mix_id, + amount, + }; + + let track_undelegate_message = wasm_execute(proxy, &msg, vec![])?; + response = response.add_message(track_undelegate_message); + } + } + + return Ok(response); + } + }; + + let new_delegation_amount = amount.clone(); + let mut mix_rewarding = mixnode_details.rewarding_details; + + // the delegation_amount might get increased if there's already a pre-existing delegation on this mixnode + // (in that case we just create a fresh delegation with the sum of both) + let mut stored_delegation_amount = amount; + + // 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 old_delegation = if let Some(existing_delegation) = + delegations_storage::delegations().may_load(deps.storage, storage_key.clone())? + { + // completely remove the delegation from the node + let og_with_reward = mix_rewarding.undelegate(&existing_delegation)?; + + // and adjust the new value by the amount removed (which contains the original delegation + // alongside any earned rewards) + stored_delegation_amount.amount += og_with_reward.amount; + + Some(existing_delegation) + } else { + None + }; + + // add the amount we're intending to delegate (whether it's fresh or we're adding to the existing one) + mix_rewarding.add_base_delegation(stored_delegation_amount.amount); + + let cosmos_event = new_delegation_event(&owner, &proxy, &new_delegation_amount, mix_id); + + let delegation = Delegation::new( + owner, + mix_id, + 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 + delegations_storage::delegations().replace( + deps.storage, + storage_key, + Some(&delegation), + old_delegation.as_ref(), + )?; + rewards_storage::MIXNODE_REWARDING.save(deps.storage, mix_id, &mix_rewarding)?; + + Ok(Response::new().add_event(cosmos_event)) +} + +pub(crate) fn undelegate( + deps: DepsMut<'_>, + owner: Addr, + mix_id: NodeId, + 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 delegation = match delegations_storage::delegations().may_load(deps.storage, storage_key)? { + None => return Ok(Response::default()), + Some(delegation) => delegation, + }; + let mix_rewarding = + rewards_storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)?.ok_or(MixnetContractError::InconsistentState { + comment: "mixnode rewarding got removed from the storage whilst there's still an existing delegation" + .into(), + })?; + // this also appropriately adjusts the storage + let tokens_to_return = + delegations::helpers::undelegate(deps.storage, delegation, mix_rewarding)?; + + let return_tokens = send_to_proxy_or_owner(&proxy, &owner, vec![tokens_to_return.clone()]); + let mut response = Response::new() + .add_message(return_tokens) + .add_event(new_undelegation_event(&owner, &proxy, mix_id)); + + 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::TrackUndelegation { + owner: owner.into_string(), + mix_id, + amount: tokens_to_return, + }; + + let track_undelegate_message = wasm_execute(proxy, &msg, vec![])?; + response = response.add_message(track_undelegate_message); + } + } + + Ok(response) +} + +pub(crate) fn unbond_mixnode( + deps: DepsMut<'_>, + env: &Env, + mix_id: NodeId, +) -> Result { + // if we're here it means user executed `_try_remove_mixnode` and as a result node was set to be + // in unbonding state and thus nothing could have been done to it (such as attempting to double unbond it) + // thus the node with all its associated information MUST exist in the storage. + let node_details = get_mixnode_details_by_id(deps.storage, mix_id)?.ok_or( + MixnetContractError::InconsistentState { + comment: "mixnode getting processed to get unbonded doesn't exist in the storage" + .into(), + }, + )?; + + // the denom on the original pledge was validated at the time of bonding so we can safely reuse it here + let rewarding_denom = &node_details.bond_information.original_pledge.denom; + let tokens = node_details + .rewarding_details + .operator_pledge_with_reward(rewarding_denom); + + let 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 mut response = Response::new() + .add_message(return_tokens) + .add_event(new_mixnode_unbonding_event(mix_id)); + + 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::TrackUnbondMixnode { + owner: owner.clone().into_string(), + amount: tokens, + }; + + let track_unbond_message = wasm_execute(proxy, &msg, vec![])?; + response = response.add_message(track_unbond_message); + } + } + + Ok(response) +} + +pub(crate) fn update_active_set_size( + deps: DepsMut<'_>, + active_set_size: u32, +) -> Result { + // We don't have to check for authorization as this event can only be pushed + // by the authorized entity. + // Furthermore, we don't need to check whether the epoch is finished as the + // queue is only emptied upon the epoch finishing. + // Also, we know the update is valid as we checked for that before pushing the event onto the queue. + + let mut rewarding_params = rewards_storage::REWARDING_PARAMS.load(deps.storage)?; + rewarding_params.try_change_active_set_size(active_set_size)?; + rewards_storage::REWARDING_PARAMS.save(deps.storage, &rewarding_params)?; + + Ok(Response::new().add_event(new_active_set_update_event(active_set_size))) +} + +impl ContractExecutableEvent for PendingEpochEventData { + fn execute(self, deps: DepsMut<'_>, env: &Env) -> Result { + // note that the basic validation on all those events was already performed before + // they were pushed onto the queue + match self { + PendingEpochEventData::Delegate { + owner, + mix_id, + amount, + proxy, + } => delegate(deps, env, owner, mix_id, amount, proxy), + PendingEpochEventData::Undelegate { + owner, + mix_id, + proxy, + } => undelegate(deps, owner, mix_id, proxy), + PendingEpochEventData::UnbondMixnode { mix_id } => unbond_mixnode(deps, env, mix_id), + PendingEpochEventData::UpdateActiveSetSize { new_size } => { + update_active_set_size(deps, new_size) + } + } + } +} + +pub(crate) fn change_mix_cost_params( + deps: DepsMut<'_>, + mix_id: NodeId, + new_costs: MixNodeCostParams, +) -> Result { + // almost an entire interval might have passed since the request was issued -> check if the + // node still exists + // + // note: there's no check if the bond is in "unbonding" state, as epoch actions would get + // cleared before touching interval actions + let mut mix_rewarding = + match rewards_storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)? { + Some(mix_rewarding) if mix_rewarding.still_bonded() => mix_rewarding, + // if node doesn't exist anymore, don't do anything, simple as that. + _ => return Ok(Response::default()), + }; + + let cosmos_event = new_mixnode_cost_params_update_event(mix_id, &new_costs); + + // TODO: can we just change cost_params without breaking rewarding calculation? + // (I'm almost certain we can, but well, it has to be tested) + mix_rewarding.cost_params = new_costs; + rewards_storage::MIXNODE_REWARDING.save(deps.storage, mix_id, &mix_rewarding)?; + + Ok(Response::new().add_event(cosmos_event)) +} + +pub(crate) fn update_rewarding_params( + deps: DepsMut<'_>, + updated_params: IntervalRewardingParamsUpdate, +) -> Result { + // We don't have to check for authorization as this event can only be pushed + // by the authorized entity. + // Furthermore, we don't need to check whether the interval is finished as the + // queue is only emptied upon the interval finishing. + // Also, we know the update is valid as we checked for that before pushing the event onto the queue. + let interval = storage::current_interval(deps.storage)?; + + let mut rewarding_params = rewards_storage::REWARDING_PARAMS.load(deps.storage)?; + rewarding_params.try_apply_updates(updated_params, interval.epochs_in_interval())?; + rewards_storage::REWARDING_PARAMS.save(deps.storage, &rewarding_params)?; + + Ok(Response::new().add_event(new_rewarding_params_update_event( + updated_params, + rewarding_params.interval, + ))) +} + +pub(crate) fn update_interval_config( + deps: DepsMut, + epochs_in_interval: u32, + epoch_duration_secs: u64, +) -> Result { + // We don't have to check for authorization as this event can only be pushed + // by the authorized entity. + // Furthermore, we don't need to check whether the interval is finished as the + // queue is only emptied upon the interval finishing. + let interval = storage::current_interval(deps.storage)?; + change_interval_config( + deps.storage, + interval, + epochs_in_interval, + epoch_duration_secs, + ) +} + +impl ContractExecutableEvent for PendingIntervalEventData { + fn execute(self, deps: DepsMut<'_>, _env: &Env) -> Result { + // note that the basic validation on all those events was already performed before + // they were pushed onto the queue + match self { + PendingIntervalEventData::ChangeMixCostParams { + mix_id: mix, + new_costs, + } => change_mix_cost_params(deps, mix, new_costs), + PendingIntervalEventData::UpdateRewardingParams { update } => { + update_rewarding_params(deps, update) + } + PendingIntervalEventData::UpdateIntervalConfig { + epochs_in_interval, + epoch_duration_secs, + } => update_interval_config(deps, epochs_in_interval, epoch_duration_secs), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::support::tests::test_helpers; + use crate::support::tests::test_helpers::{assert_decimals, TestSetup}; + use cosmwasm_std::Decimal; + use mixnet_contract_common::Percent; + 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 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 cosmwasm_std::testing::mock_info; + use cosmwasm_std::{coin, to_binary, CosmosMsg, Decimal, WasmMsg}; + use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; + + #[test] + fn returns_the_tokens_if_mixnode_has_unbonded() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let delegation = 120_000_000u128; + let delegation_coin = coin(delegation, TEST_COIN_DENOM); + let owner1 = "delegator1"; + let owner2 = "delegator2"; + + // add pre-existing delegation + test.add_immediate_delegation(owner1, delegation, mix_id); + + let env = test.env(); + unbond_mixnode(test.deps_mut(), &env, mix_id).unwrap(); + + let res_increase = delegate( + test.deps_mut(), + &env, + Addr::unchecked(owner1), + mix_id, + delegation_coin.clone(), + None, + ) + .unwrap(); + + // delegation wasn't increased + let storage_key = + Delegation::generate_storage_key(mix_id, &Addr::unchecked(owner1), None); + let amount = delegations_storage::delegations() + .load(test.deps().storage, storage_key) + .unwrap() + .amount; + assert_eq!(amount, delegation_coin); + + // and all tokens are returned back to the delegator + let (receiver, sent_amount) = get_bank_send_msg(&res_increase).unwrap(); + assert_eq!(receiver, owner1); + assert_eq!(sent_amount[0], delegation_coin); + + // for a fresh delegation, nothing was added to the storage either + let res_fresh = delegate( + test.deps_mut(), + &env, + Addr::unchecked(owner2), + mix_id, + delegation_coin.clone(), + None, + ) + .unwrap(); + let storage_key = + Delegation::generate_storage_key(mix_id, &Addr::unchecked(owner2), None); + assert!(delegations_storage::delegations() + .may_load(test.deps().storage, storage_key) + .unwrap() + .is_none()); + + // and all tokens are returned back to the delegator + let (receiver, sent_amount) = get_bank_send_msg(&res_fresh).unwrap(); + assert_eq!(receiver, owner2); + assert_eq!(sent_amount[0], delegation_coin); + } + + #[test] + fn returns_the_tokens_is_mixnode_is_unbonding() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let delegation = 120_000_000u128; + let delegation_coin = coin(delegation, TEST_COIN_DENOM); + let owner1 = "delegator1"; + let owner2 = "delegator2"; + + // add pre-existing delegation + test.add_immediate_delegation(owner1, delegation, mix_id); + + let env = test.env(); + try_remove_mixnode(test.deps_mut(), mock_info("mix-owner", &[])).unwrap(); + + let res_increase = delegate( + test.deps_mut(), + &env, + Addr::unchecked(owner1), + mix_id, + delegation_coin.clone(), + None, + ) + .unwrap(); + + // delegation wasn't increased + let storage_key = + Delegation::generate_storage_key(mix_id, &Addr::unchecked(owner1), None); + let amount = delegations_storage::delegations() + .load(test.deps().storage, storage_key) + .unwrap() + .amount; + assert_eq!(amount, delegation_coin); + + // and all tokens are returned back to the delegator + let (receiver, sent_amount) = get_bank_send_msg(&res_increase).unwrap(); + assert_eq!(receiver, owner1); + assert_eq!(sent_amount[0], delegation_coin); + + // for a fresh delegation, nothing was added to the storage either + let res_fresh = delegate( + test.deps_mut(), + &env, + Addr::unchecked(owner2), + mix_id, + delegation_coin.clone(), + None, + ) + .unwrap(); + let storage_key = + Delegation::generate_storage_key(mix_id, &Addr::unchecked(owner2), None); + assert!(delegations_storage::delegations() + .may_load(test.deps().storage, storage_key) + .unwrap() + .is_none()); + + // and all tokens are returned back to the delegator + let (receiver, sent_amount) = get_bank_send_msg(&res_fresh).unwrap(); + assert_eq!(receiver, owner2); + assert_eq!(sent_amount[0], delegation_coin); + } + + #[test] + fn if_delegation_already_exists_a_fresh_one_with_sum_of_both_is_created() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", Some(100_000_000_000u128.into())); + + let delegation_og = 120_000_000u128; + let delegation_new = 543_000_000u128; + let delegation_coin_new = coin(delegation_new, TEST_COIN_DENOM); + + let owner = "delegator"; + test.add_immediate_delegation(owner, delegation_og, mix_id); + + let env = test.env(); + let res = delegate( + test.deps_mut(), + &env, + Addr::unchecked(owner), + mix_id, + delegation_coin_new, + None, + ) + .unwrap(); + + let expected_amount = delegation_og + delegation_new; + let expected_amount_dec = Decimal::from_atomics(expected_amount, 0).unwrap(); + + // no refunds here! + assert!(get_bank_send_msg(&res).is_none()); + + let rewarding = rewards_storage::MIXNODE_REWARDING + .load(test.deps().storage, mix_id) + .unwrap(); + let storage_key = + Delegation::generate_storage_key(mix_id, &Addr::unchecked(owner), None); + let delegation = delegations_storage::delegations() + .load(test.deps().storage, storage_key) + .unwrap(); + + assert_eq!(rewarding.unique_delegations, 1); + assert_eq!(rewarding.delegates, expected_amount_dec); + + assert_eq!(delegation.amount.amount.u128(), expected_amount) + } + + #[test] + fn if_delegation_already_exists_with_unclaimed_rewards_fresh_one_is_created() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", Some(100_000_000_000u128.into())); + + let delegation_og = 120_000_000u128; + let delegation_new = 543_000_000u128; + let delegation_coin_new = coin(delegation_new, TEST_COIN_DENOM); + + // perform some rewarding here to advance the unit delegation beyond the initial value + test.update_rewarded_set(vec![mix_id]); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let owner = "delegator"; + test.add_immediate_delegation(owner, delegation_og, mix_id); + + test.skip_to_next_epoch_end(); + let dist1 = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + test.skip_to_next_epoch_end(); + let dist2 = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let storage_key = + Delegation::generate_storage_key(mix_id, &Addr::unchecked(owner), None); + let delegation_pre = delegations_storage::delegations() + .load(test.deps().storage, storage_key.clone()) + .unwrap(); + + let env = test.env(); + let res = delegate( + test.deps_mut(), + &env, + Addr::unchecked(owner), + mix_id, + delegation_coin_new, + None, + ) + .unwrap(); + + let earned_before_update = dist1.delegates + dist2.delegates; + let truncated_reward = truncate_reward_amount(earned_before_update); + + let expected_amount = delegation_og + delegation_new + truncated_reward.u128(); + let expected_amount_dec = Decimal::from_atomics(expected_amount, 0).unwrap(); + + // no refunds here! + assert!(get_bank_send_msg(&res).is_none()); + + let rewarding = test.mix_rewarding(mix_id); + let delegation_post = delegations_storage::delegations() + .load(test.deps().storage, storage_key) + .unwrap(); + + assert_ne!( + delegation_pre.cumulative_reward_ratio, + delegation_post.cumulative_reward_ratio + ); + assert_eq!( + delegation_post.cumulative_reward_ratio, + rewarding.total_unit_reward + ); + + assert_eq!(rewarding.unique_delegations, 1); + assert_eq!(rewarding.delegates, expected_amount_dec); + + assert_eq!(delegation_post.amount.amount.u128(), expected_amount) + } + + #[test] + fn appropriately_updates_state_for_fresh_delegation() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", Some(100_000_000_000u128.into())); + let owner = "delegator"; + + let delegation = 120_000_000u128; + let delegation_coin = coin(120_000_000u128, TEST_COIN_DENOM); + + // perform some rewarding here to advance the unit delegation beyond the initial value + test.update_rewarded_set(vec![mix_id]); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let storage_key = + Delegation::generate_storage_key(mix_id, &Addr::unchecked(owner), None); + let delegation_pre = delegations_storage::delegations() + .may_load(test.deps().storage, storage_key.clone()) + .unwrap(); + let rewarding_pre = test.mix_rewarding(mix_id); + assert!(delegation_pre.is_none()); + assert!(rewarding_pre.delegates.is_zero()); + + let env = test.env(); + let res = delegate( + test.deps_mut(), + &env, + Addr::unchecked(owner), + mix_id, + delegation_coin.clone(), + None, + ) + .unwrap(); + assert!(get_bank_send_msg(&res).is_none()); + + let delegation_post = delegations_storage::delegations() + .load(test.deps().storage, storage_key) + .unwrap(); + let rewarding_post = test.mix_rewarding(mix_id); + assert_eq!(delegation_post.amount, delegation_coin); + assert_eq!( + delegation_post.cumulative_reward_ratio, + rewarding_post.total_unit_reward + ); + assert_eq!( + rewarding_post.delegates, + 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 owner1 = "delegator1"; + let owner2 = "delegator2"; + + let env = test.env(); + unbond_mixnode(test.deps_mut(), &env, mix_id).unwrap(); + + let vesting_contract = test.vesting_contract(); + let dummy_proxy = Addr::unchecked("not-vesting-contract"); + + // for a fresh delegation, nothing was added to the storage either + let res_vesting = delegate( + test.deps_mut(), + &env, + Addr::unchecked(owner1), + mix_id, + delegation_coin.clone(), + Some(vesting_contract.clone()), + ) + .unwrap(); + let storage_key = Delegation::generate_storage_key( + mix_id, + &Addr::unchecked(owner1), + 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: owner1.to_string(), + mix_id, + amount: delegation_coin.clone(), + }) + .unwrap(); + assert_eq!(&expected_msg, msg); + assert!(funds.is_empty()) + } + } + assert!(found_track); + + let res_other_proxy = delegate( + test.deps_mut(), + &env, + Addr::unchecked(owner1), + mix_id, + delegation_coin.clone(), + Some(dummy_proxy.clone()), + ) + .unwrap(); + let storage_key = Delegation::generate_storage_key( + mix_id, + &Addr::unchecked(owner2), + Some(dummy_proxy.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_other_proxy).unwrap(); + assert_eq!(receiver, dummy_proxy.as_str()); + assert_eq!(sent_amount[0], delegation_coin); + + // no track messages here! + for msg in &res_other_proxy.messages { + if let CosmosMsg::Wasm(WasmMsg::Execute { .. }) = &msg.msg { + panic!("we shouldn't have attempted to call any other contract!") + } + } + } + } + + #[cfg(test)] + mod undelegating { + use super::*; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use crate::support::tests::test_helpers::get_bank_send_msg; + use cosmwasm_std::{coin, to_binary, CosmosMsg, WasmMsg}; + use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; + + #[test] + fn doesnt_return_any_tokens_if_it_doesnt_exist() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let owner = Addr::unchecked("delegator"); + + let res = undelegate(test.deps_mut(), owner, mix_id, None).unwrap(); + assert!(get_bank_send_msg(&res).is_none()); + } + + #[test] + fn errors_out_if_mix_rewarding_doesnt_exist() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let owner = Addr::unchecked("delegator"); + test.add_immediate_delegation(owner.as_str(), 100_000_000u32, mix_id); + + // 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(), owner, mix_id, None); + assert!(matches!( + res, + Err(MixnetContractError::InconsistentState { .. }) + )); + } + + #[test] + fn returns_all_delegated_tokens_with_earned_rewards() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", Some(100_000_000_000u128.into())); + + let owner = "delegator"; + let delegation = 120_000_000u128; + + // perform some rewarding here to advance the unit delegation beyond the initial value + test.update_rewarded_set(vec![mix_id]); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + test.add_immediate_delegation(owner, delegation, mix_id); + + test.skip_to_next_epoch_end(); + let dist1 = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + test.skip_to_next_epoch_end(); + let dist2 = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let expected_reward = dist1.delegates + dist2.delegates; + let truncated_reward = truncate_reward_amount(expected_reward); + + let expected_return = delegation + truncated_reward.u128(); + + let res = undelegate(test.deps_mut(), Addr::unchecked(owner), mix_id, None).unwrap(); + let (receiver, sent_amount) = get_bank_send_msg(&res).unwrap(); + assert_eq!(receiver, owner); + assert_eq!(sent_amount[0].amount.u128(), expected_return); + + // make sure delegation no longer exists + let storage_key = + Delegation::generate_storage_key(mix_id, &Addr::unchecked(owner), None); + assert!(delegations_storage::delegations() + .may_load(test.deps().storage, storage_key) + .unwrap() + .is_none()); + + // and mix rewarding no longer contains any information about the delegation + let rewarding = test.mix_rewarding(mix_id); + 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 owner1 = "delegator1"; + let owner2 = "delegator2"; + + let vesting_contract = test.vesting_contract(); + let dummy_proxy = Addr::unchecked("not-vesting-contract"); + + test.add_immediate_delegation_with_proxy( + owner1, + delegation, + mix_id, + vesting_contract.clone(), + ); + test.add_immediate_delegation_with_proxy( + owner2, + delegation, + mix_id, + dummy_proxy.clone(), + ); + + // for a fresh delegation, nothing was added to the storage either + let res_vesting = undelegate( + test.deps_mut(), + Addr::unchecked(owner1), + mix_id, + Some(vesting_contract.clone()), + ) + .unwrap(); + let storage_key = Delegation::generate_storage_key( + mix_id, + &Addr::unchecked(owner1), + 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: owner1.to_string(), + mix_id, + amount: delegation_coin.clone(), + }) + .unwrap(); + assert_eq!(&expected_msg, msg); + assert!(funds.is_empty()) + } + } + assert!(found_track); + + let res_other_proxy = undelegate( + test.deps_mut(), + Addr::unchecked(owner2), + mix_id, + Some(dummy_proxy.clone()), + ) + .unwrap(); + let storage_key = Delegation::generate_storage_key( + mix_id, + &Addr::unchecked(owner2), + Some(dummy_proxy.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_other_proxy).unwrap(); + assert_eq!(receiver, dummy_proxy.as_str()); + assert_eq!(sent_amount[0], delegation_coin); + + // no track messages here! + for msg in &res_other_proxy.messages { + if let CosmosMsg::Wasm(WasmMsg::Execute { .. }) = &msg.msg { + panic!("we shouldn't have attempted to call any other contract!") + } + } + } + } + + #[cfg(test)] + mod mixnode_unbonding { + use super::*; + use crate::mixnodes::storage as mixnodes_storage; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use crate::support::tests::test_helpers::get_bank_send_msg; + use cosmwasm_std::{coin, to_binary, CosmosMsg, Uint128, WasmMsg}; + use mixnet_contract_common::mixnode::UnbondedMixnode; + use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; + + #[test] + fn returns_hard_error_if_mixnode_doesnt_exist() { + // this should have never happened so hard error MUST be thrown here + let mut test = TestSetup::new(); + let env = test.env(); + + let res = unbond_mixnode(test.deps_mut(), &env, 1); + assert!(matches!( + res, + Err(MixnetContractError::InconsistentState { .. }) + )); + } + + #[test] + fn returns_original_pledge_alongside_any_earned_rewards() { + let mut test = TestSetup::new(); + + let owner = "mix-owner"; + let pledge = Uint128::new(250_000_000); + let mix_id = test.add_dummy_mixnode(owner, Some(pledge)); + let mix_details = mixnodes_storage::mixnode_bonds() + .load(test.deps().storage, mix_id) + .unwrap(); + let layer = mix_details.layer; + + test.update_rewarded_set(vec![mix_id]); + test.skip_to_next_epoch_end(); + let dist1 = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + test.skip_to_next_epoch_end(); + let dist2 = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let expected_reward = dist1.operator + dist2.operator; + let truncated_reward = truncate_reward_amount(expected_reward); + let expected_return = pledge + truncated_reward; + + let env = test.env(); + let res = unbond_mixnode(test.deps_mut(), &env, mix_id).unwrap(); + let (receiver, sent_amount) = get_bank_send_msg(&res).unwrap(); + assert_eq!(receiver, owner); + assert_eq!(sent_amount[0].amount, expected_return); + + assert!(rewards_storage::MIXNODE_REWARDING + .may_load(test.deps().storage, mix_id) + .unwrap() + .is_none()); + assert!(mixnodes_storage::mixnode_bonds() + .may_load(test.deps().storage, mix_id) + .unwrap() + .is_none()); + let expected = UnbondedMixnode { + identity_key: mix_details.identity().to_string(), + owner: Addr::unchecked(owner), + proxy: None, + unbonding_height: env.block.height, + }; + assert_eq!( + expected, + mixnodes_storage::unbonded_mixnodes() + .load(test.deps().storage, mix_id) + .unwrap() + ); + assert_eq!( + mixnodes_storage::LAYERS.load(test.deps().storage).unwrap()[layer], + 0 + ) + } + + #[test] + fn attaches_vesting_contract_track_message_if_tokens_are_returned() { + let mut test = TestSetup::new(); + + let vesting_contract = test.vesting_contract(); + let dummy_proxy = Addr::unchecked("not-vesting-contract"); + + let pledge = Uint128::new(250_000_000); + let pledge_coin = coin(250_000_000, TEST_COIN_DENOM); + let owner1 = "mix-owner1"; + let owner2 = "mix-owner2"; + let mix_id_vesting = + test.add_dummy_mixnode_with_proxy(owner1, Some(pledge), vesting_contract.clone()); + let mix_id_other_proxy = + test.add_dummy_mixnode_with_proxy(owner2, Some(pledge), dummy_proxy.clone()); + + let env = test.env(); + let res_vesting = unbond_mixnode(test.deps_mut(), &env, 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_vesting).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_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::TrackUnbondMixnode { + owner: owner1.to_string(), + amount: pledge_coin.clone(), + }) + .unwrap(); + assert_eq!(&expected_msg, msg); + assert!(funds.is_empty()) + } + } + assert!(found_track); + + let res_other_proxy = + unbond_mixnode(test.deps_mut(), &env, mix_id_other_proxy).unwrap(); + assert!(mixnodes_storage::mixnode_bonds() + .may_load(test.deps().storage, mix_id_other_proxy) + .unwrap() + .is_none()); + // and all tokens are returned back to the proxy + let (receiver, sent_amount) = get_bank_send_msg(&res_other_proxy).unwrap(); + assert_eq!(receiver, dummy_proxy.as_str()); + assert_eq!(sent_amount[0], pledge_coin); + + // no track messages here! + for msg in &res_other_proxy.messages { + if let CosmosMsg::Wasm(WasmMsg::Execute { .. }) = &msg.msg { + panic!("we shouldn't have attempted to call any other contract!") + } + } + } + } + + #[test] + fn updating_active_set_updates_rewarding_params() { + let mut test = TestSetup::new(); + let current = rewards_storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap(); + + update_active_set_size(test.deps_mut(), 50).unwrap(); + let updated = rewards_storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap(); + assert_ne!(current.active_set_size, updated.active_set_size); + assert_eq!(updated.active_set_size, 50) + } + + #[cfg(test)] + mod changing_mix_cost_params { + use super::*; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use cosmwasm_std::coin; + use mixnet_contract_common::Percent; + + #[test] + fn doesnt_do_anything_if_mixnode_has_unbonded() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let env = test.env(); + unbond_mixnode(test.deps_mut(), &env, mix_id).unwrap(); + + let new_params = MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(42).unwrap(), + interval_operating_cost: coin(123_456_789, TEST_COIN_DENOM), + }; + + let res = change_mix_cost_params(test.deps_mut(), mix_id, new_params); + assert_eq!(res, Ok(Response::default())); + } + + #[test] + fn for_bonded_mixnode_updates_saved_value() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let before = test.mix_rewarding(mix_id).cost_params; + + let new_params = MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(42).unwrap(), + interval_operating_cost: coin(123_456_789, TEST_COIN_DENOM), + }; + + let res = change_mix_cost_params(test.deps_mut(), mix_id, new_params.clone()); + assert_eq!( + res, + Ok(Response::new() + .add_event(new_mixnode_cost_params_update_event(mix_id, &new_params))) + ); + + let after = test.mix_rewarding(mix_id).cost_params; + assert_ne!(before, new_params); + assert_eq!(after, new_params); + } + } + + #[test] + fn updating_interval_rewarding_params_appropriately_recomputes_state() { + let mut test = TestSetup::new(); + + let before = rewards_storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap(); + + let two = Decimal::from_atomics(2u32, 0).unwrap(); + let four = Decimal::from_atomics(4u32, 0).unwrap(); + + // TODO: be more fuzzy about it and try to vary other fields that can cause + // re-computation like pool emission or rewarded set size update + let update = IntervalRewardingParamsUpdate { + reward_pool: Some(before.interval.reward_pool / two), + staking_supply: Some(before.interval.staking_supply * four), + sybil_resistance_percent: Some(Percent::from_percentage_value(42).unwrap()), + active_set_work_factor: None, + interval_pool_emission: None, + rewarded_set_size: None, + }; + + let res = update_rewarding_params(test.deps_mut(), update); + assert!(res.is_ok()); + let after = rewards_storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap(); + + // with half the reward pool, our reward budget is also halved + assert_decimals( + before.interval.epoch_reward_budget, + two * after.interval.epoch_reward_budget, + ); + + // and with 4x the staking supply, the saturation point is also increased 4-folds + assert_decimals( + four * before.interval.stake_saturation_point, + after.interval.stake_saturation_point, + ); + + assert_eq!( + after.interval.sybil_resistance, + Percent::from_percentage_value(42).unwrap() + ) + } + + #[test] + fn updating_interval_config_recomputes_rewarding_params() { + let mut test = TestSetup::new(); + + let two = Decimal::from_atomics(2u32, 0).unwrap(); + + let params_before = rewards_storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap(); + + // skip few epochs just for the sake of it + test.skip_to_next_epoch(); + test.skip_to_next_epoch(); + test.skip_to_next_epoch(); + test.skip_to_next_epoch(); + test.skip_to_next_epoch(); + + let interval_before = + crate::interval::storage::current_interval(test.deps().storage).unwrap(); + + // half the number of epochs (thus double reward budget) + // and change epoch length + update_interval_config( + test.deps_mut(), + interval_before.epochs_in_interval() / 2, + 1234, + ) + .unwrap(); + + let interval_after = + crate::interval::storage::current_interval(test.deps().storage).unwrap(); + let params_after = rewards_storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap(); + assert_eq!( + interval_after.epochs_in_interval(), + interval_before.epochs_in_interval() / 2 + ); + assert_eq!( + params_after.interval.epoch_reward_budget, + params_before.interval.epoch_reward_budget * two + ); + assert_eq!(interval_after.epoch_length(), Duration::from_secs(1234)) + } +} diff --git a/contracts/mixnet/src/interval/queries.rs b/contracts/mixnet/src/interval/queries.rs index dfa58f5e23..14622ddd82 100644 --- a/contracts/mixnet/src/interval/queries.rs +++ b/contracts/mixnet/src/interval/queries.rs @@ -1,365 +1,521 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::storage; -use crate::error::ContractError; -use cosmwasm_std::{Env, Order, StdResult, Storage}; +use crate::constants::{ + EPOCH_EVENTS_DEFAULT_RETRIEVAL_LIMIT, EPOCH_EVENTS_MAX_RETRIEVAL_LIMIT, + INTERVAL_EVENTS_DEFAULT_RETRIEVAL_LIMIT, INTERVAL_EVENTS_MAX_RETRIEVAL_LIMIT, + REWARDED_SET_DEFAULT_RETRIEVAL_LIMIT, REWARDED_SET_MAX_RETRIEVAL_LIMIT, +}; +use crate::interval::storage; +use cosmwasm_std::{Deps, Env, Order, StdResult}; use cw_storage_plus::Bound; +use mixnet_contract_common::pending_events::{PendingEpochEvent, PendingIntervalEvent}; use mixnet_contract_common::{ - IdentityKey, Interval, PagedRewardedSetResponse, RewardedSetNodeStatus, - RewardedSetUpdateDetails, + CurrentIntervalResponse, EpochEventId, IntervalEventId, NodeId, PagedRewardedSetResponse, + PendingEpochEventsResponse, PendingIntervalEventsResponse, }; -pub fn query_current_epoch(storage: &dyn Storage) -> Result { - storage::current_epoch(storage) +pub fn query_current_interval_details( + deps: Deps<'_>, + env: Env, +) -> StdResult { + let interval = storage::current_interval(deps.storage)?; + + Ok(CurrentIntervalResponse::new(interval, env)) } -pub(crate) fn query_rewarded_set_refresh_minimum_blocks() -> u64 { - crate::constants::REWARDED_SET_REFRESH_BLOCKS -} +pub fn query_rewarded_set_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(REWARDED_SET_DEFAULT_RETRIEVAL_LIMIT) + .min(REWARDED_SET_MAX_RETRIEVAL_LIMIT) as usize; -// note: I have removed the `query_rewarded_set_for_interval`, because I don't think it's appropriate -// for the contract to go through so much data (i.e. all "rewarded" sets of particular interval) in one go. -// To achieve the same result, the client would have to instead first call `query_rewarded_set_heights_for_interval` -// to learn the heights used in given interval and then for each of them `query_rewarded_set` for that particular height. - -pub fn query_current_rewarded_set_height(storage: &dyn Storage) -> Result { - Ok(storage::CURRENT_REWARDED_SET_HEIGHT.load(storage)?) -} - -fn query_rewarded_set_at_height( - storage: &dyn Storage, - height: u64, - start_after: Option, - limit: u32, -) -> Result, ContractError> { let start = start_after.map(Bound::exclusive); - let rewarded_set = storage::REWARDED_SET - .prefix(height) - .range(storage, start, None, Order::Ascending) - .take(limit as usize) - .collect::>()?; - Ok(rewarded_set) + let nodes = storage::REWARDED_SET + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .collect::>>()?; + + let start_next_after = nodes.last().map(|node| node.0); + + Ok(PagedRewardedSetResponse { + nodes, + start_next_after, + }) } -pub fn query_rewarded_set( - storage: &dyn Storage, - height: Option, - start_after: Option, - limit: Option, -) -> Result { - let height = match height { - Some(height) => height, - None => query_current_rewarded_set_height(storage)?, - }; - let limit = limit - .unwrap_or(storage::REWARDED_NODE_DEFAULT_PAGE_LIMIT) - .min(storage::REWARDED_NODE_MAX_PAGE_LIMIT); - - // query for an additional element to determine paging requirements - let mut paged_result = query_rewarded_set_at_height(storage, height, start_after, limit + 1)?; - - if paged_result.len() > limit as usize { - paged_result.truncate(limit as usize); - Ok(PagedRewardedSetResponse { - start_next_after: paged_result.last().map(|res| res.0.clone()), - identities: paged_result, - at_height: height, - }) - } else { - Ok(PagedRewardedSetResponse { - identities: paged_result, - start_next_after: None, - at_height: height, - }) - } -} - -// this was all put together into the same query so that all information would be synced together -pub fn query_rewarded_set_update_details( +pub fn query_pending_epoch_events_paged( + deps: Deps<'_>, env: Env, - storage: &dyn Storage, -) -> Result { - Ok(RewardedSetUpdateDetails { - refresh_rate_blocks: query_rewarded_set_refresh_minimum_blocks(), - last_refreshed_block: query_current_rewarded_set_height(storage)?, - current_height: env.block.height, + start_after: Option, + limit: Option, +) -> StdResult { + let interval = storage::current_interval(deps.storage)?; + + let limit = limit + .unwrap_or(EPOCH_EVENTS_DEFAULT_RETRIEVAL_LIMIT) + .min(EPOCH_EVENTS_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let events = storage::PENDING_EPOCH_EVENTS + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|row| row.into())) + .collect::>>()?; + + let start_next_after = events.last().map(|event| event.id); + + Ok(PendingEpochEventsResponse { + seconds_until_executable: interval.secs_until_current_epoch_end(&env), + events, + start_next_after, + }) +} + +pub fn query_pending_interval_events_paged( + deps: Deps<'_>, + env: Env, + start_after: Option, + limit: Option, +) -> StdResult { + let interval = storage::current_interval(deps.storage)?; + + let limit = limit + .unwrap_or(INTERVAL_EVENTS_DEFAULT_RETRIEVAL_LIMIT) + .min(INTERVAL_EVENTS_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let events = storage::PENDING_INTERVAL_EVENTS + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|row| row.into())) + .collect::>>()?; + + let start_next_after = events.last().map(|event| event.id); + + Ok(PendingIntervalEventsResponse { + seconds_until_executable: interval.secs_until_current_interval_end(&env), + events, + start_next_after, }) } #[cfg(test)] mod tests { use super::*; - use crate::interval::storage::REWARDED_NODE_MAX_PAGE_LIMIT; - use crate::support::tests::test_helpers; - use cosmwasm_std::testing::mock_env; - - fn store_rewarded_nodes( - storage: &mut dyn Storage, - height: u64, - active_set: u32, - rewarded_set: u32, - ) -> Vec { - let identities = (0..rewarded_set) - .map(|i| format!("identity{:04}", i)) - .collect::>(); - storage::save_rewarded_set(storage, height, active_set, identities.clone()).unwrap(); - identities - } + use crate::support::tests::test_helpers::TestSetup; #[test] - fn querying_for_rewarded_set_at_height() { - let mut deps = test_helpers::init_contract(); + fn querying_for_current_interval_details() { + let mut test = TestSetup::new(); - // store some nodes - let identities1 = store_rewarded_nodes(deps.as_mut().storage, 1, 100, 200); - let identities2 = store_rewarded_nodes(deps.as_mut().storage, 2, 50, 200); - let identities3 = store_rewarded_nodes(deps.as_mut().storage, 3, 150, 200); - let identities4 = store_rewarded_nodes(deps.as_mut().storage, 4, 300, 500); - let identities5 = store_rewarded_nodes(deps.as_mut().storage, 5, 500, 500); + let interval = test.current_interval(); + let env = test.env(); + let res = query_current_interval_details(test.deps(), env.clone()).unwrap(); - // expected2 and 3 are basically sanity checks to ensure changing active set size (increase or decrease) - // doesn't affect the ordering + assert_eq!(res.interval, interval); + assert!(!res.is_current_interval_over); + assert!(!res.is_current_epoch_over); + assert_eq!(res.current_blocktime, env.block.time.seconds()); - let expected1 = identities1 - .into_iter() - .enumerate() - .map(|(i, identity)| { - if i < 100 { - (identity, RewardedSetNodeStatus::Active) - } else { - (identity, RewardedSetNodeStatus::Standby) - } - }) - .collect::>(); + test.skip_to_current_epoch_end(); + let interval = test.current_interval(); + let env = test.env(); + let res = query_current_interval_details(test.deps(), env.clone()).unwrap(); - assert_eq!( - expected1, - query_rewarded_set_at_height(deps.as_ref().storage, 1, None, 1000).unwrap() - ); + assert_eq!(res.interval, interval); + assert!(!res.is_current_interval_over); + assert!(res.is_current_epoch_over); + assert_eq!(res.current_blocktime, env.block.time.seconds()); - let expected2 = identities2 - .into_iter() - .enumerate() - .map(|(i, identity)| { - if i < 50 { - (identity, RewardedSetNodeStatus::Active) - } else { - (identity, RewardedSetNodeStatus::Standby) - } - }) - .collect::>(); + test.skip_to_current_interval_end(); + let interval = test.current_interval(); + let env = test.env(); + let res = query_current_interval_details(test.deps(), env.clone()).unwrap(); - assert_eq!( - expected2, - query_rewarded_set_at_height(deps.as_ref().storage, 2, None, 1000).unwrap() - ); - - let expected3 = identities3 - .into_iter() - .enumerate() - .map(|(i, identity)| { - if i < 150 { - (identity, RewardedSetNodeStatus::Active) - } else { - (identity, RewardedSetNodeStatus::Standby) - } - }) - .collect::>(); - - assert_eq!( - expected3, - query_rewarded_set_at_height(deps.as_ref().storage, 3, None, 1000).unwrap() - ); - - // check limit and paging - // active: 300, rewarded: 500 - let first_100 = identities4 - .iter() - .take(100) - .map(|identity| (identity.clone(), RewardedSetNodeStatus::Active)) - .collect::>(); - assert_eq!( - first_100, - query_rewarded_set_at_height(deps.as_ref().storage, 4, None, 100).unwrap() - ); - - let expected_single1 = vec![("identity0299".to_string(), RewardedSetNodeStatus::Active)]; - let expected_single2 = vec![("identity0300".to_string(), RewardedSetNodeStatus::Standby)]; - assert_eq!( - expected_single1, - query_rewarded_set_at_height( - deps.as_ref().storage, - 4, - Some("identity0298".to_string()), - 1 - ) - .unwrap() - ); - assert_eq!( - expected_single2, - query_rewarded_set_at_height( - deps.as_ref().storage, - 4, - Some("identity0299".to_string()), - 1 - ) - .unwrap() - ); - - let last_100 = identities4 - .iter() - .skip(400) - .map(|identity| (identity.clone(), RewardedSetNodeStatus::Standby)) - .collect::>(); - assert_eq!( - last_100, - query_rewarded_set_at_height( - deps.as_ref().storage, - 4, - Some("identity0399".to_string()), - 100 - ) - .unwrap() - ); - - // all nodes are in the active set - let expected5 = identities5 - .into_iter() - .map(|identity| (identity, RewardedSetNodeStatus::Active)) - .collect::>(); - - assert_eq!( - expected5, - query_rewarded_set_at_height(deps.as_ref().storage, 5, None, 1000).unwrap() - ); + assert_eq!(res.interval, interval); + assert!(res.is_current_interval_over); + assert!(res.is_current_epoch_over); + assert_eq!(res.current_blocktime, env.block.time.seconds()); } - #[test] - fn querying_for_rewarded_set() { - let mut deps = test_helpers::init_contract(); + #[cfg(test)] + mod rewarded_set { + use super::*; - let current_height = 123; - let other_height = 456; - let different_height = 789; + fn set_rewarded_set_to_n_nodes(test: &mut TestSetup, n: usize) { + let set = (1u32..).take(n).collect::>(); + test.update_rewarded_set(set) + } - storage::CURRENT_REWARDED_SET_HEIGHT - .save(deps.as_mut().storage, ¤t_height) + #[test] + fn obeys_limits() { + let mut test = TestSetup::new(); + set_rewarded_set_to_n_nodes(&mut test, 200); + + let limit = 2; + let page1 = query_rewarded_set_paged(test.deps(), None, Some(limit)).unwrap(); + assert_eq!(limit, page1.nodes.len() as u32); + } + + #[test] + fn has_default_limit() { + let mut test = TestSetup::new(); + set_rewarded_set_to_n_nodes(&mut test, 2000); + + // query without explicitly setting a limit + let page1 = query_rewarded_set_paged(test.deps(), None, None).unwrap(); + + assert_eq!( + REWARDED_SET_DEFAULT_RETRIEVAL_LIMIT, + page1.nodes.len() as u32 + ); + } + + #[test] + fn has_max_limit() { + let mut test = TestSetup::new(); + set_rewarded_set_to_n_nodes(&mut test, 2000); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 10000; + let page1 = query_rewarded_set_paged(test.deps(), None, Some(crazy_limit)).unwrap(); + + assert_eq!(REWARDED_SET_MAX_RETRIEVAL_LIMIT, page1.nodes.len() as u32); + } + + #[test] + fn pagination_works() { + let mut test = TestSetup::new(); + + set_rewarded_set_to_n_nodes(&mut test, 1); + + let per_page = 2; + let page1 = query_rewarded_set_paged(test.deps(), None, Some(per_page)).unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.nodes.len()); + + set_rewarded_set_to_n_nodes(&mut test, 2); + + // page1 should have 2 results on it + let page1 = query_rewarded_set_paged(test.deps(), None, Some(per_page)).unwrap(); + assert_eq!(2, page1.nodes.len()); + + set_rewarded_set_to_n_nodes(&mut test, 3); + + // page1 still has the same 2 results + let another_page1 = + query_rewarded_set_paged(test.deps(), None, Some(per_page)).unwrap(); + assert_eq!(2, another_page1.nodes.len()); + assert_eq!(page1, another_page1); + + // retrieving the next page should start after the last key on this page + let start_after = page1.start_next_after.unwrap(); + let page2 = + query_rewarded_set_paged(test.deps(), Some(start_after), Some(per_page)).unwrap(); + + assert_eq!(1, page2.nodes.len()); + + // save another one + set_rewarded_set_to_n_nodes(&mut test, 4); + + let page2 = + query_rewarded_set_paged(test.deps(), Some(start_after), Some(per_page)).unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.nodes.len()); + } + } + + #[cfg(test)] + mod pending_epoch_events { + use super::*; + use cosmwasm_std::Addr; + use mixnet_contract_common::pending_events::PendingEpochEventData; + use rand_chacha::rand_core::RngCore; + + fn push_n_dummy_epoch_actions(test: &mut TestSetup, n: usize) { + for _ in 0..n { + push_dummy_epoch_action(test) + } + } + + fn push_dummy_epoch_action(test: &mut TestSetup) { + let dummy_action = PendingEpochEventData::Undelegate { + owner: Addr::unchecked("foomp"), + mix_id: test.rng.next_u32(), + proxy: None, + }; + storage::push_new_epoch_event(test.deps_mut().storage, &dummy_action).unwrap(); + } + + #[test] + fn obeys_limits() { + let mut test = TestSetup::new(); + push_n_dummy_epoch_actions(&mut test, 100); + let env = test.env(); + + let limit = 2; + + let page1 = + query_pending_epoch_events_paged(test.deps(), env, None, Some(limit)).unwrap(); + assert_eq!(limit, page1.events.len() as u32); + } + + #[test] + fn has_default_limit() { + let mut test = TestSetup::new(); + push_n_dummy_epoch_actions(&mut test, 1000); + let env = test.env(); + + // query without explicitly setting a limit + let page1 = query_pending_epoch_events_paged(test.deps(), env, None, None).unwrap(); + + assert_eq!( + EPOCH_EVENTS_DEFAULT_RETRIEVAL_LIMIT, + page1.events.len() as u32 + ); + } + + #[test] + fn has_max_limit() { + let mut test = TestSetup::new(); + push_n_dummy_epoch_actions(&mut test, 1000); + let env = test.env(); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 10000; + let page1 = query_pending_epoch_events_paged(test.deps(), env, None, Some(crazy_limit)) + .unwrap(); + + assert_eq!(EPOCH_EVENTS_MAX_RETRIEVAL_LIMIT, page1.events.len() as u32); + } + + #[test] + fn pagination_works() { + let mut test = TestSetup::new(); + let env = test.env(); + push_dummy_epoch_action(&mut test); + + let per_page = 2; + let page1 = + query_pending_epoch_events_paged(test.deps(), env.clone(), None, Some(per_page)) + .unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.events.len()); + + // save another + push_dummy_epoch_action(&mut test); + + // page1 should have 2 results on it + let page1 = + query_pending_epoch_events_paged(test.deps(), env.clone(), None, Some(per_page)) + .unwrap(); + assert_eq!(2, page1.events.len()); + + push_dummy_epoch_action(&mut test); + + // page1 still has the same 2 results + let another_page1 = + query_pending_epoch_events_paged(test.deps(), env.clone(), None, Some(per_page)) + .unwrap(); + assert_eq!(2, another_page1.events.len()); + assert_eq!(page1, another_page1); + + // retrieving the next page should start after the last key on this page + let start_after = page1.start_next_after.unwrap(); + let page2 = query_pending_epoch_events_paged( + test.deps(), + env.clone(), + Some(start_after), + Some(per_page), + ) .unwrap(); - let identities1 = store_rewarded_nodes(deps.as_mut().storage, current_height, 50, 100); - let identities2 = store_rewarded_nodes(deps.as_mut().storage, other_height, 100, 200); - let identities3 = store_rewarded_nodes( - deps.as_mut().storage, - different_height, - storage::REWARDED_NODE_MAX_PAGE_LIMIT, - storage::REWARDED_NODE_MAX_PAGE_LIMIT * 2, - ); + assert_eq!(1, page2.events.len()); - // if height is not set, current height is used, else it's just passed - let expected1 = PagedRewardedSetResponse { - identities: identities1 - .into_iter() - .enumerate() - .map(|(i, identity)| { - if i < 50 { - (identity, RewardedSetNodeStatus::Active) - } else { - (identity, RewardedSetNodeStatus::Standby) - } - }) - .collect::>(), - start_next_after: None, - at_height: current_height, - }; - let expected2 = PagedRewardedSetResponse { - identities: identities2 - .into_iter() - .enumerate() - .map(|(i, identity)| { - if i < 100 { - (identity, RewardedSetNodeStatus::Active) - } else { - (identity, RewardedSetNodeStatus::Standby) - } - }) - .collect::>(), - start_next_after: None, - at_height: other_height, - }; + // save another one + push_dummy_epoch_action(&mut test); - assert_eq!( - Ok(expected1), - query_rewarded_set(deps.as_ref().storage, None, None, None) - ); - assert_eq!( - Ok(expected2), - query_rewarded_set(deps.as_ref().storage, Some(other_height), None, None) - ); - - // if limit is not set, a default one is used instead - let expected3 = PagedRewardedSetResponse { - identities: identities3 - .iter() - .take(storage::REWARDED_NODE_DEFAULT_PAGE_LIMIT as usize) - .cloned() - .map(|identity| (identity, RewardedSetNodeStatus::Active)) - .collect::>(), - start_next_after: Some(format!( - "identity{:04}", - storage::REWARDED_NODE_DEFAULT_PAGE_LIMIT - 1 - )), - at_height: different_height, - }; - assert_eq!( - Ok(expected3), - query_rewarded_set(deps.as_ref().storage, Some(different_height), None, None) - ); - - // limit cannot be larger that pre-defined maximum - let expected4 = PagedRewardedSetResponse { - identities: identities3 - .iter() - .take(storage::REWARDED_NODE_MAX_PAGE_LIMIT as usize) - .cloned() - .map(|identity| (identity, RewardedSetNodeStatus::Active)) - .collect::>(), - start_next_after: Some(format!( - "identity{:04}", - storage::REWARDED_NODE_MAX_PAGE_LIMIT - 1 - )), - at_height: different_height, - }; - assert_eq!( - Ok(expected4), - query_rewarded_set( - deps.as_ref().storage, - Some(different_height), - None, - Some(REWARDED_NODE_MAX_PAGE_LIMIT * 100) + let page2 = query_pending_epoch_events_paged( + test.deps(), + env, + Some(start_after), + Some(per_page), ) - ); - } - - #[test] - fn querying_for_rewarded_set_update_details() { - let env = mock_env(); - let mut deps = test_helpers::init_contract(); - - let current_height = 123; - storage::CURRENT_REWARDED_SET_HEIGHT - .save(deps.as_mut().storage, ¤t_height) .unwrap(); - // returns whatever is in the correct environment - assert_eq!( - RewardedSetUpdateDetails { - refresh_rate_blocks: crate::constants::REWARDED_SET_REFRESH_BLOCKS, - last_refreshed_block: current_height, - current_height: env.block.height - }, - query_rewarded_set_update_details(env, deps.as_ref().storage).unwrap() - ) + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.events.len()); + } + + #[test] + fn shows_correct_time_until_possible_execution() { + let mut test = TestSetup::new(); + let env = test.env(); + push_dummy_epoch_action(&mut test); + + let res = + query_pending_epoch_events_paged(test.deps(), env.clone(), None, None).unwrap(); + let interval = test.current_interval(); + + // it's essentially always the time until the epoch end + assert_eq!( + res.seconds_until_executable, + interval.secs_until_current_epoch_end(&env) + ) + } + } + + #[cfg(test)] + mod pending_interval_events { + use super::*; + use crate::support::tests::fixtures; + use mixnet_contract_common::pending_events::PendingIntervalEventData; + use rand_chacha::rand_core::RngCore; + + fn push_n_dummy_interval_actions(test: &mut TestSetup, n: usize) { + for _ in 0..n { + push_dummy_interval_action(test) + } + } + + fn push_dummy_interval_action(test: &mut TestSetup) { + let dummy_action = PendingIntervalEventData::ChangeMixCostParams { + mix_id: test.rng.next_u32(), + new_costs: fixtures::mix_node_cost_params_fixture(), + }; + storage::push_new_interval_event(test.deps_mut().storage, &dummy_action).unwrap(); + } + + #[test] + fn obeys_limits() { + let mut test = TestSetup::new(); + push_n_dummy_interval_actions(&mut test, 100); + let env = test.env(); + + let limit = 2; + + let page1 = + query_pending_interval_events_paged(test.deps(), env, None, Some(limit)).unwrap(); + assert_eq!(limit, page1.events.len() as u32); + } + + #[test] + fn has_default_limit() { + let mut test = TestSetup::new(); + push_n_dummy_interval_actions(&mut test, 1000); + let env = test.env(); + + // query without explicitly setting a limit + let page1 = query_pending_interval_events_paged(test.deps(), env, None, None).unwrap(); + + assert_eq!( + INTERVAL_EVENTS_DEFAULT_RETRIEVAL_LIMIT, + page1.events.len() as u32 + ); + } + + #[test] + fn has_max_limit() { + let mut test = TestSetup::new(); + push_n_dummy_interval_actions(&mut test, 1000); + let env = test.env(); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 10000; + let page1 = + query_pending_interval_events_paged(test.deps(), env, None, Some(crazy_limit)) + .unwrap(); + + assert_eq!( + INTERVAL_EVENTS_MAX_RETRIEVAL_LIMIT, + page1.events.len() as u32 + ); + } + + #[test] + fn pagination_works() { + let mut test = TestSetup::new(); + let env = test.env(); + push_dummy_interval_action(&mut test); + + let per_page = 2; + let page1 = + query_pending_interval_events_paged(test.deps(), env.clone(), None, Some(per_page)) + .unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.events.len()); + + // save another + push_dummy_interval_action(&mut test); + + // page1 should have 2 results on it + let page1 = + query_pending_interval_events_paged(test.deps(), env.clone(), None, Some(per_page)) + .unwrap(); + assert_eq!(2, page1.events.len()); + + push_dummy_interval_action(&mut test); + + // page1 still has the same 2 results + let another_page1 = + query_pending_interval_events_paged(test.deps(), env.clone(), None, Some(per_page)) + .unwrap(); + assert_eq!(2, another_page1.events.len()); + assert_eq!(page1, another_page1); + + // retrieving the next page should start after the last key on this page + let start_after = page1.start_next_after.unwrap(); + let page2 = query_pending_interval_events_paged( + test.deps(), + env.clone(), + Some(start_after), + Some(per_page), + ) + .unwrap(); + + assert_eq!(1, page2.events.len()); + + // save another one + push_dummy_interval_action(&mut test); + + let page2 = query_pending_interval_events_paged( + test.deps(), + env, + Some(start_after), + Some(per_page), + ) + .unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.events.len()); + } + + #[test] + fn shows_correct_time_until_possible_execution() { + let mut test = TestSetup::new(); + let env = test.env(); + push_dummy_interval_action(&mut test); + + let res = + query_pending_interval_events_paged(test.deps(), env.clone(), None, None).unwrap(); + let interval = test.current_interval(); + + // it's essentially always the time until the interval end + assert_eq!( + res.seconds_until_executable, + interval.secs_until_current_interval_end(&env) + ) + } } } diff --git a/contracts/mixnet/src/interval/storage.rs b/contracts/mixnet/src/interval/storage.rs index 31f506d33b..733da8c3d0 100644 --- a/contracts/mixnet/src/interval/storage.rs +++ b/contracts/mixnet/src/interval/storage.rs @@ -1,73 +1,103 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{StdResult, Storage}; -use cw_storage_plus::{Item, Map}; -use mixnet_contract_common::{ - reward_params::EpochRewardParams, IdentityKey, Interval, RewardedSetNodeStatus, +use crate::constants::{ + CURRENT_INTERVAL_KEY, EPOCH_EVENT_ID_COUNTER_KEY, INTERVAL_EVENT_ID_COUNTER_KEY, + LAST_EPOCH_EVENT_ID_KEY, LAST_INTERVAL_EVENT_ID_KEY, PENDING_EPOCH_EVENTS_NAMESPACE, + PENDING_INTERVAL_EVENTS_NAMESPACE, REWARDED_SET_KEY, }; +use cosmwasm_std::{Order, StdResult, Storage}; +use cw_storage_plus::{Item, Map}; +use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData}; +use mixnet_contract_common::{ + EpochEventId, Interval, IntervalEventId, NodeId, RewardedSetNodeStatus, +}; +use std::collections::HashMap; -use crate::{error::ContractError, support::helpers::epoch_reward_params}; +pub(crate) const CURRENT_INTERVAL: Item<'_, Interval> = Item::new(CURRENT_INTERVAL_KEY); +pub(crate) const REWARDED_SET: Map = Map::new(REWARDED_SET_KEY); -// type aliases for better reasoning for storage keys -// (I found it helpful) -type BlockHeight = u64; -type IntervalId = u32; +pub(crate) const EPOCH_EVENT_ID_COUNTER: Item = Item::new(EPOCH_EVENT_ID_COUNTER_KEY); +pub(crate) const INTERVAL_EVENT_ID_COUNTER: Item = + Item::new(INTERVAL_EVENT_ID_COUNTER_KEY); -// TODO: those values need to be verified -pub(crate) const REWARDED_NODE_DEFAULT_PAGE_LIMIT: u32 = 1000; -pub(crate) const REWARDED_NODE_MAX_PAGE_LIMIT: u32 = 1500; +pub(crate) const LAST_PROCESSED_EPOCH_EVENT: Item = + Item::new(LAST_EPOCH_EVENT_ID_KEY); +pub(crate) const LAST_PROCESSED_INTERVAL_EVENT: Item = + Item::new(LAST_INTERVAL_EVENT_ID_KEY); -pub(crate) const CURRENT_EPOCH: Item<'_, Interval> = Item::new("ceph"); -pub(crate) const CURRENT_EPOCH_REWARD_PARAMS: Item<'_, EpochRewardParams> = Item::new("erp"); -pub(crate) const CURRENT_REWARDED_SET_HEIGHT: Item<'_, BlockHeight> = Item::new("crh"); +// we're indexing the events by an increasing ID so that we'd execute them in the order they were created +// (we can't use block height as it's very possible multiple requests might be created in the same block height, +// and composite keys would be more complex than just using an increasing ID) +/// Contains operations that should get resolved at the end of the current epoch. +pub(crate) const PENDING_EPOCH_EVENTS: Map = + Map::new(PENDING_EPOCH_EVENTS_NAMESPACE); -// I've changed the `()` data to an `u8` as after serializing `()` is represented as "null", -// taking more space than a single digit u8. If we don't care about what's there, why not go with more efficient approach? : ) -// pub(crate) const REWARDED_SET_HEIGHTS_FOR_INTERVAL: Map<'_, (IntervalId, BlockHeight), u8> = -// Map::new("rsh"); +/// Contains operations that should get resolved at the end of the current interval. +pub(crate) const PENDING_INTERVAL_EVENTS: Map = + Map::new(PENDING_INTERVAL_EVENTS_NAMESPACE); -// pub(crate) const REWARDED_SET: Map<(u64, IdentityKey), NodeStatus> = Map::new("rs"); -pub(crate) const REWARDED_SET: Map<'_, (BlockHeight, IdentityKey), RewardedSetNodeStatus> = - Map::new("rs"); - -pub(crate) const EPOCHS: Map<'_, IntervalId, Interval> = Map::new("ephs"); - -pub fn save_epoch(storage: &mut dyn Storage, epoch: &Interval) -> Result<(), ContractError> { - CURRENT_EPOCH.save(storage, epoch)?; - EPOCHS.save(storage, epoch.id(), epoch)?; - Ok(()) +pub(crate) fn current_interval(storage: &dyn Storage) -> StdResult { + CURRENT_INTERVAL.load(storage) } -pub fn current_epoch_reward_params( - storage: &dyn Storage, -) -> Result { - Ok(CURRENT_EPOCH_REWARD_PARAMS.load(storage)?) +pub(crate) fn save_interval(storage: &mut dyn Storage, interval: &Interval) -> StdResult<()> { + CURRENT_INTERVAL.save(storage, interval) } -pub fn save_epoch_reward_params( - epoch_id: u32, +pub(crate) fn next_epoch_event_id_counter(store: &mut dyn Storage) -> StdResult { + let id: EpochEventId = EPOCH_EVENT_ID_COUNTER.may_load(store)?.unwrap_or_default() + 1; + EPOCH_EVENT_ID_COUNTER.save(store, &id)?; + Ok(id) +} + +pub(crate) fn next_interval_event_id_counter( + store: &mut dyn Storage, +) -> StdResult { + let id: IntervalEventId = INTERVAL_EVENT_ID_COUNTER + .may_load(store)? + .unwrap_or_default() + + 1; + INTERVAL_EVENT_ID_COUNTER.save(store, &id)?; + Ok(id) +} + +pub(crate) fn push_new_epoch_event( storage: &mut dyn Storage, -) -> Result<(), ContractError> { - let epoch_reward_params = epoch_reward_params(storage)?; - CURRENT_EPOCH_REWARD_PARAMS.save(storage, &epoch_reward_params)?; - crate::rewards::storage::EPOCH_REWARD_PARAMS.save(storage, epoch_id, &epoch_reward_params)?; - Ok(()) -} - -pub fn current_epoch(storage: &dyn Storage) -> Result { - CURRENT_EPOCH - .load(storage) - .map_err(|_| ContractError::EpochNotInitialized) -} - -pub(crate) fn save_rewarded_set( - storage: &mut dyn Storage, - height: BlockHeight, - active_set_size: u32, - entries: Vec, + event: &PendingEpochEventData, ) -> StdResult<()> { - for (i, identity) in entries.into_iter().enumerate() { + let event_id = next_epoch_event_id_counter(storage)?; + PENDING_EPOCH_EVENTS.save(storage, event_id, event) +} + +pub(crate) fn push_new_interval_event( + storage: &mut dyn Storage, + event: &PendingIntervalEventData, +) -> StdResult<()> { + let event_id = next_interval_event_id_counter(storage)?; + PENDING_INTERVAL_EVENTS.save(storage, event_id, event) +} + +pub(crate) fn update_rewarded_set( + storage: &mut dyn Storage, + active_set_size: u32, + new_set: Vec, +) -> StdResult<()> { + // our goal is to reduce the number of reads and writes to the underlying storage, + // whilst completely overwriting the current rewarded set. + // the naive implementation would be to read the entire current rewarded set, + // remove all of those entries + // and write the new one in its place. + // However, very often it might turn out that a node hasn't changed its status in the updated epoch, + // and in those cases we can save on having to remove the entry and writing a new one. + + // Note: so far it seems the contract compiles (and stores) fine with a `HashMap`, but if we ever + // run into any issues due to any randomness? we can switch it up for a BTreeMap + let mut old_nodes = REWARDED_SET + .range(storage, None, None, Order::Ascending) + .collect::, _>>()?; + + for (i, node_id) in new_set.into_iter().enumerate() { // first k nodes are active let set_status = if i < active_set_size as usize { RewardedSetNodeStatus::Active @@ -75,47 +105,77 @@ pub(crate) fn save_rewarded_set( RewardedSetNodeStatus::Standby }; - REWARDED_SET.save(storage, (height, identity), &set_status)?; + if !matches!(old_nodes.get(&node_id), Some(status) if status == &set_status) { + // if the status changed, or didn't exist, write it down: + REWARDED_SET.save(storage, node_id, &set_status)?; + } + + old_nodes.remove(&node_id); + } + + // finally remove the entries for nodes that no longer exist [in the rewarded set] + for old_node_id in old_nodes.keys() { + REWARDED_SET.remove(storage, *old_node_id) } Ok(()) } +pub(crate) fn initialise_storage( + storage: &mut dyn Storage, + starting_interval: Interval, +) -> StdResult<()> { + save_interval(storage, &starting_interval)?; + LAST_PROCESSED_EPOCH_EVENT.save(storage, &0)?; + LAST_PROCESSED_INTERVAL_EVENT.save(storage, &0)?; + EPOCH_EVENT_ID_COUNTER.save(storage, &0)?; + INTERVAL_EVENT_ID_COUNTER.save(storage, &0) +} + #[cfg(test)] mod tests { use super::*; - use crate::support::tests::test_helpers; + use cosmwasm_std::testing::mock_dependencies; + use cosmwasm_std::Order; + + fn read_entire_set(storage: &mut dyn Storage) -> HashMap { + REWARDED_SET + .range(storage, None, None, Order::Ascending) + .map(|r| r.unwrap()) + .collect() + } #[test] - fn saving_rewarded_set() { - let mut deps = test_helpers::init_contract(); + fn updating_rewarded_set() { + // just some variables to keep test assertions more concise + let active = &RewardedSetNodeStatus::Active; + let standby = &RewardedSetNodeStatus::Standby; - let active_set_size = 100; - let mut nodes = Vec::new(); - for i in 0..1000 { - nodes.push(format!("identity{:04}", i)) - } + let mut deps = mock_dependencies(); + let store = deps.as_mut().storage; + assert!(read_entire_set(store).is_empty()); - save_rewarded_set(deps.as_mut().storage, 1234, active_set_size, nodes).unwrap(); + // writing initial rewarded set shouldn't do anything fancy + update_rewarded_set(store, 2, vec![6, 2, 7, 4, 1]).unwrap(); + let current_set = read_entire_set(store); + assert_eq!(current_set.len(), 5); + assert_eq!(active, current_set.get(&6).unwrap()); + assert_eq!(active, current_set.get(&2).unwrap()); + assert_eq!(standby, current_set.get(&7).unwrap()); + assert_eq!(standby, current_set.get(&4).unwrap()); + assert_eq!(standby, current_set.get(&1).unwrap()); + assert!(current_set.get(&42).is_none()); - // first k nodes MUST BE active - for i in 0..1000 { - let identity = format!("identity{:04}", i); - if i < active_set_size { - assert_eq!( - RewardedSetNodeStatus::Active, - REWARDED_SET - .load(deps.as_ref().storage, (1234, identity)) - .unwrap() - ) - } else { - assert_eq!( - RewardedSetNodeStatus::Standby, - REWARDED_SET - .load(deps.as_ref().storage, (1234, identity)) - .unwrap() - ) - } - } + update_rewarded_set(store, 2, vec![2, 5, 6, 3, 4]).unwrap(); + let current_set = read_entire_set(store); + assert_eq!(current_set.len(), 5); + assert_eq!(active, current_set.get(&2).unwrap()); + assert_eq!(active, current_set.get(&5).unwrap()); + assert_eq!(standby, current_set.get(&6).unwrap()); + assert_eq!(standby, current_set.get(&3).unwrap()); + assert_eq!(standby, current_set.get(&4).unwrap()); + // those no longer are in the rewarded set + assert!(current_set.get(&7).is_none()); + assert!(current_set.get(&1).is_none()); } } diff --git a/contracts/mixnet/src/interval/transactions.rs b/contracts/mixnet/src/interval/transactions.rs index c9340846b7..3a6dd400df 100644 --- a/contracts/mixnet/src/interval/transactions.rs +++ b/contracts/mixnet/src/interval/transactions.rs @@ -2,271 +2,1492 @@ // SPDX-License-Identifier: Apache-2.0 use super::storage; -use crate::error::ContractError; -use crate::error::ContractError::EpochInProgress; -use crate::mixnet_contract_settings::storage as mixnet_params_storage; -use crate::support::helpers::is_authorized; +use crate::interval::helpers::change_interval_config; +use crate::interval::pending_events::ContractExecutableEvent; +use crate::interval::storage::push_new_interval_event; +use crate::rewards; +use crate::rewards::storage as rewards_storage; +use crate::support::helpers::{ensure_is_authorized, ensure_is_owner}; use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Storage}; -use mixnet_contract_common::events::{new_advance_interval_event, new_change_rewarded_set_event}; -use mixnet_contract_common::{IdentityKey, Interval}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::events::{ + new_advance_epoch_event, new_pending_epoch_events_execution_event, + new_pending_interval_config_update_event, new_pending_interval_events_execution_event, + new_reconcile_pending_events, +}; +use mixnet_contract_common::pending_events::PendingIntervalEventData; +use mixnet_contract_common::NodeId; +use std::collections::BTreeSet; -// We've distributed the rewards to the rewarded set from the validator api before making this call (implicit order, should be solved in the future) -// We now write the new rewarded set, snapshot the mixnodes and finally reconcile all delegations and undelegations. That way the rewards for the previous -// epoch will be calculated correctly as the delegations and undelegations from the previous epoch will only take effect in the next (current) one. -pub fn try_write_rewarded_set( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - rewarded_set: Vec, - active_set_size: u32, -) -> Result { - let state = mixnet_params_storage::CONTRACT_STATE.load(deps.storage)?; +// those two should be called in separate tx (from advancing epoch), +// since there might be a lot of events to execute. +// however, it should also be called when advancing epoch itself in case somebody +// managed to sneak in a transaction between those two operations +// (but then the amount of work is going to be minimal) +pub(crate) fn perform_pending_epoch_actions( + mut deps: DepsMut<'_>, + env: &Env, + limit: Option, +) -> Result<(Response, u32), MixnetContractError> { + let last_executed = storage::LAST_PROCESSED_EPOCH_EVENT.load(deps.storage)?; + let last_inserted = storage::EPOCH_EVENT_ID_COUNTER.load(deps.storage)?; - // check if this is executed by the permitted validator, if not reject the transaction - if info.sender != state.rewarding_validator_address { - return Err(ContractError::Unauthorized); + // no pending events + if last_executed == last_inserted { + return Ok((Response::new(), 0)); } - // We don't want more then we need, less should be fine, as we could have less nodes bonded overall - if active_set_size > state.params.mixnode_active_set_size { - return Err(ContractError::UnexpectedActiveSetSize { - received: active_set_size, - expected: state.params.mixnode_active_set_size, - }); + let pending = last_inserted - last_executed; + let last = limit + .map(|limit| { + if limit >= pending { + last_inserted + } else { + last_executed + limit + } + }) + .unwrap_or(last_inserted); + + let mut response = Response::new(); + // no need to use the [cosmwasm] range iterator as we know the exact keys in order + for event_id in last_executed + 1..=last { + let event = storage::PENDING_EPOCH_EVENTS.load(deps.storage, event_id)?; + let mut sub_response = event.execute(deps.branch(), env)?; + response.messages.append(&mut sub_response.messages); + response.attributes.append(&mut sub_response.attributes); + response.events.append(&mut sub_response.events); + // response.data.append(&mut sub_response.data); + + storage::PENDING_EPOCH_EVENTS.remove(deps.storage, event_id); } - if rewarded_set.len() as u32 > state.params.mixnode_rewarded_set_size { - return Err(ContractError::UnexpectedRewardedSetSize { - received: rewarded_set.len() as u32, - expected: state.params.mixnode_rewarded_set_size, - }); - } + storage::LAST_PROCESSED_EPOCH_EVENT.save(deps.storage, &last)?; - let block_height = env.block.height; - let num_nodes = rewarded_set.len(); - - storage::save_rewarded_set(deps.storage, block_height, active_set_size, rewarded_set)?; - storage::CURRENT_REWARDED_SET_HEIGHT.save(deps.storage, &block_height)?; - - Ok(Response::new().add_event(new_change_rewarded_set_event( - state.params.mixnode_active_set_size, - state.params.mixnode_rewarded_set_size, - num_nodes as u32, - ))) + Ok((response, last - last_executed)) } -pub fn try_init_epoch( - info: MessageInfo, +pub(crate) fn perform_pending_interval_actions( + mut deps: DepsMut<'_>, + env: &Env, + limit: Option, +) -> Result<(Response, u32), MixnetContractError> { + let last_executed = storage::LAST_PROCESSED_INTERVAL_EVENT.load(deps.storage)?; + let last_inserted = storage::INTERVAL_EVENT_ID_COUNTER.load(deps.storage)?; + + // no pending events + if last_executed == last_inserted { + return Ok((Response::new(), 0)); + } + + let pending = last_inserted - last_executed; + let last = limit + .map(|limit| { + if limit >= pending { + last_inserted + } else { + last_executed + limit + } + }) + .unwrap_or(last_inserted); + + let mut response = Response::new(); + // no need to use the [cosmwasm] range iterator as we know the exact keys in order + for event_id in last_executed + 1..=last { + let event = storage::PENDING_INTERVAL_EVENTS.load(deps.storage, event_id)?; + let mut sub_response = event.execute(deps.branch(), env)?; + response.messages.append(&mut sub_response.messages); + response.attributes.append(&mut sub_response.attributes); + response.events.append(&mut sub_response.events); + // response.data.append(&mut sub_response.data); + + storage::PENDING_INTERVAL_EVENTS.remove(deps.storage, event_id); + } + + storage::LAST_PROCESSED_INTERVAL_EVENT.save(deps.storage, &last)?; + + Ok((response, last - last_executed)) +} + +pub fn try_reconcile_epoch_events( + mut deps: DepsMut<'_>, + env: Env, + mut limit: Option, +) -> Result { + let mut response = Response::new().add_event(new_reconcile_pending_events()); + + // there's no need for authorization, as anyone willing to pay the fees should be allowed to reconcile + // contract events ASSUMING the corresponding epoch/interval has finished + let interval = storage::current_interval(deps.storage)?; + if !interval.is_current_epoch_over(&env) { + // if the current epoch is in progress, so must be the interval so there's no need to check that + return Err(MixnetContractError::EpochInProgress { + current_block_time: env.block.time.seconds(), + epoch_start: interval.current_epoch_start_unix_timestamp(), + epoch_end: interval.current_epoch_end_unix_timestamp(), + }); + } else { + let (mut sub_response, executed) = + perform_pending_epoch_actions(deps.branch(), &env, limit)?; + response.messages.append(&mut sub_response.messages); + response.attributes.append(&mut sub_response.attributes); + response.events.append(&mut sub_response.events); + response + .events + .push(new_pending_epoch_events_execution_event(executed)); + + limit = limit.map(|l| l - executed) + } + + if interval.is_current_interval_over(&env) { + // first clear epoch events queue and then touch the interval actions + let (mut sub_response, executed) = + perform_pending_interval_actions(deps.branch(), &env, limit)?; + response.messages.append(&mut sub_response.messages); + response.attributes.append(&mut sub_response.attributes); + response.events.append(&mut sub_response.events); + response + .events + .push(new_pending_interval_events_execution_event(executed)); + } + + Ok(response) +} + +fn update_rewarded_set( storage: &mut dyn Storage, - env: Env, -) -> Result { - is_authorized(info.sender.as_str().to_string(), storage)?; + new_rewarded_set: Vec, + expected_active_set_size: u32, +) -> Result<(), MixnetContractError> { + let reward_params = rewards_storage::REWARDING_PARAMS.load(storage)?; - init_epoch(storage, env)?; + // the rewarded set has been determined based off active set size taken from the contract, + // thus the expected value HAS TO match + if expected_active_set_size != reward_params.active_set_size { + return Err(MixnetContractError::UnexpectedActiveSetSize { + received: expected_active_set_size, + expected: reward_params.active_set_size, + }); + } - Ok(Response::default()) -} + if new_rewarded_set.len() as u32 > reward_params.rewarded_set_size { + return Err(MixnetContractError::UnexpectedRewardedSetSize { + received: new_rewarded_set.len() as u32, + expected: reward_params.rewarded_set_size, + }); + } -pub fn init_epoch(storage: &mut dyn Storage, env: Env) -> Result { - let epoch = Interval::init_epoch(env); - storage::save_epoch(storage, &epoch)?; - Ok(epoch) + // check for duplicates + let mut tmp_set = BTreeSet::new(); + for node_id in &new_rewarded_set { + if !tmp_set.insert(node_id) { + return Err(MixnetContractError::DuplicateRewardedSetNode { node_id: *node_id }); + } + } + + Ok(storage::update_rewarded_set( + storage, + expected_active_set_size, + new_rewarded_set, + )?) } pub fn try_advance_epoch( + mut deps: DepsMut<'_>, env: Env, - storage: &mut dyn Storage, - sender: String, -) -> Result { - // in theory, we could have just changed the state and relied on its reversal upon failed - // execution, but better safe than sorry and do not modify the state at all unless we know - // all checks have succeeded. - + info: MessageInfo, + new_rewarded_set: Vec, + expected_active_set_size: u32, +) -> Result { // Only rewarding validator can attempt to advance epoch + ensure_is_authorized(info.sender, deps.storage)?; - is_authorized(sender, storage)?; + let mut response = Response::new(); - let current_epoch = storage::current_epoch(storage)?; - if current_epoch.is_over(env.clone()) { - let next_epoch = current_epoch.next_on_chain(env); - - storage::save_epoch(storage, &next_epoch)?; - storage::save_epoch_reward_params(next_epoch.id(), storage)?; - - return Ok(Response::new().add_event(new_advance_interval_event(next_epoch))); + // we must make sure that we roll into new epoch / interval with up to date state + // with no pending actions (like somebody wanting to update their profit margin) + let current_interval = storage::current_interval(deps.storage)?; + if !current_interval.is_current_epoch_over(&env) { + return Err(MixnetContractError::EpochInProgress { + current_block_time: env.block.time.seconds(), + epoch_start: current_interval.current_epoch_start_unix_timestamp(), + epoch_end: current_interval.current_epoch_end_unix_timestamp(), + }); + } else { + let (mut sub_response, executed) = + perform_pending_epoch_actions(deps.branch(), &env, None)?; + response.messages.append(&mut sub_response.messages); + response.attributes.append(&mut sub_response.attributes); + response.events.append(&mut sub_response.events); + response + .events + .push(new_pending_epoch_events_execution_event(executed)); + } + + // first clear epoch events queue and then touch the interval actions + if current_interval.is_current_interval_over(&env) { + // the interval has finished -> we can change things such as the profit margin + let (mut sub_response, executed) = + perform_pending_interval_actions(deps.branch(), &env, None)?; + response.messages.append(&mut sub_response.messages); + response.attributes.append(&mut sub_response.attributes); + response.events.append(&mut sub_response.events); + response + .events + .push(new_pending_interval_events_execution_event(executed)); + + // this one is a very important one! + rewards::helpers::apply_reward_pool_changes(deps.storage)?; + } + + let updated_interval = current_interval.advance_epoch(); + let num_nodes = new_rewarded_set.len(); + + // finally save updated interval and the rewarded set + storage::save_interval(deps.storage, &updated_interval)?; + update_rewarded_set(deps.storage, new_rewarded_set, expected_active_set_size)?; + + Ok(response.add_event(new_advance_epoch_event(updated_interval, num_nodes as u32))) +} + +pub(crate) fn try_update_interval_config( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + epochs_in_interval: u32, + epoch_duration_secs: u64, + force_immediately: bool, +) -> Result { + ensure_is_owner(info.sender, deps.storage)?; + + let interval = storage::current_interval(deps.storage)?; + if force_immediately || interval.is_current_interval_over(&env) { + change_interval_config( + deps.storage, + interval, + epochs_in_interval, + epoch_duration_secs, + ) + } else { + // push the interval event + let interval_event = PendingIntervalEventData::UpdateIntervalConfig { + epochs_in_interval, + epoch_duration_secs, + }; + push_new_interval_event(deps.storage, &interval_event)?; + let time_left = interval.secs_until_current_interval_end(&env); + Ok( + Response::new().add_event(new_pending_interval_config_update_event( + epochs_in_interval, + epoch_duration_secs, + time_left, + )), + ) } - Err(EpochInProgress { - current_block_time: env.block.time.seconds(), - epoch_start: current_epoch.start_unix_timestamp(), - epoch_end: current_epoch.end_unix_timestamp(), - }) } #[cfg(test)] mod tests { use super::*; - use crate::support::tests::test_helpers; - use cosmwasm_std::testing::{mock_env, mock_info}; - use cosmwasm_std::Timestamp; - use mixnet_contract_common::RewardedSetNodeStatus; - use mixnet_params_storage::rewarding_validator_address; + use crate::support::tests::fixtures; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::Addr; + use mixnet_contract_common::pending_events::PendingEpochEventData; + use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; - #[test] - fn writing_rewarded_set() { - let mut env = mock_env(); - let mut deps = test_helpers::init_contract(); - let current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - let authorised_sender = mock_info(current_state.rewarding_validator_address.as_str(), &[]); - let full_rewarded_set = (0..current_state.params.mixnode_rewarded_set_size) - .map(|i| format!("identity{:04}", i)) - .collect::>(); - let last_update = 123; - storage::CURRENT_REWARDED_SET_HEIGHT - .save(deps.as_mut().storage, &last_update) - .unwrap(); - - // can only be performed by the permitted validator - let dummy_sender = mock_info("dummy_sender", &[]); - assert_eq!( - Err(ContractError::Unauthorized), - try_write_rewarded_set( - deps.as_mut(), - env.clone(), - dummy_sender, - full_rewarded_set.clone(), - current_state.params.mixnode_active_set_size - ) - ); - - // the sender must use the same active set size as the one defined in the contract - assert_eq!( - Err(ContractError::UnexpectedActiveSetSize { - received: 123, - expected: current_state.params.mixnode_active_set_size - }), - try_write_rewarded_set( - deps.as_mut(), - env.clone(), - authorised_sender.clone(), - full_rewarded_set.clone(), - 123 - ) - ); - - // the sender cannot provide more nodes than the rewarded set size - let mut bigger_set = full_rewarded_set.clone(); - bigger_set.push("another_node".to_string()); - assert_eq!( - Err(ContractError::UnexpectedRewardedSetSize { - received: current_state.params.mixnode_rewarded_set_size + 1, - expected: current_state.params.mixnode_rewarded_set_size - }), - try_write_rewarded_set( - deps.as_mut(), - env.clone(), - authorised_sender.clone(), - bigger_set, - current_state.params.mixnode_active_set_size - ) - ); - - // cannot be performed too soon after a previous update - env.block.height = last_update + 1; - // after successful rewarded set write, all internal storage structures are updated appropriately - env.block.height = last_update + crate::constants::REWARDED_SET_REFRESH_BLOCKS; - let expected_response = Response::new().add_event(new_change_rewarded_set_event( - current_state.params.mixnode_active_set_size, - current_state.params.mixnode_rewarded_set_size, - full_rewarded_set.len() as u32, - )); - - assert_eq!( - Ok(expected_response), - try_write_rewarded_set( - deps.as_mut(), - env.clone(), - authorised_sender, - full_rewarded_set.clone(), - current_state.params.mixnode_active_set_size - ) - ); - - for (i, rewarded_node) in full_rewarded_set.into_iter().enumerate() { - if (i as u32) < current_state.params.mixnode_active_set_size { - assert_eq!( - RewardedSetNodeStatus::Active, - storage::REWARDED_SET - .load(deps.as_ref().storage, (env.block.height, rewarded_node)) - .unwrap() - ) - } else { - assert_eq!( - RewardedSetNodeStatus::Standby, - storage::REWARDED_SET - .load(deps.as_ref().storage, (env.block.height, rewarded_node)) - .unwrap() - ) - } + 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 + for i in 0..n { + let dummy_action = PendingEpochEventData::Undelegate { + owner: Addr::unchecked("foomp"), + mix_id: i as NodeId, + proxy: None, + }; + storage::push_new_epoch_event(test.deps_mut().storage, &dummy_action).unwrap(); + } + } + + fn push_n_dummy_interval_actions(test: &mut TestSetup, n: usize) { + // if you attempt to update cost parameters of an unbonded mixnode, + // it will return an empty response, but will not fail + for i in 0..n { + let dummy_action = PendingIntervalEventData::ChangeMixCostParams { + mix_id: i as NodeId, + new_costs: fixtures::mix_node_cost_params_fixture(), + }; + storage::push_new_interval_event(test.deps_mut().storage, &dummy_action).unwrap(); + } + } + + #[cfg(test)] + mod performing_pending_epoch_actions { + use super::*; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use cosmwasm_std::{coin, coins, wasm_execute, Addr, BankMsg, Empty, SubMsg}; + use mixnet_contract_common::events::{ + new_active_set_update_event, new_delegation_on_unbonded_node_event, + new_undelegation_event, + }; + use mixnet_contract_common::pending_events::PendingEpochEventData; + + #[test] + fn without_limit_executes_all_actions() { + let mut test = TestSetup::new(); + assert_eq!( + 0, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + + let env = test.env(); + // no events are pending, nothing should get done + let (res, executed) = + perform_pending_epoch_actions(test.deps_mut(), &env, None).unwrap(); + assert_eq!(res, Response::new()); + assert_eq!(executed, 0); + assert_eq!( + 0, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + + push_n_dummy_epoch_actions(&mut test, 42); + let (res, executed) = + perform_pending_epoch_actions(test.deps_mut(), &env, None).unwrap(); + // dummy actions don't emit any events + assert_eq!(res, Response::new()); + assert_eq!(executed, 42); + assert_eq!( + 42, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + + push_n_dummy_epoch_actions(&mut test, 10); + let (res, executed) = + perform_pending_epoch_actions(test.deps_mut(), &env, None).unwrap(); + assert_eq!(res, Response::new()); + assert_eq!(executed, 10); + assert_eq!( + 52, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + + push_n_dummy_epoch_actions(&mut test, 1); + let (res, executed) = + perform_pending_epoch_actions(test.deps_mut(), &env, None).unwrap(); + assert_eq!(res, Response::new()); + assert_eq!(executed, 1); + assert_eq!( + 53, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + + push_n_dummy_epoch_actions(&mut test, 10); + let action_with_event = PendingEpochEventData::UpdateActiveSetSize { new_size: 50 }; + storage::push_new_epoch_event(test.deps_mut().storage, &action_with_event).unwrap(); + push_n_dummy_epoch_actions(&mut test, 10); + let (res, executed) = + perform_pending_epoch_actions(test.deps_mut(), &env, None).unwrap(); + assert_eq!( + res, + Response::new().add_event(new_active_set_update_event(50)) + ); + assert_eq!(executed, 21); + assert_eq!( + 74, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + } + + #[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); + let delegator = Addr::unchecked("delegator"); + let amount = 123_456_789u128; + test.add_immediate_delegation(delegator.as_str(), amount, legit_mix); + + let mut expected_events = Vec::new(); + let mut expected_messages: Vec> = Vec::new(); + + // delegate to node that doesn't exist, + // we expect to receive BankMsg with tokens being returned, + // and event regarding delegation + let non_existent_delegation = PendingEpochEventData::Delegate { + owner: Addr::unchecked("foomp"), + mix_id: 123, + amount: coin(123, TEST_COIN_DENOM), + proxy: None, + }; + storage::push_new_epoch_event(test.deps_mut().storage, &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 { + to_address: "foomp".to_string(), + 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 = PendingEpochEventData::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, &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 = PendingEpochEventData::UpdateActiveSetSize { new_size: 50 }; + storage::push_new_epoch_event(test.deps_mut().storage, &action_with_event).unwrap(); + expected_events.push(new_active_set_update_event(50)); + + // undelegation just returns tokens and emits event + let legit_undelegate = PendingEpochEventData::Undelegate { + owner: delegator.clone(), + mix_id: legit_mix, + proxy: None, + }; + storage::push_new_epoch_event(test.deps_mut().storage, &legit_undelegate).unwrap(); + expected_events.push(new_undelegation_event(&delegator, &None, legit_mix)); + expected_messages.push(SubMsg::new(BankMsg::Send { + to_address: delegator.into_string(), + amount: coins(amount, TEST_COIN_DENOM), + })); + + let (res, executed) = + perform_pending_epoch_actions(test.deps_mut(), &env, None).unwrap(); + let mut expected = Response::new().add_events(expected_events); + expected.messages = expected_messages; + assert_eq!(res, expected); + assert_eq!(executed, 4); + assert_eq!( + 4, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + } + + #[test] + fn respects_limit() { + let mut test = TestSetup::new(); + + let env = test.env(); + + push_n_dummy_epoch_actions(&mut test, 20); + + // no events are pending, nothing should get done + let (_, executed) = + perform_pending_epoch_actions(test.deps_mut(), &env, Some(0)).unwrap(); + assert_eq!(executed, 0); + assert_eq!( + 0, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + + let (_, executed) = + perform_pending_epoch_actions(test.deps_mut(), &env, Some(10)).unwrap(); + assert_eq!(executed, 10); + assert_eq!( + 10, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + + let (_, executed) = + perform_pending_epoch_actions(test.deps_mut(), &env, Some(10)).unwrap(); + assert_eq!(executed, 10); + assert_eq!( + 20, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + + push_n_dummy_epoch_actions(&mut test, 20); + let (_, executed) = + perform_pending_epoch_actions(test.deps_mut(), &env, Some(100)).unwrap(); + assert_eq!(executed, 20); + assert_eq!( + 40, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + } + } + + #[cfg(test)] + mod performing_pending_interval_actions { + use super::*; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use cosmwasm_std::{coin, Empty, SubMsg}; + use mixnet_contract_common::events::{ + new_interval_config_update_event, new_mixnode_cost_params_update_event, + new_rewarding_params_update_event, + }; + use mixnet_contract_common::mixnode::MixNodeCostParams; + use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate; + use mixnet_contract_common::Percent; + + #[test] + fn without_limit_executes_all_actions() { + let mut test = TestSetup::new(); + assert_eq!( + 0, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); + + let env = test.env(); + // no events are pending, nothing should get done + let (res, executed) = + perform_pending_interval_actions(test.deps_mut(), &env, None).unwrap(); + assert_eq!(res, Response::new()); + assert_eq!(executed, 0); + assert_eq!( + 0, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); + + push_n_dummy_interval_actions(&mut test, 42); + let (res, executed) = + perform_pending_interval_actions(test.deps_mut(), &env, None).unwrap(); + // dummy actions don't emit any events + assert_eq!(res, Response::new()); + assert_eq!(executed, 42); + assert_eq!( + 42, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); + + push_n_dummy_interval_actions(&mut test, 10); + let (res, executed) = + perform_pending_interval_actions(test.deps_mut(), &env, None).unwrap(); + assert_eq!(res, Response::new()); + assert_eq!(executed, 10); + assert_eq!( + 52, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); + + push_n_dummy_interval_actions(&mut test, 1); + let (res, executed) = + perform_pending_interval_actions(test.deps_mut(), &env, None).unwrap(); + assert_eq!(res, Response::new()); + assert_eq!(executed, 1); + assert_eq!( + 53, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); + + push_n_dummy_interval_actions(&mut test, 10); + let update = IntervalRewardingParamsUpdate { + rewarded_set_size: Some(500), + ..Default::default() + }; + let action_with_event = PendingIntervalEventData::UpdateRewardingParams { update }; + storage::push_new_interval_event(test.deps_mut().storage, &action_with_event).unwrap(); + push_n_dummy_interval_actions(&mut test, 10); + let (res, executed) = + perform_pending_interval_actions(test.deps_mut(), &env, None).unwrap(); + let updated_params = test.rewarding_params().interval; + assert_eq!( + res, + Response::new() + .add_event(new_rewarding_params_update_event(update, updated_params)) + ); + assert_eq!(executed, 21); + assert_eq!( + 74, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); + } + + #[test] + fn catches_all_events_and_messages_from_executed_actions() { + let mut test = TestSetup::new(); + let env = test.env(); + + let mut expected_events = Vec::new(); + let expected_messages: Vec> = Vec::new(); + + let legit_mix = test.add_dummy_mixnode("mix-owner", None); + let new_costs = MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(12).unwrap(), + interval_operating_cost: coin(123_000, TEST_COIN_DENOM), + }; + let cost_change = PendingIntervalEventData::ChangeMixCostParams { + mix_id: legit_mix, + new_costs: new_costs.clone(), + }; + + storage::push_new_interval_event(test.deps_mut().storage, &cost_change).unwrap(); + expected_events.push(new_mixnode_cost_params_update_event(legit_mix, &new_costs)); + + let update = IntervalRewardingParamsUpdate { + rewarded_set_size: Some(500), + ..Default::default() + }; + let change_params = PendingIntervalEventData::UpdateRewardingParams { update }; + storage::push_new_interval_event(test.deps_mut().storage, &change_params).unwrap(); + let interval = test.current_interval(); + let mut expected_updated = test.rewarding_params(); + expected_updated + .try_apply_updates(update, interval.epochs_in_interval()) + .unwrap(); + expected_events.push(new_rewarding_params_update_event( + update, + expected_updated.interval, + )); + + let change_interval = PendingIntervalEventData::UpdateIntervalConfig { + epochs_in_interval: 123, + epoch_duration_secs: 1000, + }; + let mut expected_updated2 = expected_updated; + expected_updated2.apply_epochs_in_interval_change(123); + storage::push_new_interval_event(test.deps_mut().storage, &change_interval).unwrap(); + expected_events.push(new_interval_config_update_event( + 123, + 1000, + expected_updated2.interval, + )); + + let (res, executed) = + perform_pending_interval_actions(test.deps_mut(), &env, None).unwrap(); + let mut expected = Response::new().add_events(expected_events); + expected.messages = expected_messages; + assert_eq!(res, expected); + assert_eq!(executed, 3); + assert_eq!( + 3, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); + } + + #[test] + fn respects_limit() { + let mut test = TestSetup::new(); + + let env = test.env(); + + push_n_dummy_interval_actions(&mut test, 20); + + // no events are pending, nothing should get done + let (_, executed) = + perform_pending_interval_actions(test.deps_mut(), &env, Some(0)).unwrap(); + assert_eq!(executed, 0); + assert_eq!( + 0, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); + + let (_, executed) = + perform_pending_interval_actions(test.deps_mut(), &env, Some(10)).unwrap(); + assert_eq!(executed, 10); + assert_eq!( + 10, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); + + let (_, executed) = + perform_pending_interval_actions(test.deps_mut(), &env, Some(10)).unwrap(); + assert_eq!(executed, 10); + assert_eq!( + 20, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); + + push_n_dummy_interval_actions(&mut test, 20); + let (_, executed) = + perform_pending_interval_actions(test.deps_mut(), &env, Some(100)).unwrap(); + assert_eq!(executed, 20); + assert_eq!( + 40, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); + } + } + + #[cfg(test)] + mod reconciling_epoch_events { + use super::*; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use cosmwasm_std::{coin, coins, BankMsg, Empty, SubMsg}; + use mixnet_contract_common::events::{ + new_delegation_on_unbonded_node_event, new_rewarding_params_update_event, + }; + use mixnet_contract_common::pending_events::PendingEpochEventData; + use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate; + + #[test] + fn returns_error_if_epoch_is_in_progress() { + let mut test = TestSetup::new(); + let env = test.env(); + + let res = try_reconcile_epoch_events(test.deps_mut(), env, None); + assert!(matches!( + res, + Err(MixnetContractError::EpochInProgress { .. }) + )) + } + + #[test] + fn only_clears_epoch_events_if_interval_is_in_progress() { + let mut test = TestSetup::new(); + + push_n_dummy_epoch_actions(&mut test, 10); + push_n_dummy_interval_actions(&mut test, 10); + test.skip_to_current_epoch_end(); + + let env = test.env(); + try_reconcile_epoch_events(test.deps_mut(), env, None).unwrap(); + + let epoch_events = test.pending_epoch_events(); + let interval_events = test.pending_interval_events(); + assert!(epoch_events.is_empty()); + assert_eq!(interval_events.len(), 10); + } + + #[test] + fn clears_both_epoch_and_interval_events_if_interval_has_finished() { + let mut test = TestSetup::new(); + + push_n_dummy_epoch_actions(&mut test, 10); + push_n_dummy_interval_actions(&mut test, 10); + test.skip_to_current_interval_end(); + + let env = test.env(); + try_reconcile_epoch_events(test.deps_mut(), env, None).unwrap(); + + let epoch_events = test.pending_epoch_events(); + let interval_events = test.pending_interval_events(); + assert!(epoch_events.is_empty()); + assert!(interval_events.is_empty()); + } + + #[test] + fn with_limit_prioritises_epoch_events() { + let mut test1 = TestSetup::new(); + let mut test2 = TestSetup::new(); + let mut test3 = TestSetup::new(); + let mut test4 = TestSetup::new(); + + for test in [&mut test1, &mut test2, &mut test3, &mut test4].iter_mut() { + push_n_dummy_epoch_actions(test, 10); + push_n_dummy_interval_actions(test, 10); + test.skip_to_current_interval_end(); + } + + let env = test1.env(); + + try_reconcile_epoch_events(test1.deps_mut(), env.clone(), Some(5)).unwrap(); + let epoch_events = test1.pending_epoch_events(); + let interval_events = test1.pending_interval_events(); + assert_eq!(epoch_events.len(), 5); + assert_eq!(interval_events.len(), 10); + + try_reconcile_epoch_events(test1.deps_mut(), env.clone(), Some(7)).unwrap(); + let epoch_events = test1.pending_epoch_events(); + let interval_events = test1.pending_interval_events(); + assert!(epoch_events.is_empty()); + assert_eq!(interval_events.len(), 8); + + try_reconcile_epoch_events(test1.deps_mut(), env.clone(), Some(7)).unwrap(); + let epoch_events = test1.pending_epoch_events(); + let interval_events = test1.pending_interval_events(); + assert!(epoch_events.is_empty()); + assert_eq!(interval_events.len(), 1); + + try_reconcile_epoch_events(test1.deps_mut(), env.clone(), Some(7)).unwrap(); + let epoch_events = test1.pending_epoch_events(); + let interval_events = test1.pending_interval_events(); + assert!(epoch_events.is_empty()); + assert!(interval_events.is_empty()); + + try_reconcile_epoch_events(test2.deps_mut(), env.clone(), Some(15)).unwrap(); + let epoch_events = test2.pending_epoch_events(); + let interval_events = test2.pending_interval_events(); + assert!(epoch_events.is_empty()); + assert_eq!(interval_events.len(), 5); + + try_reconcile_epoch_events(test2.deps_mut(), env.clone(), Some(15)).unwrap(); + let epoch_events = test2.pending_epoch_events(); + let interval_events = test2.pending_interval_events(); + assert!(epoch_events.is_empty()); + assert!(interval_events.is_empty()); + + try_reconcile_epoch_events(test3.deps_mut(), env.clone(), Some(20)).unwrap(); + let epoch_events = test3.pending_epoch_events(); + let interval_events = test3.pending_interval_events(); + assert!(epoch_events.is_empty()); + assert!(interval_events.is_empty()); + + try_reconcile_epoch_events(test4.deps_mut(), env, Some(100)).unwrap(); + let epoch_events = test4.pending_epoch_events(); + let interval_events = test4.pending_interval_events(); + assert!(epoch_events.is_empty()); + assert!(interval_events.is_empty()); + } + + #[test] + fn catches_all_emitted_cosmos_events_and_messages() { + let mut test = TestSetup::new(); + + let mut expected_events = vec![new_reconcile_pending_events()]; + let mut expected_messages: Vec> = Vec::new(); + + // epoch event + let non_existent_delegation = PendingEpochEventData::Delegate { + owner: Addr::unchecked("foomp"), + mix_id: 123, + amount: coin(123, TEST_COIN_DENOM), + proxy: None, + }; + storage::push_new_epoch_event(test.deps_mut().storage, &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 { + to_address: "foomp".to_string(), + amount: coins(123, TEST_COIN_DENOM), + })); + expected_events.push(new_pending_epoch_events_execution_event(1)); + + // interval event + let update = IntervalRewardingParamsUpdate { + rewarded_set_size: Some(500), + ..Default::default() + }; + let change_params = PendingIntervalEventData::UpdateRewardingParams { update }; + storage::push_new_interval_event(test.deps_mut().storage, &change_params).unwrap(); + let interval = test.current_interval(); + let mut expected_updated = test.rewarding_params(); + expected_updated + .try_apply_updates(update, interval.epochs_in_interval()) + .unwrap(); + expected_events.push(new_rewarding_params_update_event( + update, + expected_updated.interval, + )); + expected_events.push(new_pending_interval_events_execution_event(1)); + + test.skip_to_current_interval_end(); + let env = test.env(); + let res = try_reconcile_epoch_events(test.deps_mut(), env, None).unwrap(); + let mut expected = Response::new().add_events(expected_events); + expected.messages = expected_messages; + assert_eq!(res, expected); + assert_eq!( + 1, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + assert_eq!( + 1, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); } - assert_eq!( - env.block.height, - storage::CURRENT_REWARDED_SET_HEIGHT - .load(deps.as_ref().storage) - .unwrap() - ); } #[test] - fn advancing_epoch() { - let mut env = mock_env(); - let mut deps = test_helpers::init_contract(); - let sender = rewarding_validator_address(&deps.storage).unwrap(); + fn updating_rewarded_set() { + // the actual logic behind writing stuff to the storage has been tested in + // different unit test + let mut test = TestSetup::new(); + let current_active_set = test.rewarding_params().active_set_size; + let current_rewarded_set = test.rewarding_params().rewarded_set_size; - let _current_epoch = init_epoch(&mut deps.storage, env.clone()).unwrap(); - - // Works as its after the current epoch - env.block.time = Timestamp::from_seconds(1641081600); - assert!(try_advance_epoch(env.clone(), deps.as_mut().storage, sender.clone()).is_ok()); - - let current_epoch = crate::interval::storage::current_epoch(&mut deps.storage).unwrap(); - - // same if the current blocktime is set to BEFORE the first interval has even begun - // (say we decided to set the first interval to be some time in the future at initialisation) - env.block.time = Timestamp::from_seconds(1609459200); + // active set size has to match the expectation + let err = update_rewarded_set( + test.deps_mut().storage, + vec![1, 2, 3], + current_active_set - 10, + ) + .unwrap_err(); assert_eq!( - Err(ContractError::EpochInProgress { - current_block_time: 1609459200, - epoch_start: current_epoch.start_unix_timestamp(), - epoch_end: current_epoch.end_unix_timestamp() - }), - try_advance_epoch(env.clone(), deps.as_mut().storage, sender.clone(),) + err, + MixnetContractError::UnexpectedActiveSetSize { + received: current_active_set - 10, + expected: current_active_set, + } ); - // works otherwise + // number of nodes provided has to be equal or smaller than the current rewarded set size - // interval that has just finished - env.block.time = - Timestamp::from_seconds(current_epoch.start_unix_timestamp() as u64 + 10000); - let expected_new_epoch = current_epoch.next_on_chain(env.clone()); - let expected_response = - Response::new().add_event(new_advance_interval_event(expected_new_epoch)); + // fewer nodes + let res = update_rewarded_set(test.deps_mut().storage, vec![1, 2, 3], current_active_set); + assert!(res.is_ok()); + + let exact_num = (1u32..) + .take(current_rewarded_set as usize) + .collect::>(); + let res = update_rewarded_set(test.deps_mut().storage, exact_num, current_active_set); + assert!(res.is_ok()); + + // one more + let too_many = (1u32..) + .take((current_rewarded_set + 1) as usize) + .collect::>(); + let err = + update_rewarded_set(test.deps_mut().storage, too_many, current_active_set).unwrap_err(); assert_eq!( - Ok(expected_response), - try_advance_epoch(env.clone(), deps.as_mut().storage, sender) + err, + MixnetContractError::UnexpectedRewardedSetSize { + received: current_rewarded_set + 1, + expected: current_rewarded_set, + } ); - // interval way back in the past (i.e. 'somebody' failed to advance it for a long time) - env.block.time = Timestamp::from_seconds(1672531200); - storage::save_epoch(deps.as_mut().storage, ¤t_epoch).unwrap(); - let expected_new_epoch = current_epoch.next_on_chain(env.clone()); - let expected_response = - Response::new().add_event(new_advance_interval_event(expected_new_epoch)); - let sender = rewarding_validator_address(&deps.storage).unwrap(); + // doesn't allow for duplicates + let nodes_with_duplicate = vec![1, 2, 3, 4, 5, 1]; + let err = update_rewarded_set( + test.deps_mut().storage, + nodes_with_duplicate, + current_active_set, + ) + .unwrap_err(); assert_eq!( - Ok(expected_response), - try_advance_epoch(env.clone(), deps.as_mut().storage, sender) + err, + MixnetContractError::DuplicateRewardedSetNode { node_id: 1 } ); + let nodes_with_duplicate = vec![1, 2, 3, 5, 4, 5]; + let err = update_rewarded_set( + test.deps_mut().storage, + nodes_with_duplicate, + current_active_set, + ) + .unwrap_err(); + assert_eq!( + err, + MixnetContractError::DuplicateRewardedSetNode { node_id: 5 } + ); + } + + #[cfg(test)] + mod advancing_epoch { + use super::*; + use crate::rewards::models::RewardPoolChange; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::{coin, coins, BankMsg, Decimal, Empty, SubMsg}; + use mixnet_contract_common::events::{ + new_delegation_on_unbonded_node_event, new_rewarding_params_update_event, + }; + use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate; + use mixnet_contract_common::RewardedSetNodeStatus; + + #[test] + fn can_only_be_performed_by_specified_rewarding_validator() { + let mut test = TestSetup::new(); + let current_active_set = test.rewarding_params().active_set_size; + let some_sender = mock_info("foomper", &[]); + + test.skip_to_current_epoch_end(); + + let env = test.env(); + let res = try_advance_epoch( + test.deps_mut(), + env, + some_sender, + vec![1, 2, 3], + current_active_set, + ); + assert_eq!(res, Err(MixnetContractError::Unauthorized)); + + // good address (sanity check) + let env = test.env(); + let sender = test.rewarding_validator(); + let res = try_advance_epoch( + test.deps_mut(), + env, + sender, + vec![1, 2, 3], + current_active_set, + ); + assert!(res.is_ok()) + } + + #[test] + fn can_only_be_performed_if_epoch_is_over() { + let mut test = TestSetup::new(); + let current_active_set = test.rewarding_params().active_set_size; + + let env = test.env(); + let sender = test.rewarding_validator(); + let res = try_advance_epoch( + test.deps_mut(), + env, + sender.clone(), + vec![1, 2, 3], + current_active_set, + ); + assert!(matches!( + res, + Err(MixnetContractError::EpochInProgress { .. }) + )); + + // sanity check + test.skip_to_current_epoch_end(); + let env = test.env(); + let res = try_advance_epoch( + test.deps_mut(), + env, + sender, + vec![1, 2, 3], + current_active_set, + ); + assert!(res.is_ok()) + } + + #[test] + fn only_clears_epoch_events_if_interval_is_in_progress() { + let mut test = TestSetup::new(); + let current_active_set = test.rewarding_params().active_set_size; + + push_n_dummy_epoch_actions(&mut test, 10); + push_n_dummy_interval_actions(&mut test, 10); + test.skip_to_current_epoch_end(); + + let env = test.env(); + let sender = test.rewarding_validator(); + try_advance_epoch( + test.deps_mut(), + env, + sender, + vec![1, 2, 3], + current_active_set, + ) + .unwrap(); + + let epoch_events = test.pending_epoch_events(); + let interval_events = test.pending_interval_events(); + assert!(epoch_events.is_empty()); + assert_eq!(interval_events.len(), 10); + } + + #[test] + fn clears_both_epoch_and_interval_events_if_interval_has_finished() { + let mut test = TestSetup::new(); + let current_active_set = test.rewarding_params().active_set_size; + + push_n_dummy_epoch_actions(&mut test, 10); + push_n_dummy_interval_actions(&mut test, 10); + test.skip_to_current_interval_end(); + + let env = test.env(); + let sender = test.rewarding_validator(); + try_advance_epoch( + test.deps_mut(), + env, + sender, + vec![1, 2, 3], + current_active_set, + ) + .unwrap(); + + let epoch_events = test.pending_epoch_events(); + let interval_events = test.pending_interval_events(); + assert!(epoch_events.is_empty()); + assert!(interval_events.is_empty()); + } + + #[test] + fn if_executes_any_events_it_propagates_responses() { + let mut test = TestSetup::new(); + let current_active_set = test.rewarding_params().active_set_size; + + let mut expected_events = Vec::new(); + let mut expected_messages: Vec> = Vec::new(); + + let non_existent_delegation = PendingEpochEventData::Delegate { + owner: Addr::unchecked("foomp"), + mix_id: 123, + amount: coin(123, TEST_COIN_DENOM), + proxy: None, + }; + storage::push_new_epoch_event(test.deps_mut().storage, &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 { + to_address: "foomp".to_string(), + amount: coins(123, TEST_COIN_DENOM), + })); + expected_events.push(new_pending_epoch_events_execution_event(1)); + + // interval event + let update = IntervalRewardingParamsUpdate { + rewarded_set_size: Some(500), + ..Default::default() + }; + let change_params = PendingIntervalEventData::UpdateRewardingParams { update }; + storage::push_new_interval_event(test.deps_mut().storage, &change_params).unwrap(); + let interval = test.current_interval(); + let mut expected_updated = test.rewarding_params(); + expected_updated + .try_apply_updates(update, interval.epochs_in_interval()) + .unwrap(); + expected_events.push(new_rewarding_params_update_event( + update, + expected_updated.interval, + )); + expected_events.push(new_pending_interval_events_execution_event(1)); + let current_interval = test.current_interval(); + let expected = current_interval.advance_epoch(); + expected_events.push(new_advance_epoch_event(expected, 3)); + + test.skip_to_current_interval_end(); + + let env = test.env(); + let sender = test.rewarding_validator(); + let res = try_advance_epoch( + test.deps_mut(), + env, + sender, + vec![1, 2, 3], + current_active_set, + ) + .unwrap(); + + let mut expected = Response::new().add_events(expected_events); + expected.messages = expected_messages; + assert_eq!(res, expected); + assert_eq!( + 1, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + assert_eq!( + 1, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); + } + + #[test] + fn if_interval_is_over_applies_reward_pool_changes() { + let mut test = TestSetup::new(); + let current_active_set = test.rewarding_params().active_set_size; + + let start_params = test.rewarding_params(); + + let pool_update = Decimal::from_atomics(100_000_000u32, 0).unwrap(); + // push some changes + rewards_storage::PENDING_REWARD_POOL_CHANGE + .save( + test.deps_mut().storage, + &RewardPoolChange { + removed: pool_update, + added: Default::default(), + }, + ) + .unwrap(); + + // end of epoch - nothing has happened + let sender = test.rewarding_validator(); + test.skip_to_current_epoch_end(); + let env = test.env(); + try_advance_epoch( + test.deps_mut(), + env, + sender, + vec![1, 2, 3], + current_active_set, + ) + .unwrap(); + + let params = test.rewarding_params(); + let pool_change = rewards_storage::PENDING_REWARD_POOL_CHANGE + .load(test.deps().storage) + .unwrap(); + assert_eq!(params, start_params); + assert_eq!(pool_change.removed, pool_update); + + let sender = test.rewarding_validator(); + test.skip_to_current_interval_end(); + let env = test.env(); + try_advance_epoch( + test.deps_mut(), + env, + sender, + vec![1, 2, 3], + current_active_set, + ) + .unwrap(); + + let epochs_in_interval = test.current_interval().epochs_in_interval(); + let update = IntervalRewardingParamsUpdate { + reward_pool: Some(start_params.interval.reward_pool - pool_update), + staking_supply: Some(start_params.interval.staking_supply + pool_update), + ..Default::default() + }; + let mut expected = start_params; + expected + .try_apply_updates(update, epochs_in_interval) + .unwrap(); + + let params = test.rewarding_params(); + let pool_change = rewards_storage::PENDING_REWARD_POOL_CHANGE + .load(test.deps().storage) + .unwrap(); + assert_eq!(params, expected); + assert_eq!(pool_change.removed, Decimal::zero()); + } + + #[test] + fn updates_rewarded_set_and_interval_data() { + let mut test = TestSetup::new(); + let current_active_set = test.rewarding_params().active_set_size; + + let interval_pre = test.current_interval(); + let rewarded_set_pre = test.rewarded_set(); + assert!(rewarded_set_pre.is_empty()); + + let sender = test.rewarding_validator(); + test.skip_to_current_interval_end(); + let env = test.env(); + try_advance_epoch( + test.deps_mut(), + env, + sender, + vec![1, 2, 3], + current_active_set, + ) + .unwrap(); + + let interval_post = test.current_interval(); + let rewarded_set = test.rewarded_set(); + + let expected_id = interval_pre.current_epoch_absolute_id() + 1; + assert_eq!(interval_post.current_epoch_absolute_id(), expected_id); + assert_eq!( + rewarded_set, + vec![ + (1, RewardedSetNodeStatus::Active), + (2, RewardedSetNodeStatus::Active), + (3, RewardedSetNodeStatus::Active) + ] + ); + } + } + + #[cfg(test)] + mod updating_interval_config { + use super::*; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::Decimal; + use std::time::Duration; + + #[test] + fn can_only_be_done_by_contract_owner() { + let mut test = TestSetup::new(); + + let rewarding_validator = test.rewarding_validator(); + let owner = test.owner(); + let random = mock_info("random-guy", &[]); + + let env = test.env(); + let res = try_update_interval_config( + test.deps_mut(), + env.clone(), + rewarding_validator, + 100, + 1000, + false, + ); + assert_eq!(res, Err(MixnetContractError::Unauthorized)); + + let res = + try_update_interval_config(test.deps_mut(), env.clone(), random, 100, 1000, false); + assert_eq!(res, Err(MixnetContractError::Unauthorized)); + + let res = try_update_interval_config(test.deps_mut(), env, owner, 100, 1000, false); + assert!(res.is_ok()) + } + + #[test] + fn if_interval_is_finished_change_happens_immediately() { + let mut test = TestSetup::new(); + let owner = test.owner(); + + let old = test.rewarding_params(); + test.skip_to_current_interval_end(); + + let env = test.env(); + let res = + try_update_interval_config(test.deps_mut(), env, owner.clone(), 100, 1000, false); + assert!(res.is_ok()); + let new = test.rewarding_params(); + let interval = test.current_interval(); + assert_ne!(old, new); + assert_eq!(interval.epochs_in_interval(), 100); + + // sanity check for "normal" case + let mut test = TestSetup::new(); + let env = test.env(); + let res = try_update_interval_config(test.deps_mut(), env, owner, 100, 1000, false); + assert!(res.is_ok()); + let new = test.rewarding_params(); + assert_eq!(old, new); + } + + #[test] + fn if_update_is_forced_it_happens_immediately() { + let mut test = TestSetup::new(); + let owner = test.owner(); + + let old = test.rewarding_params(); + let env = test.env(); + let res = try_update_interval_config(test.deps_mut(), env, owner, 100, 1000, true); + assert!(res.is_ok()); + let new = test.rewarding_params(); + let interval = test.current_interval(); + assert_ne!(old, new); + assert_eq!(interval.epochs_in_interval(), 100); + } + + #[test] + fn without_forcing_it_change_happens_upon_clearing_interval_events() { + let mut test = TestSetup::new(); + let owner = test.owner(); + + let old = test.rewarding_params(); + let env = test.env(); + let res = try_update_interval_config(test.deps_mut(), env, owner, 100, 1000, false); + assert!(res.is_ok()); + let new = test.rewarding_params(); + assert_eq!(old, new); + + // make sure it's actually saved to pending events + let events = test.pending_interval_events(); + assert!(matches!(events[0], + PendingIntervalEventData::UpdateIntervalConfig { epochs_in_interval, epoch_duration_secs } if epochs_in_interval == 100 && epoch_duration_secs == 1000 + )); + + test.execute_all_pending_events(); + let new = test.rewarding_params(); + let interval = test.current_interval(); + assert_ne!(old, new); + assert_eq!(interval.epochs_in_interval(), 100); + } + + #[test] + fn upon_update_fields_are_recomputed_accordingly() { + let mut test = TestSetup::new(); + let owner = test.owner(); + let two = Decimal::from_atomics(2u32, 0).unwrap(); + + let params_before = test.rewarding_params(); + + // skip few epochs just for the sake of it + test.skip_to_next_epoch(); + test.skip_to_next_epoch(); + test.skip_to_next_epoch(); + test.skip_to_next_epoch(); + test.skip_to_next_epoch(); + + let interval_before = + crate::interval::storage::current_interval(test.deps().storage).unwrap(); + + // half the number of epochs (thus double reward budget) + // and change epoch length + let env = test.env(); + try_update_interval_config( + test.deps_mut(), + env, + owner, + interval_before.epochs_in_interval() / 2, + 1234, + true, + ) + .unwrap(); + + let interval_after = + crate::interval::storage::current_interval(test.deps().storage).unwrap(); + let params_after = rewards_storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap(); + assert_eq!( + interval_after.epochs_in_interval(), + interval_before.epochs_in_interval() / 2 + ); + assert_eq!( + params_after.interval.epoch_reward_budget, + params_before.interval.epoch_reward_budget * two + ); + assert_eq!(interval_after.epoch_length(), Duration::from_secs(1234)) + } } } diff --git a/contracts/mixnet/src/lib.rs b/contracts/mixnet/src/lib.rs index 5b0d0c8d0c..418b2c7ab5 100644 --- a/contracts/mixnet/src/lib.rs +++ b/contracts/mixnet/src/lib.rs @@ -4,11 +4,9 @@ mod constants; pub mod contract; mod delegations; -mod error; mod gateways; mod interval; mod mixnet_contract_settings; mod mixnodes; -mod queued_migrations; mod rewards; mod support; diff --git a/contracts/mixnet/src/mixnet_contract_settings/mod.rs b/contracts/mixnet/src/mixnet_contract_settings/mod.rs index 9898f4e305..3bb874a2ec 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/mod.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/mod.rs @@ -1,7 +1,6 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -pub mod models; pub mod queries; pub mod storage; pub mod transactions; diff --git a/contracts/mixnet/src/mixnet_contract_settings/models.rs b/contracts/mixnet/src/mixnet_contract_settings/models.rs deleted file mode 100644 index d271972be7..0000000000 --- a/contracts/mixnet/src/mixnet_contract_settings/models.rs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use cosmwasm_std::Addr; -use mixnet_contract_common::ContractStateParams; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] -pub struct ContractState { - pub owner: Addr, // only the owner account can update state - pub mix_denom: String, - pub rewarding_validator_address: Addr, - pub params: ContractStateParams, -} diff --git a/contracts/mixnet/src/mixnet_contract_settings/queries.rs b/contracts/mixnet/src/mixnet_contract_settings/queries.rs index 845be0dc67..1181c87d2f 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/queries.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/queries.rs @@ -1,27 +1,31 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use super::storage; use cosmwasm_std::{Deps, StdResult}; -use mixnet_contract_common::{ContractStateParams, MixnetContractVersion}; +use mixnet_contract_common::{ContractBuildInformation, ContractState, ContractStateParams}; -pub fn query_contract_settings_params(deps: Deps<'_>) -> StdResult { +pub(crate) fn query_contract_state(deps: Deps<'_>) -> StdResult { + storage::CONTRACT_STATE.load(deps.storage) +} + +pub(crate) fn query_contract_settings_params(deps: Deps<'_>) -> StdResult { storage::CONTRACT_STATE .load(deps.storage) .map(|settings| settings.params) } -pub fn query_rewarding_validator_address(deps: Deps<'_>) -> StdResult { +pub(crate) fn query_rewarding_validator_address(deps: Deps<'_>) -> StdResult { storage::CONTRACT_STATE .load(deps.storage) .map(|settings| settings.rewarding_validator_address.to_string()) } -pub(crate) fn query_contract_version() -> MixnetContractVersion { +pub(crate) fn query_contract_version() -> ContractBuildInformation { // as per docs // env! macro will expand to the value of the named environment variable at // compile time, yielding an expression of type `&'static str` - MixnetContractVersion { + ContractBuildInformation { build_timestamp: env!("VERGEN_BUILD_TIMESTAMP").to_string(), build_version: env!("VERGEN_BUILD_SEMVER").to_string(), commit_sha: env!("VERGEN_GIT_SHA").to_string(), @@ -34,10 +38,8 @@ pub(crate) fn query_contract_version() -> MixnetContractVersion { #[cfg(test)] pub(crate) mod tests { use super::*; - use crate::mixnet_contract_settings::models::ContractState; use crate::support::tests::test_helpers; - - use cosmwasm_std::Addr; + use cosmwasm_std::{coin, Addr}; #[test] fn query_for_contract_settings_works() { @@ -45,14 +47,13 @@ pub(crate) mod tests { let dummy_state = ContractState { owner: Addr::unchecked("someowner"), - mix_denom: String::from("unym"), rewarding_validator_address: Addr::unchecked("monitor"), + vesting_contract_address: Addr::unchecked("foomp"), + rewarding_denom: "unym".to_string(), params: ContractStateParams { - minimum_mixnode_pledge: 123u128.into(), - minimum_gateway_pledge: 456u128.into(), - mixnode_rewarded_set_size: 1000, - mixnode_active_set_size: 500, - staking_supply: 1000000u128.into(), + minimum_mixnode_delegation: None, + minimum_mixnode_pledge: coin(123u128, "unym"), + minimum_gateway_pledge: coin(456u128, "unym"), }, }; diff --git a/contracts/mixnet/src/mixnet_contract_settings/storage.rs b/contracts/mixnet/src/mixnet_contract_settings/storage.rs index 5f4c736d13..990c5e604d 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/storage.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/storage.rs @@ -1,69 +1,58 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::mixnet_contract_settings::models::ContractState; -use cosmwasm_std::StdResult; -use cosmwasm_std::Storage; +use crate::constants::CONTRACT_STATE_KEY; +use cosmwasm_std::{Addr, Storage}; +use cosmwasm_std::{Coin, StdResult}; use cw_storage_plus::Item; -use mixnet_contract_common::{Layer, LayerDistribution}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::ContractState; -pub(crate) const CONTRACT_STATE: Item<'_, ContractState> = Item::new("config"); -pub(crate) const LAYERS: Item<'_, LayerDistribution> = Item::new("layers"); +pub(crate) const CONTRACT_STATE: Item<'_, ContractState> = Item::new(CONTRACT_STATE_KEY); -pub fn rewarding_validator_address(storage: &dyn Storage) -> StdResult { - CONTRACT_STATE +pub fn rewarding_validator_address(storage: &dyn Storage) -> Result { + Ok(CONTRACT_STATE .load(storage) - .map(|state| state.rewarding_validator_address.to_string()) + .map(|state| state.rewarding_validator_address)?) } -pub fn mix_denom(storage: &dyn Storage) -> StdResult { - CONTRACT_STATE.load(storage).map(|state| state.mix_denom) +pub(crate) fn minimum_mixnode_pledge(storage: &dyn Storage) -> Result { + Ok(CONTRACT_STATE + .load(storage) + .map(|state| state.params.minimum_mixnode_pledge)?) } -pub fn increment_layer_count(storage: &mut dyn Storage, layer: Layer) -> StdResult<()> { - LAYERS - .update(storage, |mut distribution| { - match layer { - Layer::Gateway => distribution.gateways += 1, - Layer::One => distribution.layer1 += 1, - Layer::Two => distribution.layer2 += 1, - Layer::Three => distribution.layer3 += 1, - } - Ok(distribution) - }) - .map(|_| ()) +pub(crate) fn minimum_gateway_pledge(storage: &dyn Storage) -> Result { + Ok(CONTRACT_STATE + .load(storage) + .map(|state| state.params.minimum_gateway_pledge)?) } -pub fn decrement_layer_count(storage: &mut dyn Storage, layer: Layer) -> StdResult<()> { - LAYERS - .update(storage, |mut distribution| { - match layer { - Layer::Gateway => { - distribution.gateways = distribution - .gateways - .checked_sub(1) - .expect("tried to subtract from unsigned zero!") - } - Layer::One => { - distribution.layer1 = distribution - .layer1 - .checked_sub(1) - .expect("tried to subtract from unsigned zero!") - } - Layer::Two => { - distribution.layer2 = distribution - .layer2 - .checked_sub(1) - .expect("tried to subtract from unsigned zero!") - } - Layer::Three => { - distribution.layer3 = distribution - .layer3 - .checked_sub(1) - .expect("tried to subtract from unsigned zero!") - } - } - Ok(distribution) - }) - .map(|_| ()) +#[allow(unused)] +pub(crate) fn minimum_delegation_stake( + storage: &dyn Storage, +) -> Result, MixnetContractError> { + Ok(CONTRACT_STATE + .load(storage) + .map(|state| state.params.minimum_mixnode_delegation)?) +} + +#[allow(unused)] +pub(crate) fn rewarding_denom(storage: &dyn Storage) -> Result { + Ok(CONTRACT_STATE + .load(storage) + .map(|state| state.rewarding_denom)?) +} + +pub(crate) fn vesting_contract_address(storage: &dyn Storage) -> Result { + Ok(CONTRACT_STATE + .load(storage) + .map(|state| state.vesting_contract_address)?) +} + +pub(crate) fn initialise_storage( + storage: &mut dyn Storage, + initial_state: ContractState, +) -> StdResult<()> { + CONTRACT_STATE.save(storage, &initial_state) } diff --git a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs index 2a4cef7af7..1413d492ca 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs @@ -1,56 +1,51 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use super::storage; -use crate::error::ContractError; -use cosmwasm_std::Addr; use cosmwasm_std::DepsMut; use cosmwasm_std::MessageInfo; use cosmwasm_std::Response; -use mixnet_contract_common::events::new_settings_update_event; +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_rewarding_validator_address( deps: DepsMut<'_>, info: MessageInfo, address: String, -) -> Result { +) -> Result { let mut state = storage::CONTRACT_STATE.load(deps.storage)?; if info.sender != state.owner { - return Err(ContractError::Unauthorized); + return Err(MixnetContractError::Unauthorized); } - state.rewarding_validator_address = Addr::unchecked(address); + let new_address = deps.api.addr_validate(&address)?; + let old_address = state.rewarding_validator_address; + + state.rewarding_validator_address = new_address.clone(); storage::CONTRACT_STATE.save(deps.storage, &state)?; - Ok(Response::default()) + Ok( + Response::new().add_event(new_rewarding_validator_address_update_event( + old_address, + new_address, + )), + ) } pub(crate) fn try_update_contract_settings( deps: DepsMut<'_>, info: MessageInfo, params: ContractStateParams, -) -> Result { +) -> 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(ContractError::Unauthorized); - } - - if params.mixnode_rewarded_set_size == 0 { - return Err(ContractError::ZeroRewardedSet); - } - - if params.mixnode_active_set_size == 0 { - return Err(ContractError::ZeroActiveSet); - } - - // note: rewarded_set = active_set + idle_set - // hence rewarded set must always be bigger than (or equal to) the active set - if params.mixnode_rewarded_set_size < params.mixnode_active_set_size { - return Err(ContractError::InvalidActiveSetSize); + return Err(MixnetContractError::Unauthorized); } let response = Response::new().add_event(new_settings_update_event(&state.params, ¶ms)); @@ -64,13 +59,14 @@ pub(crate) fn try_update_contract_settings( #[cfg(test)] pub mod tests { use super::*; - use crate::contract::{INITIAL_GATEWAY_PLEDGE, INITIAL_MIXNODE_PLEDGE, INITIAL_STAKING_SUPPLY}; - use crate::error::ContractError; + use crate::constants::{INITIAL_GATEWAY_PLEDGE_AMOUNT, INITIAL_MIXNODE_PLEDGE_AMOUNT}; use crate::mixnet_contract_settings::queries::query_rewarding_validator_address; + use crate::mixnet_contract_settings::storage::rewarding_denom; use crate::mixnet_contract_settings::transactions::try_update_contract_settings; use crate::support::tests::test_helpers; use cosmwasm_std::testing::mock_info; - use cosmwasm_std::Response; + use cosmwasm_std::{Addr, Coin, Response, Uint128}; + use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::ContractStateParams; #[test] @@ -83,7 +79,7 @@ pub mod tests { info, "not-the-creator".to_string(), ); - assert_eq!(res, Err(ContractError::Unauthorized)); + assert_eq!(res, Err(MixnetContractError::Unauthorized)); let info = mock_info("creator", &[]); let res = try_update_rewarding_validator_address( @@ -91,7 +87,15 @@ pub mod tests { info, "new-good-address".to_string(), ); - assert_eq!(res, Ok(Response::default())); + assert_eq!( + res, + Ok( + Response::default().add_event(new_rewarding_validator_address_update_event( + Addr::unchecked("rewarder"), + Addr::unchecked("new-good-address") + )) + ) + ); let state = storage::CONTRACT_STATE.load(&deps.storage).unwrap(); assert_eq!( @@ -108,13 +112,18 @@ pub mod tests { #[test] fn updating_contract_settings() { let mut deps = test_helpers::init_contract(); + let denom = rewarding_denom(deps.as_ref().storage).unwrap(); let new_params = ContractStateParams { - minimum_mixnode_pledge: INITIAL_MIXNODE_PLEDGE, - minimum_gateway_pledge: INITIAL_GATEWAY_PLEDGE, - mixnode_rewarded_set_size: 100, - mixnode_active_set_size: 50, - staking_supply: INITIAL_STAKING_SUPPLY, + minimum_mixnode_delegation: None, + minimum_mixnode_pledge: Coin { + denom: denom.clone(), + amount: INITIAL_MIXNODE_PLEDGE_AMOUNT, + }, + minimum_gateway_pledge: Coin { + denom, + amount: INITIAL_GATEWAY_PLEDGE_AMOUNT + Uint128::new(1234), + }, }; let initial_params = storage::CONTRACT_STATE @@ -128,7 +137,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(ContractError::Unauthorized)); + assert_eq!(res, Err(MixnetContractError::Unauthorized)); // but works fine from the creator account let info = mock_info("creator", &[]); @@ -142,25 +151,25 @@ pub mod tests { let current_state = storage::CONTRACT_STATE.load(deps.as_ref().storage).unwrap(); assert_eq!(current_state.params, new_params); - // error is thrown if rewarded set is smaller than the active set - let info = mock_info("creator", &[]); - let mut new_params = current_state.params.clone(); - new_params.mixnode_rewarded_set_size = new_params.mixnode_active_set_size - 1; - let res = try_update_contract_settings(deps.as_mut(), info, new_params); - assert_eq!(Err(ContractError::InvalidActiveSetSize), res); - - // error is thrown for 0 size rewarded set - let info = mock_info("creator", &[]); - let mut new_params = current_state.params.clone(); - new_params.mixnode_rewarded_set_size = 0; - let res = try_update_contract_settings(deps.as_mut(), info, new_params); - assert_eq!(Err(ContractError::ZeroRewardedSet), res); - - // error is thrown for 0 size active set - let info = mock_info("creator", &[]); - let mut new_params = current_state.params; - new_params.mixnode_active_set_size = 0; - let res = try_update_contract_settings(deps.as_mut(), info, new_params); - assert_eq!(Err(ContractError::ZeroActiveSet), res); + // // error is thrown if rewarded set is smaller than the active set + // let info = mock_info("creator", &[]); + // let mut new_params = current_state.params.clone(); + // new_params.mixnode_rewarded_set_size = new_params.mixnode_active_set_size - 1; + // let res = try_update_contract_settings(deps.as_mut(), info, new_params); + // assert_eq!(Err(MixnetContractError::InvalidActiveSetSize), res); + // + // // error is thrown for 0 size rewarded set + // let info = mock_info("creator", &[]); + // let mut new_params = current_state.params.clone(); + // new_params.mixnode_rewarded_set_size = 0; + // let res = try_update_contract_settings(deps.as_mut(), info, new_params); + // assert_eq!(Err(MixnetContractError::ZeroRewardedSet), res); + // + // // error is thrown for 0 size active set + // let info = mock_info("creator", &[]); + // let mut new_params = current_state.params; + // new_params.mixnode_active_set_size = 0; + // let res = try_update_contract_settings(deps.as_mut(), info, new_params); + // assert_eq!(Err(MixnetContractError::ZeroActiveSet), res); } } diff --git a/contracts/mixnet/src/mixnodes/bonding_queries.rs b/contracts/mixnet/src/mixnodes/bonding_queries.rs deleted file mode 100644 index 3c5085e04e..0000000000 --- a/contracts/mixnet/src/mixnodes/bonding_queries.rs +++ /dev/null @@ -1,276 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use super::storage::{self, StoredMixnodeBond}; -use cosmwasm_std::{Deps, Order, StdResult}; -use cw_storage_plus::Bound; -use mixnet_contract_common::{ - IdentityKey, MixNodeBond, MixOwnershipResponse, MixnodeBondResponse, PagedMixnodeResponse, -}; - -pub fn get_blacklisted_nodes(deps: Deps<'_>) -> Vec { - storage::MIXNODES_BOND_BLACKLIST - .keys(deps.storage, None, None, Order::Ascending) - .filter_map(|i| i.ok()) - .collect() -} - -pub fn query_mixnode_at_height( - deps: Deps<'_>, - mix_identity: String, - height: u64, -) -> StdResult> { - storage::mixnodes().may_load_at_height(deps.storage, &mix_identity, height) -} - -pub fn query_checkpoints_for_mixnode( - deps: Deps<'_>, - mix_identity: IdentityKey, -) -> StdResult> { - Ok(storage::mixnodes() - .changelog() - .prefix(&mix_identity) - .keys(deps.storage, None, None, Order::Ascending) - .filter_map(|x| x.ok()) - .collect()) -} - -pub fn query_mixnodes_paged( - deps: Deps<'_>, - start_after: Option, - limit: Option, -) -> StdResult { - let limit = limit - .unwrap_or(storage::BOND_PAGE_DEFAULT_LIMIT) - .min(storage::BOND_PAGE_MAX_LIMIT) as usize; - - let start = start_after.as_deref().map(Bound::exclusive); - - let nodes = storage::mixnodes() - .range(deps.storage, start, None, Order::Ascending) - .take(limit) - .map(|res| res.map(|item| item.1)) - .map(|stored_bond| { - // I really don't like this additional read per entry, but I don't see an obvious way to remove it - stored_bond.map(|stored_bond| { - let total_delegation = - storage::TOTAL_DELEGATION.load(deps.storage, stored_bond.identity()); - total_delegation - .map(|total_delegation| stored_bond.attach_delegation(total_delegation)) - }) - }) - .collect::>>>()??; - - let start_next_after = nodes.last().map(|node| node.identity().clone()); - - Ok(PagedMixnodeResponse::new(nodes, limit, start_next_after)) -} - -pub fn query_owns_mixnode(deps: Deps<'_>, address: String) -> StdResult { - let validated_addr = deps.api.addr_validate(&address)?; - let stored_bond = storage::mixnodes() - .idx - .owner - .item(deps.storage, validated_addr.clone())? - .map(|record| record.1); - - let mixnode = match stored_bond { - None => None, - Some(bond) => { - let total_delegation = - storage::TOTAL_DELEGATION.may_load(deps.storage, bond.identity())?; - Some(bond.attach_delegation(total_delegation.unwrap_or_default())) - } - }; - - Ok(MixOwnershipResponse { - address: validated_addr, - mixnode, - }) -} - -pub fn query_mixnode_bond(deps: Deps<'_>, identity: IdentityKey) -> StdResult { - Ok(MixnodeBondResponse { - mixnode: storage::read_full_mixnode_bond(deps.storage, &identity)?, - identity, - }) -} - -#[cfg(test)] -pub(crate) mod tests { - use super::storage; - use super::*; - use crate::mixnodes::storage::BOND_PAGE_DEFAULT_LIMIT; - use crate::support::tests::test_helpers; - use crate::{contract::execute, support::tests}; - use cosmwasm_std::testing::{mock_env, mock_info}; - - #[test] - fn mixnodes_empty_on_init() { - let deps = test_helpers::init_contract(); - let response = query_mixnodes_paged(deps.as_ref(), None, Option::from(2)).unwrap(); - assert_eq!(0, response.nodes.len()); - } - - #[test] - fn mixnodes_paged_retrieval_obeys_limits() { - let mut deps = test_helpers::init_contract(); - let limit = 2; - for n in 0..1000 { - let key = format!("bond{}", n); - test_helpers::add_mixnode(&key, tests::fixtures::good_mixnode_pledge(), deps.as_mut()); - } - - let page1 = query_mixnodes_paged(deps.as_ref(), None, Option::from(limit)).unwrap(); - assert_eq!(limit, page1.nodes.len() as u32); - } - - #[test] - fn mixnodes_paged_retrieval_has_default_limit() { - let mut deps = test_helpers::init_contract(); - for n in 0..1000 { - let key = format!("bond{}", n); - test_helpers::add_mixnode(&key, tests::fixtures::good_mixnode_pledge(), deps.as_mut()); - } - - // query without explicitly setting a limit - let page1 = query_mixnodes_paged(deps.as_ref(), None, None).unwrap(); - - assert_eq!(BOND_PAGE_DEFAULT_LIMIT, page1.nodes.len() as u32); - } - - #[test] - fn mixnodes_paged_retrieval_has_max_limit() { - let mut deps = test_helpers::init_contract(); - for n in 0..1000 { - let key = format!("bond{}", n); - test_helpers::add_mixnode(&key, tests::fixtures::good_mixnode_pledge(), deps.as_mut()); - } - - // query with a crazily high limit in an attempt to use too many resources - let crazy_limit = 1000; - let page1 = query_mixnodes_paged(deps.as_ref(), None, Option::from(crazy_limit)).unwrap(); - - // we default to a decent sized upper bound instead - let expected_limit = storage::BOND_PAGE_MAX_LIMIT; - assert_eq!(expected_limit, page1.nodes.len() as u32); - } - - #[test] - fn pagination_works() { - // prepare 4 messages and identities that are sorted by the generated identities - // (because we query them in an ascended manner) - let mut exec_data = (0..4) - .map(|i| { - let sender = format!("nym-addr{}", i); - let (msg, identity) = tests::messages::valid_bond_mixnode_msg(&sender); - (msg, (sender, identity)) - }) - .collect::>(); - exec_data.sort_by(|(_, (_, id1)), (_, (_, id2))| id1.cmp(id2)); - let (messages, sender_identities): (Vec<_>, Vec<_>) = exec_data.into_iter().unzip(); - - let mut deps = test_helpers::init_contract(); - - let info = mock_info( - &sender_identities[0].0.clone(), - &tests::fixtures::good_mixnode_pledge(), - ); - execute(deps.as_mut(), mock_env(), info, messages[0].clone()).unwrap(); - - let per_page = 2; - let page1 = query_mixnodes_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); - - // page should have 1 result on it - assert_eq!(1, page1.nodes.len()); - - // save another - let info = mock_info( - &sender_identities[1].0.clone(), - &tests::fixtures::good_mixnode_pledge(), - ); - execute(deps.as_mut(), mock_env(), info, messages[1].clone()).unwrap(); - - // page1 should have 2 results on it - let page1 = query_mixnodes_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); - assert_eq!(2, page1.nodes.len()); - - let info = mock_info( - &sender_identities[2].0.clone(), - &tests::fixtures::good_mixnode_pledge(), - ); - execute(deps.as_mut(), mock_env(), info, messages[2].clone()).unwrap(); - - // page1 still has 2 results - let page1 = query_mixnodes_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); - assert_eq!(2, page1.nodes.len()); - - // retrieving the next page should start after the last key on this page - let start_after = page1.start_next_after.unwrap(); - let page2 = query_mixnodes_paged( - deps.as_ref(), - Option::from(start_after.clone()), - Option::from(per_page), - ) - .unwrap(); - - assert_eq!(1, page2.nodes.len()); - - // save another one - let info = mock_info( - &sender_identities[3].0.clone(), - &tests::fixtures::good_mixnode_pledge(), - ); - execute(deps.as_mut(), mock_env(), info, messages[3].clone()).unwrap(); - - let page2 = query_mixnodes_paged( - deps.as_ref(), - Option::from(start_after), - Option::from(per_page), - ) - .unwrap(); - - // now we have 2 pages, with 2 results on the second page - assert_eq!(2, page2.nodes.len()); - } - - #[test] - fn query_for_mixnode_owner_works() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - - // "fred" does not own a mixnode if there are no mixnodes - let res = query_owns_mixnode(deps.as_ref(), "fred".to_string()).unwrap(); - assert!(res.mixnode.is_none()); - - // mixnode was added to "bob", "fred" still does not own one - test_helpers::add_mixnode("bob", tests::fixtures::good_mixnode_pledge(), deps.as_mut()); - - let res = query_owns_mixnode(deps.as_ref(), "fred".to_string()).unwrap(); - assert!(res.mixnode.is_none()); - - // "fred" now owns a mixnode! - test_helpers::add_mixnode( - "fred", - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - - let res = query_owns_mixnode(deps.as_ref(), "fred".to_string()).unwrap(); - assert!(res.mixnode.is_some()); - - let api = deps.api.clone(); - - // but after unbonding it, he doesn't own one anymore - crate::mixnodes::transactions::try_remove_mixnode( - &env, - deps.as_mut().storage, - &api, - mock_info("fred", &[]), - ) - .unwrap(); - - let res = query_owns_mixnode(deps.as_ref(), "fred".to_string()).unwrap(); - assert!(res.mixnode.is_none()); - } -} diff --git a/contracts/mixnet/src/mixnodes/helpers.rs b/contracts/mixnet/src/mixnodes/helpers.rs new file mode 100644 index 0000000000..57f162c610 --- /dev/null +++ b/contracts/mixnet/src/mixnodes/helpers.rs @@ -0,0 +1,476 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::storage; +use crate::interval::storage as interval_storage; +use crate::mixnodes::storage::{assign_layer, next_mixnode_id_counter}; +use crate::rewards::storage as rewards_storage; +use cosmwasm_std::{Addr, Coin, Decimal, Env, StdResult, Storage}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::mixnode::{ + MixNodeCostParams, MixNodeDetails, MixNodeRewarding, UnbondedMixnode, +}; +use mixnet_contract_common::{Layer, MixNode, MixNodeBond, NodeId}; + +pub(crate) fn must_get_mixnode_bond_by_owner( + store: &dyn Storage, + owner: &Addr, +) -> Result { + Ok(storage::mixnode_bonds() + .idx + .owner + .item(store, owner.clone())? + .ok_or(MixnetContractError::NoAssociatedMixNodeBond { + owner: owner.clone(), + })? + .1) +} + +pub(crate) fn get_mixnode_details_by_id( + store: &dyn Storage, + node_id: NodeId, +) -> StdResult> { + if let Some(bond_information) = storage::mixnode_bonds().may_load(store, node_id)? { + // if bond exists, rewarding details MUST also exist + let rewarding_details = + rewards_storage::MIXNODE_REWARDING.load(store, bond_information.id)?; + Ok(Some(MixNodeDetails::new( + bond_information, + rewarding_details, + ))) + } else { + Ok(None) + } +} + +pub(crate) fn get_mixnode_details_by_owner( + store: &dyn Storage, + address: Addr, +) -> StdResult> { + if let Some(bond_information) = storage::mixnode_bonds() + .idx + .owner + .item(store, address)? + .map(|record| record.1) + { + // if bond exists, rewarding details MUST also exist + let rewarding_details = + rewards_storage::MIXNODE_REWARDING.load(store, bond_information.id)?; + Ok(Some(MixNodeDetails::new( + bond_information, + rewarding_details, + ))) + } else { + Ok(None) + } +} + +pub(crate) fn save_new_mixnode( + storage: &mut dyn Storage, + env: Env, + mixnode: MixNode, + cost_params: MixNodeCostParams, + owner: Addr, + proxy: Option, + pledge: Coin, +) -> Result<(NodeId, Layer), MixnetContractError> { + let layer = assign_layer(storage)?; + let node_id = next_mixnode_id_counter(storage)?; + let current_epoch = interval_storage::current_interval(storage)?.current_epoch_absolute_id(); + + let mixnode_rewarding = MixNodeRewarding::initialise_new(cost_params, &pledge, current_epoch); + let mixnode_bond = MixNodeBond::new( + node_id, + owner, + pledge, + layer, + mixnode, + proxy, + env.block.height, + ); + + // save mixnode bond data + // note that this implicitly checks for uniqueness on identity key, sphinx key and owner + storage::mixnode_bonds().save(storage, node_id, &mixnode_bond)?; + + // save rewarding data + rewards_storage::MIXNODE_REWARDING.save(storage, node_id, &mixnode_rewarding)?; + + Ok((node_id, layer)) +} + +pub(crate) fn cleanup_post_unbond_mixnode_storage( + storage: &mut dyn Storage, + env: &Env, + current_details: &MixNodeDetails, +) -> Result<(), MixnetContractError> { + let node_id = current_details.bond_information.id; + // remove all bond information (we don't need it anymore + // note that "normal" remove is `may_load` followed by `replace` with a `None` + // and we have already loaded the data from the storage + storage::mixnode_bonds().replace( + storage, + node_id, + None, + Some(¤t_details.bond_information), + )?; + + // if there are no pending delegations to return, we can also + // purge all information regarding rewarding parameters + if current_details.rewarding_details.unique_delegations == 0 { + rewards_storage::MIXNODE_REWARDING.remove(storage, node_id); + } else { + // otherwise just set operator's tokens to zero as to indicate they have unbonded + // and already claimed those + let mut zeroed = current_details.rewarding_details.clone(); + zeroed.operator = Decimal::zero(); + + rewards_storage::MIXNODE_REWARDING.save(storage, node_id, &zeroed)?; + } + + let identity = current_details.bond_information.identity().to_owned(); + let owner = current_details.bond_information.owner().to_owned(); + let proxy = current_details.bond_information.proxy.to_owned(); + + // save minimal information about this mixnode + storage::unbonded_mixnodes().save( + storage, + node_id, + &UnbondedMixnode { + identity_key: identity, + owner, + proxy, + unbonding_height: env.block.height, + }, + )?; + storage::decrement_layer_count(storage, current_details.bond_information.layer) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::support::tests::fixtures::{ + mix_node_cost_params_fixture, mix_node_fixture, TEST_COIN_DENOM, + }; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::coin; + + const OWNER_EXISTS: &str = "mix-owner-existing"; + const OWNER_UNBONDING: &str = "mix-owner-unbonding"; + const OWNER_UNBONDED: &str = "mix-owner-unbonded"; + const OWNER_UNBONDED_LEFTOVER: &str = "mix-owner-unbonded-leftover"; + + // create a mixnode that is bonded, unbonded, in the process of unbonding and unbonded with leftover mix rewarding details + fn setup_mix_combinations(test: &mut TestSetup) -> Vec { + let mix_id_exists = test.add_dummy_mixnode(OWNER_EXISTS, None); + let mix_id_unbonding = test.add_dummy_mixnode(OWNER_UNBONDING, None); + let mix_id_unbonded = test.add_dummy_mixnode(OWNER_UNBONDED, None); + let mix_id_unbonded_leftover = test.add_dummy_mixnode(OWNER_UNBONDED_LEFTOVER, None); + + // manually adjust delegation info as to indicate the rewarding information shouldnt get removed + let mut rewarding_details = test.mix_rewarding(mix_id_unbonded_leftover); + rewarding_details.delegates = Decimal::raw(12345); + rewarding_details.unique_delegations = 1; + rewards_storage::MIXNODE_REWARDING + .save( + test.deps_mut().storage, + mix_id_unbonded_leftover, + &rewarding_details, + ) + .unwrap(); + + test.immediately_unbond_mixnode(mix_id_unbonded); + test.immediately_unbond_mixnode(mix_id_unbonded_leftover); + test.start_unbonding_mixnode(mix_id_unbonding); + + vec![ + mix_id_exists, + mix_id_unbonding, + mix_id_unbonded, + mix_id_unbonded_leftover, + ] + } + + #[test] + fn getting_mixnode_bond_by_owner() { + let mut test = TestSetup::new(); + + let owner_exists = Addr::unchecked(OWNER_EXISTS); + let owner_unbonding = Addr::unchecked(OWNER_UNBONDING); + let owner_unbonded = Addr::unchecked(OWNER_UNBONDED); + let owner_unbonded_leftover = Addr::unchecked(OWNER_UNBONDED_LEFTOVER); + + let ids = setup_mix_combinations(&mut test); + let mix_id_exists = ids[0]; + let mix_id_unbonding = ids[1]; + + // if this is a normally bonded mixnode, all should be fine + let res = must_get_mixnode_bond_by_owner(test.deps().storage, &owner_exists).unwrap(); + assert_eq!(res.id, mix_id_exists); + + // if node is in the process of unbonding, we still should be capable of retrieving its details + let res = must_get_mixnode_bond_by_owner(test.deps().storage, &owner_unbonding).unwrap(); + assert_eq!(res.id, mix_id_unbonding); + + // but if node has unbonded, the information is purged and query fails + let res = must_get_mixnode_bond_by_owner(test.deps().storage, &owner_unbonded); + assert_eq!( + res, + Err(MixnetContractError::NoAssociatedMixNodeBond { + owner: owner_unbonded + }) + ); + + let res = must_get_mixnode_bond_by_owner(test.deps().storage, &owner_unbonded_leftover); + assert_eq!( + res, + Err(MixnetContractError::NoAssociatedMixNodeBond { + owner: owner_unbonded_leftover + }) + ); + } + + #[test] + fn getting_mixnode_details_by_id() { + let mut test = TestSetup::new(); + + let ids = setup_mix_combinations(&mut test); + let mix_id_exists = ids[0]; + let mix_id_unbonding = ids[1]; + let mix_id_unbonded = ids[2]; + let mix_id_unbonded_leftover = ids[3]; + + let res = get_mixnode_details_by_id(test.deps().storage, mix_id_exists) + .unwrap() + .unwrap(); + assert_eq!(res.bond_information.id, mix_id_exists); + + let res = get_mixnode_details_by_id(test.deps().storage, mix_id_unbonding) + .unwrap() + .unwrap(); + assert_eq!(res.bond_information.id, mix_id_unbonding); + + let res = get_mixnode_details_by_id(test.deps().storage, mix_id_unbonded).unwrap(); + assert!(res.is_none()); + + let res = get_mixnode_details_by_id(test.deps().storage, mix_id_unbonded_leftover).unwrap(); + assert!(res.is_none()) + } + + #[test] + fn getting_mixnode_details_by_owner() { + let mut test = TestSetup::new(); + + let owner_exists = Addr::unchecked(OWNER_EXISTS); + let owner_unbonding = Addr::unchecked(OWNER_UNBONDING); + let owner_unbonded = Addr::unchecked(OWNER_UNBONDED); + let owner_unbonded_leftover = Addr::unchecked(OWNER_UNBONDED_LEFTOVER); + + let ids = setup_mix_combinations(&mut test); + let mix_id_exists = ids[0]; + let mix_id_unbonding = ids[1]; + + // if this is a normally bonded mixnode, all should be fine + let res = get_mixnode_details_by_owner(test.deps().storage, owner_exists) + .unwrap() + .unwrap(); + assert_eq!(res.bond_information.id, mix_id_exists); + + // if node is in the process of unbonding, we still should be capable of retrieving its details + let res = get_mixnode_details_by_owner(test.deps().storage, owner_unbonding) + .unwrap() + .unwrap(); + assert_eq!(res.bond_information.id, mix_id_unbonding); + + // but if node has unbonded, the information is purged and query fails + let res = get_mixnode_details_by_owner(test.deps().storage, owner_unbonded).unwrap(); + assert!(res.is_none()); + + let res = + get_mixnode_details_by_owner(test.deps().storage, owner_unbonded_leftover).unwrap(); + assert!(res.is_none()); + } + + #[test] + fn saving_new_mixnode() { + let mut test = TestSetup::new(); + + // get some mixnodes in + test.add_dummy_mixnode("owner1", None); + test.add_dummy_mixnode("owner2", None); + test.add_dummy_mixnode("owner3", None); + test.add_dummy_mixnode("owner4", None); + test.add_dummy_mixnode("owner5", None); + + let env = test.env(); + let id_key = "identity-key"; + let sphinx_key = "sphinx-key"; + let mut mixnode = mix_node_fixture(); + mixnode.identity_key = id_key.into(); + mixnode.sphinx_key = sphinx_key.into(); + let cost_params = mix_node_cost_params_fixture(); + let owner = Addr::unchecked("mix-owner"); + let pledge = coin(100_000_000, TEST_COIN_DENOM); + + let (id, layer) = save_new_mixnode( + test.deps_mut().storage, + env.clone(), + mixnode, + cost_params.clone(), + owner.clone(), + None, + pledge.clone(), + ) + .unwrap(); + assert_eq!(id, 6); + assert_eq!(layer, Layer::Three); + + assert_eq!( + storage::MIXNODE_ID_COUNTER + .load(test.deps().storage) + .unwrap(), + 6 + ); + assert_eq!(storage::LAYERS.load(test.deps().storage).unwrap().layer3, 2); + let mix_details = get_mixnode_details_by_id(test.deps().storage, id) + .unwrap() + .unwrap(); + assert_eq!(mix_details.mix_id(), id); + assert_eq!(mix_details.original_pledge(), &pledge); + assert_eq!( + mix_details.bond_information.bonding_height, + env.block.height + ); + + // try to add node with duplicate identity... + let mut mixnode = mix_node_fixture(); + mixnode.identity_key = id_key.into(); + let res = save_new_mixnode( + test.deps_mut().storage, + env.clone(), + mixnode, + cost_params.clone(), + Addr::unchecked("different-owner"), + None, + pledge.clone(), + ); + assert!(res.is_err()); + + // and duplicate owner... + let mixnode = mix_node_fixture(); + let res = save_new_mixnode( + test.deps_mut().storage, + env.clone(), + mixnode, + cost_params.clone(), + owner, + None, + pledge.clone(), + ); + assert!(res.is_err()); + + // and duplicate sphinx key... + let mut mixnode = mix_node_fixture(); + mixnode.sphinx_key = sphinx_key.into(); + let res = save_new_mixnode( + test.deps_mut().storage, + env, + mixnode, + cost_params, + Addr::unchecked("different-owner"), + None, + pledge, + ); + assert!(res.is_err()); + } + + #[test] + fn cleaning_post_unbond_storage() { + let mut test = TestSetup::new(); + + let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id_leftover = test.add_dummy_mixnode("mix-owner-leftover", None); + + // manually adjust delegation info as to indicate the rewarding information shouldnt get removed + let mut rewarding_details = test.mix_rewarding(mix_id_leftover); + rewarding_details.delegates = Decimal::raw(12345); + rewarding_details.unique_delegations = 1; + rewards_storage::MIXNODE_REWARDING + .save(test.deps_mut().storage, mix_id_leftover, &rewarding_details) + .unwrap(); + + let bond1 = storage::mixnode_bonds() + .load(test.deps().storage, mix_id) + .unwrap(); + let bond2 = storage::mixnode_bonds() + .load(test.deps().storage, mix_id_leftover) + .unwrap(); + + let env = test.env(); + let details1 = get_mixnode_details_by_id(test.deps().storage, mix_id) + .unwrap() + .unwrap(); + cleanup_post_unbond_mixnode_storage(test.deps_mut().storage, &env, &details1).unwrap(); + + // bond information is gone + let bond = storage::mixnode_bonds() + .may_load(test.deps().storage, mix_id) + .unwrap(); + assert!(bond.is_none()); + + // rewarding information is gone + let mix_rewarding = rewards_storage::MIXNODE_REWARDING + .may_load(test.deps().storage, mix_id) + .unwrap(); + assert!(mix_rewarding.is_none()); + + // unbonded details are inserted + let unbonded_details = storage::unbonded_mixnodes() + .load(test.deps().storage, mix_id) + .unwrap(); + let expected = UnbondedMixnode { + identity_key: bond1.mix_node.identity_key, + owner: bond1.owner, + proxy: None, + unbonding_height: env.block.height, + }; + assert_eq!(unbonded_details, expected); + + // layers are decremented + assert_eq!(storage::LAYERS.load(test.deps().storage).unwrap().layer1, 0); + + let details2 = get_mixnode_details_by_id(test.deps().storage, mix_id_leftover) + .unwrap() + .unwrap(); + cleanup_post_unbond_mixnode_storage(test.deps_mut().storage, &env, &details2).unwrap(); + + // bond information is gone + let bond = storage::mixnode_bonds() + .may_load(test.deps().storage, mix_id_leftover) + .unwrap(); + assert!(bond.is_none()); + + // rewarding information is NOT gone, but operator field is reset + let mix_rewarding = rewards_storage::MIXNODE_REWARDING + .may_load(test.deps().storage, mix_id_leftover) + .unwrap() + .unwrap(); + assert!(mix_rewarding.operator.is_zero()); + assert_eq!(mix_rewarding.unique_delegations, 1); + + // unbonded details are inserted + let unbonded_details = storage::unbonded_mixnodes() + .load(test.deps().storage, mix_id_leftover) + .unwrap(); + let expected = UnbondedMixnode { + identity_key: bond2.mix_node.identity_key, + owner: bond2.owner, + proxy: None, + unbonding_height: env.block.height, + }; + assert_eq!(unbonded_details, expected); + + // layers are decremented + assert_eq!(storage::LAYERS.load(test.deps().storage).unwrap().layer2, 0); + } +} diff --git a/contracts/mixnet/src/mixnodes/layer_queries.rs b/contracts/mixnet/src/mixnodes/layer_queries.rs deleted file mode 100644 index 1764e7211e..0000000000 --- a/contracts/mixnet/src/mixnodes/layer_queries.rs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::mixnet_contract_settings::storage as mixnet_params_storage; -use cosmwasm_std::{Deps, StdResult}; -use mixnet_contract_common::LayerDistribution; - -pub(crate) fn query_layer_distribution(deps: Deps<'_>) -> StdResult { - mixnet_params_storage::LAYERS.load(deps.storage) -} diff --git a/contracts/mixnet/src/mixnodes/mod.rs b/contracts/mixnet/src/mixnodes/mod.rs index 8f80d5b408..f3a3c9941f 100644 --- a/contracts/mixnet/src/mixnodes/mod.rs +++ b/contracts/mixnet/src/mixnodes/mod.rs @@ -1,7 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -pub mod bonding_queries; -pub mod layer_queries; +pub mod helpers; +pub mod queries; pub mod storage; pub mod transactions; diff --git a/contracts/mixnet/src/mixnodes/queries.rs b/contracts/mixnet/src/mixnodes/queries.rs new file mode 100644 index 0000000000..12ca4c3d36 --- /dev/null +++ b/contracts/mixnet/src/mixnodes/queries.rs @@ -0,0 +1,1336 @@ +// Copyright 2021-2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::storage; +use crate::constants::{ + MIXNODE_BOND_DEFAULT_RETRIEVAL_LIMIT, MIXNODE_BOND_MAX_RETRIEVAL_LIMIT, + MIXNODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT, MIXNODE_DETAILS_MAX_RETRIEVAL_LIMIT, + UNBONDED_MIXNODES_DEFAULT_RETRIEVAL_LIMIT, UNBONDED_MIXNODES_MAX_RETRIEVAL_LIMIT, +}; +use crate::mixnodes::helpers::{get_mixnode_details_by_id, get_mixnode_details_by_owner}; +use crate::rewards::storage as rewards_storage; +use cosmwasm_std::{Deps, Order, StdResult, Storage}; +use cw_storage_plus::Bound; +use mixnet_contract_common::mixnode::{ + MixNodeBond, MixNodeDetails, MixnodeRewardingDetailsResponse, PagedMixnodesDetailsResponse, + PagedUnbondedMixnodesResponse, StakeSaturationResponse, UnbondedMixnodeResponse, +}; +use mixnet_contract_common::{ + IdentityKey, LayerDistribution, MixOwnershipResponse, MixnodeDetailsResponse, NodeId, + PagedMixnodeBondsResponse, +}; + +pub fn query_mixnode_bonds_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(MIXNODE_BOND_DEFAULT_RETRIEVAL_LIMIT) + .min(MIXNODE_BOND_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::mixnode_bonds() + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|item| item.1)) + .collect::>>()?; + + let start_next_after = nodes.last().map(|node| node.id); + + Ok(PagedMixnodeBondsResponse::new( + nodes, + limit, + start_next_after, + )) +} + +fn attach_rewarding_info( + storage: &dyn Storage, + read_bond: StdResult<(NodeId, MixNodeBond)>, +) -> StdResult { + match read_bond { + Ok((_, bond)) => { + // if we managed to read the bond we MUST be able to also read rewarding information. + // if we fail, this is a hard error and the query should definitely fail and we should investigate + // the reasons for that. + let mix_rewarding = rewards_storage::MIXNODE_REWARDING.load(storage, bond.id)?; + Ok(MixNodeDetails::new(bond, mix_rewarding)) + } + Err(err) => Err(err), + } +} + +pub fn query_mixnodes_details_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(MIXNODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT) + .min(MIXNODE_DETAILS_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::mixnode_bonds() + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| attach_rewarding_info(deps.storage, res)) + .collect::>>()?; + + let start_next_after = nodes.last().map(|details| details.mix_id()); + + Ok(PagedMixnodesDetailsResponse::new( + nodes, + limit, + start_next_after, + )) +} + +pub fn query_unbonded_mixnodes_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(UNBONDED_MIXNODES_DEFAULT_RETRIEVAL_LIMIT) + .min(UNBONDED_MIXNODES_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::unbonded_mixnodes() + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .collect::>>()?; + + let start_next_after = nodes.last().map(|res| res.0); + + Ok(PagedUnbondedMixnodesResponse::new( + nodes, + limit, + start_next_after, + )) +} + +pub fn query_unbonded_mixnodes_by_owner_paged( + deps: Deps<'_>, + owner: String, + start_after: Option, + limit: Option, +) -> StdResult { + let owner = deps.api.addr_validate(&owner)?; + + let limit = limit + .unwrap_or(UNBONDED_MIXNODES_DEFAULT_RETRIEVAL_LIMIT) + .min(UNBONDED_MIXNODES_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::unbonded_mixnodes() + .idx + .owner + .prefix(owner) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .collect::>>()?; + + let start_next_after = nodes.last().map(|res| res.0); + + Ok(PagedUnbondedMixnodesResponse::new( + nodes, + limit, + start_next_after, + )) +} + +pub fn query_unbonded_mixnodes_by_identity_paged( + deps: Deps<'_>, + identity_key: String, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(UNBONDED_MIXNODES_DEFAULT_RETRIEVAL_LIMIT) + .min(UNBONDED_MIXNODES_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::unbonded_mixnodes() + .idx + .identity_key + .prefix(identity_key) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .collect::>>()?; + + let start_next_after = nodes.last().map(|res| res.0); + + Ok(PagedUnbondedMixnodesResponse::new( + nodes, + limit, + start_next_after, + )) +} + +pub fn query_owned_mixnode(deps: Deps<'_>, address: String) -> StdResult { + let validated_addr = deps.api.addr_validate(&address)?; + let mixnode_details = get_mixnode_details_by_owner(deps.storage, validated_addr.clone())?; + + Ok(MixOwnershipResponse { + address: validated_addr, + mixnode_details, + }) +} + +pub fn query_mixnode_details(deps: Deps<'_>, mix_id: NodeId) -> StdResult { + let mixnode_details = get_mixnode_details_by_id(deps.storage, mix_id)?; + + Ok(MixnodeDetailsResponse { + mix_id, + mixnode_details, + }) +} + +pub fn query_mixnode_details_by_identity( + deps: Deps<'_>, + mix_identity: IdentityKey, +) -> StdResult> { + if let Some(bond_information) = storage::mixnode_bonds() + .idx + .identity_key + .item(deps.storage, mix_identity)? + .map(|record| record.1) + { + // if bond exists, rewarding details MUST also exist + let rewarding_details = + rewards_storage::MIXNODE_REWARDING.load(deps.storage, bond_information.id)?; + Ok(Some(MixNodeDetails::new( + bond_information, + rewarding_details, + ))) + } else { + Ok(None) + } +} + +pub fn query_mixnode_rewarding_details( + deps: Deps<'_>, + mix_id: NodeId, +) -> StdResult { + let rewarding_details = rewards_storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)?; + + Ok(MixnodeRewardingDetailsResponse { + mix_id, + rewarding_details, + }) +} + +pub fn query_unbonded_mixnode( + deps: Deps<'_>, + mix_id: NodeId, +) -> StdResult { + let unbonded_info = storage::unbonded_mixnodes().may_load(deps.storage, mix_id)?; + + Ok(UnbondedMixnodeResponse { + mix_id, + unbonded_info, + }) +} + +pub fn query_stake_saturation( + deps: Deps<'_>, + mix_id: NodeId, +) -> StdResult { + let mix_rewarding = match rewards_storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)? { + Some(mix_rewarding) => mix_rewarding, + None => { + return Ok(StakeSaturationResponse { + mix_id, + current_saturation: None, + uncapped_saturation: None, + }) + } + }; + + let rewarding_params = rewards_storage::REWARDING_PARAMS.load(deps.storage)?; + + Ok(StakeSaturationResponse { + mix_id, + current_saturation: Some(mix_rewarding.bond_saturation(&rewarding_params)), + uncapped_saturation: Some(mix_rewarding.uncapped_bond_saturation(&rewarding_params)), + }) +} + +pub(crate) fn query_layer_distribution(deps: Deps<'_>) -> StdResult { + storage::LAYERS.load(deps.storage) +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::interval::pending_events; + use crate::support::tests::fixtures::good_mixnode_pledge; + use crate::support::tests::test_helpers::TestSetup; + use crate::support::tests::{fixtures, test_helpers}; + use cosmwasm_std::testing::mock_env; + use cosmwasm_std::Decimal; + + #[cfg(test)] + mod mixnode_bonds { + use super::*; + use crate::support::tests::fixtures::good_mixnode_pledge; + + #[test] + fn obeys_limits() { + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + let limit = 2; + + test_helpers::add_dummy_mixnodes(&mut rng, deps.as_mut(), env, 1000); + let page1 = query_mixnode_bonds_paged(deps.as_ref(), None, Some(limit)).unwrap(); + assert_eq!(limit, page1.nodes.len() as u32); + } + + #[test] + fn has_default_limit() { + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + test_helpers::add_dummy_mixnodes(&mut rng, deps.as_mut(), env, 1000); + + // query without explicitly setting a limit + let page1 = query_mixnode_bonds_paged(deps.as_ref(), None, None).unwrap(); + + assert_eq!( + MIXNODE_BOND_DEFAULT_RETRIEVAL_LIMIT, + page1.nodes.len() as u32 + ); + } + + #[test] + fn has_max_limit() { + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + test_helpers::add_dummy_mixnodes(&mut rng, deps.as_mut(), env, 1000); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000; + let page1 = query_mixnode_bonds_paged(deps.as_ref(), None, Some(crazy_limit)).unwrap(); + + // we default to a decent sized upper bound instead + assert_eq!(MIXNODE_BOND_MAX_RETRIEVAL_LIMIT, page1.nodes.len() as u32); + } + + #[test] + fn pagination_works() { + // as we add mixnodes, we're always inserting them in ascending manner due to monotonically increasing id + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + test_helpers::add_mixnode( + &mut rng, + deps.as_mut(), + env.clone(), + "addr1", + good_mixnode_pledge(), + ); + + let per_page = 2; + let page1 = query_mixnode_bonds_paged(deps.as_ref(), None, Some(per_page)).unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.nodes.len()); + + // save another + test_helpers::add_mixnode( + &mut rng, + deps.as_mut(), + env.clone(), + "addr2", + good_mixnode_pledge(), + ); + + // page1 should have 2 results on it + let page1 = query_mixnode_bonds_paged(deps.as_ref(), None, Some(per_page)).unwrap(); + assert_eq!(2, page1.nodes.len()); + + test_helpers::add_mixnode( + &mut rng, + deps.as_mut(), + env.clone(), + "addr3", + good_mixnode_pledge(), + ); + + // page1 still has the same 2 results + let another_page1 = + query_mixnode_bonds_paged(deps.as_ref(), None, Some(per_page)).unwrap(); + assert_eq!(2, another_page1.nodes.len()); + assert_eq!(page1, another_page1); + + // retrieving the next page should start after the last key on this page + let start_after = page1.start_next_after.unwrap(); + let page2 = query_mixnode_bonds_paged(deps.as_ref(), Some(start_after), Some(per_page)) + .unwrap(); + + assert_eq!(1, page2.nodes.len()); + + // save another one + test_helpers::add_mixnode(&mut rng, deps.as_mut(), env, "addr4", good_mixnode_pledge()); + + let page2 = query_mixnode_bonds_paged(deps.as_ref(), Some(start_after), Some(per_page)) + .unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.nodes.len()); + } + } + + #[cfg(test)] + mod mixnode_details { + use super::*; + use crate::support::tests::fixtures::good_mixnode_pledge; + + #[test] + fn obeys_limits() { + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + let limit = 2; + + test_helpers::add_dummy_mixnodes(&mut rng, deps.as_mut(), env, 1000); + let page1 = query_mixnodes_details_paged(deps.as_ref(), None, Some(limit)).unwrap(); + assert_eq!(limit, page1.nodes.len() as u32); + } + + #[test] + fn has_default_limit() { + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + test_helpers::add_dummy_mixnodes(&mut rng, deps.as_mut(), env, 1000); + + // query without explicitly setting a limit + let page1 = query_mixnodes_details_paged(deps.as_ref(), None, None).unwrap(); + + assert_eq!( + MIXNODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT, + page1.nodes.len() as u32 + ); + } + + #[test] + fn has_max_limit() { + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + test_helpers::add_dummy_mixnodes(&mut rng, deps.as_mut(), env, 1000); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000; + let page1 = + query_mixnodes_details_paged(deps.as_ref(), None, Some(crazy_limit)).unwrap(); + + // we default to a decent sized upper bound instead + assert_eq!( + MIXNODE_DETAILS_MAX_RETRIEVAL_LIMIT, + page1.nodes.len() as u32 + ); + } + + #[test] + fn pagination_works() { + // as we add mixnodes, we're always inserting them in ascending manner due to monotonically increasing id + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + test_helpers::add_mixnode( + &mut rng, + deps.as_mut(), + env.clone(), + "addr1", + good_mixnode_pledge(), + ); + + let per_page = 2; + let page1 = query_mixnodes_details_paged(deps.as_ref(), None, Some(per_page)).unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.nodes.len()); + + // save another + test_helpers::add_mixnode( + &mut rng, + deps.as_mut(), + env.clone(), + "addr2", + good_mixnode_pledge(), + ); + + // page1 should have 2 results on it + let page1 = query_mixnodes_details_paged(deps.as_ref(), None, Some(per_page)).unwrap(); + assert_eq!(2, page1.nodes.len()); + + test_helpers::add_mixnode( + &mut rng, + deps.as_mut(), + env.clone(), + "addr3", + good_mixnode_pledge(), + ); + + // page1 still has the same 2 results + let another_page1 = + query_mixnodes_details_paged(deps.as_ref(), None, Some(per_page)).unwrap(); + assert_eq!(2, another_page1.nodes.len()); + assert_eq!(page1, another_page1); + + // retrieving the next page should start after the last key on this page + let start_after = page1.start_next_after.unwrap(); + let page2 = + query_mixnodes_details_paged(deps.as_ref(), Some(start_after), Some(per_page)) + .unwrap(); + + assert_eq!(1, page2.nodes.len()); + + // save another one + test_helpers::add_mixnode(&mut rng, deps.as_mut(), env, "addr4", good_mixnode_pledge()); + + let page2 = + query_mixnodes_details_paged(deps.as_ref(), Some(start_after), Some(per_page)) + .unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.nodes.len()); + } + } + + #[cfg(test)] + mod unbonded_mixnodes { + use super::*; + use cosmwasm_std::Addr; + use mixnet_contract_common::mixnode::UnbondedMixnode; + + #[test] + fn obeys_limits() { + let mut deps = test_helpers::init_contract(); + let _env = mock_env(); + let mut rng = test_helpers::test_rng(); + let limit = 2; + + test_helpers::add_dummy_unbonded_mixnodes(&mut rng, deps.as_mut(), 1000); + let page1 = query_unbonded_mixnodes_paged(deps.as_ref(), None, Some(limit)).unwrap(); + assert_eq!(limit, page1.nodes.len() as u32); + } + + #[test] + fn has_default_limit() { + let mut deps = test_helpers::init_contract(); + let _env = mock_env(); + let mut rng = test_helpers::test_rng(); + test_helpers::add_dummy_unbonded_mixnodes(&mut rng, deps.as_mut(), 1000); + + // query without explicitly setting a limit + let page1 = query_unbonded_mixnodes_paged(deps.as_ref(), None, None).unwrap(); + + assert_eq!( + UNBONDED_MIXNODES_DEFAULT_RETRIEVAL_LIMIT, + page1.nodes.len() as u32 + ); + } + + #[test] + fn has_max_limit() { + let mut deps = test_helpers::init_contract(); + let _env = mock_env(); + let mut rng = test_helpers::test_rng(); + test_helpers::add_dummy_unbonded_mixnodes(&mut rng, deps.as_mut(), 1000); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000; + let page1 = + query_unbonded_mixnodes_paged(deps.as_ref(), None, Some(crazy_limit)).unwrap(); + + // we default to a decent sized upper bound instead + assert_eq!( + UNBONDED_MIXNODES_MAX_RETRIEVAL_LIMIT, + page1.nodes.len() as u32 + ); + } + + #[test] + fn pagination_works() { + fn add_unbonded(storage: &mut dyn Storage, id: NodeId) { + storage::unbonded_mixnodes() + .save( + storage, + id, + &UnbondedMixnode { + identity_key: format!("dummy{}", id), + owner: Addr::unchecked(format!("dummy{}", id)), + proxy: None, + unbonding_height: 123, + }, + ) + .unwrap(); + } + + // as we add mixnodes, we're always inserting them in ascending manner due to monotonically increasing id + let mut deps = test_helpers::init_contract(); + + add_unbonded(deps.as_mut().storage, 1); + + let per_page = 2; + let page1 = query_unbonded_mixnodes_paged(deps.as_ref(), None, Some(per_page)).unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.nodes.len()); + + // save another + add_unbonded(deps.as_mut().storage, 2); + + // page1 should have 2 results on it + let page1 = query_unbonded_mixnodes_paged(deps.as_ref(), None, Some(per_page)).unwrap(); + assert_eq!(2, page1.nodes.len()); + + add_unbonded(deps.as_mut().storage, 3); + + // page1 still has the same 2 results + let another_page1 = + query_unbonded_mixnodes_paged(deps.as_ref(), None, Some(per_page)).unwrap(); + assert_eq!(2, another_page1.nodes.len()); + assert_eq!(page1, another_page1); + + // retrieving the next page should start after the last key on this page + let start_after = page1.start_next_after.unwrap(); + let page2 = + query_unbonded_mixnodes_paged(deps.as_ref(), Some(start_after), Some(per_page)) + .unwrap(); + + assert_eq!(1, page2.nodes.len()); + + // save another one + add_unbonded(deps.as_mut().storage, 4); + let page2 = + query_unbonded_mixnodes_paged(deps.as_ref(), Some(start_after), Some(per_page)) + .unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.nodes.len()); + } + } + + #[cfg(test)] + mod unbonded_mixnodes_by_owner { + use super::*; + use cosmwasm_std::Addr; + use mixnet_contract_common::mixnode::UnbondedMixnode; + + fn add_unbonded_with_owner(storage: &mut dyn Storage, id: NodeId, owner: &str) { + storage::unbonded_mixnodes() + .save( + storage, + id, + &UnbondedMixnode { + identity_key: format!("dummy{}", id), + owner: Addr::unchecked(owner), + proxy: None, + unbonding_height: 123, + }, + ) + .unwrap(); + } + + #[test] + fn obeys_limits() { + let mut deps = test_helpers::init_contract(); + let _env = mock_env(); + let mut rng = test_helpers::test_rng(); + let limit = 2; + let owner = "owner"; + + test_helpers::add_dummy_unbonded_mixnodes_with_owner( + &mut rng, + deps.as_mut(), + owner, + 1000, + ); + let page1 = query_unbonded_mixnodes_by_owner_paged( + deps.as_ref(), + owner.into(), + None, + Some(limit), + ) + .unwrap(); + assert_eq!(limit, page1.nodes.len() as u32); + } + + #[test] + fn has_default_limit() { + let mut deps = test_helpers::init_contract(); + let _env = mock_env(); + let mut rng = test_helpers::test_rng(); + let owner = "owner"; + + test_helpers::add_dummy_unbonded_mixnodes_with_owner( + &mut rng, + deps.as_mut(), + owner, + 1000, + ); + + // query without explicitly setting a limit + let page1 = + query_unbonded_mixnodes_by_owner_paged(deps.as_ref(), owner.into(), None, None) + .unwrap(); + + assert_eq!( + UNBONDED_MIXNODES_DEFAULT_RETRIEVAL_LIMIT, + page1.nodes.len() as u32 + ); + } + + #[test] + fn has_max_limit() { + let mut deps = test_helpers::init_contract(); + let _env = mock_env(); + let mut rng = test_helpers::test_rng(); + let owner = "owner"; + + test_helpers::add_dummy_unbonded_mixnodes_with_owner( + &mut rng, + deps.as_mut(), + owner, + 1000, + ); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000; + let page1 = query_unbonded_mixnodes_by_owner_paged( + deps.as_ref(), + owner.into(), + None, + Some(crazy_limit), + ) + .unwrap(); + + // we default to a decent sized upper bound instead + assert_eq!( + UNBONDED_MIXNODES_MAX_RETRIEVAL_LIMIT, + page1.nodes.len() as u32 + ); + } + + #[test] + fn pagination_works() { + // as we add mixnodes, we're always inserting them in ascending manner due to monotonically increasing id + let mut deps = test_helpers::init_contract(); + let owner = "owner"; + add_unbonded_with_owner(deps.as_mut().storage, 1, owner); + + let per_page = 2; + let page1 = query_unbonded_mixnodes_by_owner_paged( + deps.as_ref(), + owner.into(), + None, + Some(per_page), + ) + .unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.nodes.len()); + + // save another + add_unbonded_with_owner(deps.as_mut().storage, 2, owner); + + // page1 should have 2 results on it + let page1 = query_unbonded_mixnodes_by_owner_paged( + deps.as_ref(), + owner.into(), + None, + Some(per_page), + ) + .unwrap(); + assert_eq!(2, page1.nodes.len()); + + add_unbonded_with_owner(deps.as_mut().storage, 3, owner); + + // page1 still has the same 2 results + let another_page1 = query_unbonded_mixnodes_by_owner_paged( + deps.as_ref(), + owner.into(), + None, + Some(per_page), + ) + .unwrap(); + assert_eq!(2, another_page1.nodes.len()); + assert_eq!(page1, another_page1); + + // retrieving the next page should start after the last key on this page + let start_after = page1.start_next_after.unwrap(); + let page2 = query_unbonded_mixnodes_by_owner_paged( + deps.as_ref(), + owner.into(), + Some(start_after), + Some(per_page), + ) + .unwrap(); + + assert_eq!(1, page2.nodes.len()); + + // save another one + add_unbonded_with_owner(deps.as_mut().storage, 4, owner); + let page2 = query_unbonded_mixnodes_by_owner_paged( + deps.as_ref(), + owner.into(), + Some(start_after), + Some(per_page), + ) + .unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.nodes.len()); + } + + #[test] + fn only_retrieves_nodes_with_specific_owner() { + let mut deps = test_helpers::init_contract(); + let owner1 = "owner1"; + let owner2 = "owner2"; + let owner3 = "owner3"; + let owner4 = "owner4"; + + add_unbonded_with_owner(deps.as_mut().storage, 1, owner1); + add_unbonded_with_owner(deps.as_mut().storage, 2, owner1); + add_unbonded_with_owner(deps.as_mut().storage, 3, owner2); + add_unbonded_with_owner(deps.as_mut().storage, 4, owner1); + add_unbonded_with_owner(deps.as_mut().storage, 5, owner3); + add_unbonded_with_owner(deps.as_mut().storage, 6, owner3); + add_unbonded_with_owner(deps.as_mut().storage, 7, owner4); + add_unbonded_with_owner(deps.as_mut().storage, 8, owner2); + add_unbonded_with_owner(deps.as_mut().storage, 9, owner1); + add_unbonded_with_owner(deps.as_mut().storage, 10, owner3); + + let expected_ids1 = vec![1, 2, 4, 9]; + let expected_ids2 = vec![3, 8]; + let expected_ids3 = vec![5, 6, 10]; + let expected_ids4 = vec![7]; + + let res1 = + query_unbonded_mixnodes_by_owner_paged(deps.as_ref(), owner1.into(), None, None) + .unwrap() + .nodes + .into_iter() + .map(|r| r.0) + .collect::>(); + assert_eq!(res1, expected_ids1); + + let res2 = + query_unbonded_mixnodes_by_owner_paged(deps.as_ref(), owner2.into(), None, None) + .unwrap() + .nodes + .into_iter() + .map(|r| r.0) + .collect::>(); + assert_eq!(res2, expected_ids2); + + let res3 = + query_unbonded_mixnodes_by_owner_paged(deps.as_ref(), owner3.into(), None, None) + .unwrap() + .nodes + .into_iter() + .map(|r| r.0) + .collect::>(); + assert_eq!(res3, expected_ids3); + + let res4 = + query_unbonded_mixnodes_by_owner_paged(deps.as_ref(), owner4.into(), None, None) + .unwrap() + .nodes + .into_iter() + .map(|r| r.0) + .collect::>(); + assert_eq!(res4, expected_ids4); + + let res5 = query_unbonded_mixnodes_by_owner_paged( + deps.as_ref(), + "doesnt-exist".into(), + None, + None, + ) + .unwrap() + .nodes + .into_iter() + .map(|r| r.0) + .collect::>(); + assert!(res5.is_empty()); + } + } + + #[cfg(test)] + mod unbonded_mixnodes_by_identity { + use super::*; + use cosmwasm_std::Addr; + use mixnet_contract_common::mixnode::UnbondedMixnode; + + fn add_unbonded_with_identity(storage: &mut dyn Storage, id: NodeId, identity: &str) { + storage::unbonded_mixnodes() + .save( + storage, + id, + &UnbondedMixnode { + identity_key: identity.to_string(), + owner: Addr::unchecked(format!("dummy{}", id)), + proxy: None, + unbonding_height: 123, + }, + ) + .unwrap(); + } + + #[test] + fn obeys_limits() { + let mut deps = test_helpers::init_contract(); + let _env = mock_env(); + let mut rng = test_helpers::test_rng(); + let limit = 2; + let identity = "foomp123"; + + test_helpers::add_dummy_unbonded_mixnodes_with_identity( + &mut rng, + deps.as_mut(), + identity, + 1000, + ); + let page1 = query_unbonded_mixnodes_by_identity_paged( + deps.as_ref(), + identity.into(), + None, + Some(limit), + ) + .unwrap(); + assert_eq!(limit, page1.nodes.len() as u32); + } + + #[test] + fn has_default_limit() { + let mut deps = test_helpers::init_contract(); + let _env = mock_env(); + let mut rng = test_helpers::test_rng(); + let identity = "foomp123"; + test_helpers::add_dummy_unbonded_mixnodes_with_identity( + &mut rng, + deps.as_mut(), + identity, + 1000, + ); + + // query without explicitly setting a limit + let page1 = query_unbonded_mixnodes_by_identity_paged( + deps.as_ref(), + identity.into(), + None, + None, + ) + .unwrap(); + + assert_eq!( + UNBONDED_MIXNODES_DEFAULT_RETRIEVAL_LIMIT, + page1.nodes.len() as u32 + ); + } + + #[test] + fn has_max_limit() { + let mut deps = test_helpers::init_contract(); + let _env = mock_env(); + let mut rng = test_helpers::test_rng(); + let identity = "foomp123"; + test_helpers::add_dummy_unbonded_mixnodes_with_identity( + &mut rng, + deps.as_mut(), + identity, + 1000, + ); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000; + let page1 = query_unbonded_mixnodes_by_identity_paged( + deps.as_ref(), + identity.into(), + None, + Some(crazy_limit), + ) + .unwrap(); + + // we default to a decent sized upper bound instead + assert_eq!( + UNBONDED_MIXNODES_MAX_RETRIEVAL_LIMIT, + page1.nodes.len() as u32 + ); + } + + #[test] + fn pagination_works() { + // as we add mixnodes, we're always inserting them in ascending manner due to monotonically increasing id + let mut deps = test_helpers::init_contract(); + let identity = "foomp123"; + + add_unbonded_with_identity(deps.as_mut().storage, 1, identity); + + let per_page = 2; + let page1 = query_unbonded_mixnodes_by_identity_paged( + deps.as_ref(), + identity.into(), + None, + Some(per_page), + ) + .unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.nodes.len()); + + // save another + add_unbonded_with_identity(deps.as_mut().storage, 2, identity); + + // page1 should have 2 results on it + let page1 = query_unbonded_mixnodes_by_identity_paged( + deps.as_ref(), + identity.into(), + None, + Some(per_page), + ) + .unwrap(); + assert_eq!(2, page1.nodes.len()); + + add_unbonded_with_identity(deps.as_mut().storage, 3, identity); + + // page1 still has the same 2 results + let another_page1 = query_unbonded_mixnodes_by_identity_paged( + deps.as_ref(), + identity.into(), + None, + Some(per_page), + ) + .unwrap(); + assert_eq!(2, another_page1.nodes.len()); + assert_eq!(page1, another_page1); + + // retrieving the next page should start after the last key on this page + let start_after = page1.start_next_after.unwrap(); + let page2 = query_unbonded_mixnodes_by_identity_paged( + deps.as_ref(), + identity.into(), + Some(start_after), + Some(per_page), + ) + .unwrap(); + + assert_eq!(1, page2.nodes.len()); + + // save another one + add_unbonded_with_identity(deps.as_mut().storage, 4, identity); + let page2 = query_unbonded_mixnodes_by_identity_paged( + deps.as_ref(), + identity.into(), + Some(start_after), + Some(per_page), + ) + .unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.nodes.len()); + } + + #[test] + fn only_retrieves_nodes_with_specific_identity_key() { + let mut deps = test_helpers::init_contract(); + let identity1 = "identity1"; + let identity2 = "identity2"; + let identity3 = "identity3"; + let identity4 = "identity4"; + + add_unbonded_with_identity(deps.as_mut().storage, 1, identity1); + add_unbonded_with_identity(deps.as_mut().storage, 2, identity1); + add_unbonded_with_identity(deps.as_mut().storage, 3, identity2); + add_unbonded_with_identity(deps.as_mut().storage, 4, identity1); + add_unbonded_with_identity(deps.as_mut().storage, 5, identity3); + add_unbonded_with_identity(deps.as_mut().storage, 6, identity3); + add_unbonded_with_identity(deps.as_mut().storage, 7, identity4); + add_unbonded_with_identity(deps.as_mut().storage, 8, identity2); + add_unbonded_with_identity(deps.as_mut().storage, 9, identity1); + add_unbonded_with_identity(deps.as_mut().storage, 10, identity3); + + let expected_ids1 = vec![1, 2, 4, 9]; + let expected_ids2 = vec![3, 8]; + let expected_ids3 = vec![5, 6, 10]; + let expected_ids4 = vec![7]; + + let res1 = query_unbonded_mixnodes_by_identity_paged( + deps.as_ref(), + identity1.into(), + None, + None, + ) + .unwrap() + .nodes + .into_iter() + .map(|r| r.0) + .collect::>(); + assert_eq!(res1, expected_ids1); + + let res2 = query_unbonded_mixnodes_by_identity_paged( + deps.as_ref(), + identity2.into(), + None, + None, + ) + .unwrap() + .nodes + .into_iter() + .map(|r| r.0) + .collect::>(); + assert_eq!(res2, expected_ids2); + + let res3 = query_unbonded_mixnodes_by_identity_paged( + deps.as_ref(), + identity3.into(), + None, + None, + ) + .unwrap() + .nodes + .into_iter() + .map(|r| r.0) + .collect::>(); + assert_eq!(res3, expected_ids3); + + let res4 = query_unbonded_mixnodes_by_identity_paged( + deps.as_ref(), + identity4.into(), + None, + None, + ) + .unwrap() + .nodes + .into_iter() + .map(|r| r.0) + .collect::>(); + assert_eq!(res4, expected_ids4); + + let res5 = query_unbonded_mixnodes_by_owner_paged( + deps.as_ref(), + "doesnt-exist".into(), + None, + None, + ) + .unwrap() + .nodes + .into_iter() + .map(|r| r.0) + .collect::>(); + assert!(res5.is_empty()); + } + } + + #[test] + fn query_for_owned_mixnode() { + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + let address = "mix-owner".to_string(); + + // when it doesnt exist + let res = query_owned_mixnode(deps.as_ref(), address.clone()).unwrap(); + assert!(res.mixnode_details.is_none()); + assert_eq!(address, res.address); + + // when it [fully] exists + let id = test_helpers::add_mixnode( + &mut rng, + deps.as_mut(), + env, + &address, + good_mixnode_pledge(), + ); + let res = query_owned_mixnode(deps.as_ref(), address.clone()).unwrap(); + let details = res.mixnode_details.unwrap(); + assert_eq!(address, details.bond_information.owner); + assert_eq!( + good_mixnode_pledge()[0], + details.bond_information.original_pledge + ); + assert_eq!(address, res.address); + + // when it partially exists, i.e. case when the operator unbonded, but there are still some pending delegates + // TODO: perhaps this should work slightly differently, to return the underlying mixnode rewarding? + + // manually adjust delegation info as to indicate the rewarding information shouldnt get removed + let mut rewarding_details = details.rewarding_details; + rewarding_details.delegates = Decimal::raw(12345); + rewarding_details.unique_delegations = 1; + rewards_storage::MIXNODE_REWARDING + .save(deps.as_mut().storage, id, &rewarding_details) + .unwrap(); + + pending_events::unbond_mixnode(deps.as_mut(), &mock_env(), id).unwrap(); + let res = query_owned_mixnode(deps.as_ref(), address.clone()).unwrap(); + assert!(res.mixnode_details.is_none()); + assert_eq!(address, res.address); + } + + #[test] + fn query_for_mixnode_details() { + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + // no node under this id + let res = query_mixnode_details(deps.as_ref(), 42).unwrap(); + assert!(res.mixnode_details.is_none()); + assert_eq!(42, res.mix_id); + + // it exists + let mix_id = + test_helpers::add_mixnode(&mut rng, deps.as_mut(), env, "foomp", good_mixnode_pledge()); + let res = query_mixnode_details(deps.as_ref(), mix_id).unwrap(); + let details = res.mixnode_details.unwrap(); + assert_eq!(mix_id, details.bond_information.id); + assert_eq!( + good_mixnode_pledge()[0], + details.bond_information.original_pledge + ); + assert_eq!(mix_id, res.mix_id); + } + + #[test] + fn query_for_mixnode_details_by_identity() { + let mut test = TestSetup::new(); + + // no node under this identity + let res = query_mixnode_details_by_identity(test.deps(), "foomp".into()).unwrap(); + assert!(res.is_none()); + + // it exists + let mix_id = test.add_dummy_mixnode("owner", None); + // this was already tested to be working : ) + let expected = query_mixnode_details(test.deps(), mix_id) + .unwrap() + .mixnode_details + .unwrap(); + let mix_identity = expected.bond_information.identity(); + + let res = query_mixnode_details_by_identity(test.deps(), mix_identity.into()).unwrap(); + assert_eq!(expected, res.unwrap()); + } + + #[test] + fn query_for_mixnode_rewarding_details() { + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + // no node under this id + let res = query_mixnode_rewarding_details(deps.as_ref(), 42).unwrap(); + assert!(res.rewarding_details.is_none()); + assert_eq!(42, res.mix_id); + + let mix_id = + test_helpers::add_mixnode(&mut rng, deps.as_mut(), env, "foomp", good_mixnode_pledge()); + let res = query_mixnode_rewarding_details(deps.as_ref(), mix_id).unwrap(); + let details = res.rewarding_details.unwrap(); + assert_eq!( + fixtures::mix_node_cost_params_fixture(), + details.cost_params + ); + assert_eq!(mix_id, res.mix_id); + } + + #[test] + fn query_for_unbonded_mixnode() { + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + let sender = "mix-owner"; + + // no node under this id + let res = query_unbonded_mixnode(deps.as_ref(), 42).unwrap(); + assert!(res.unbonded_info.is_none()); + assert_eq!(42, res.mix_id); + + // add and unbond the mixnode + let mix_id = + test_helpers::add_mixnode(&mut rng, deps.as_mut(), env, sender, good_mixnode_pledge()); + pending_events::unbond_mixnode(deps.as_mut(), &mock_env(), mix_id).unwrap(); + + let res = query_unbonded_mixnode(deps.as_ref(), mix_id).unwrap(); + assert_eq!(res.unbonded_info.unwrap().owner, sender); + assert_eq!(mix_id, res.mix_id); + } + + #[test] + fn query_for_stake_saturation() { + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + // no node under this id + let res = query_stake_saturation(deps.as_ref(), 42).unwrap(); + assert!(res.current_saturation.is_none()); + assert!(res.uncapped_saturation.is_none()); + assert_eq!(42, res.mix_id); + + let rewarding_params = rewards_storage::REWARDING_PARAMS + .load(deps.as_ref().storage) + .unwrap(); + let saturation_point = rewarding_params.interval.stake_saturation_point; + + let mix_id = + test_helpers::add_mixnode(&mut rng, deps.as_mut(), env, "foomp", good_mixnode_pledge()); + + // below saturation point + // there's only the base pledge without any delegation + let expected = + Decimal::from_atomics(good_mixnode_pledge()[0].amount, 0).unwrap() / saturation_point; + let res = query_stake_saturation(deps.as_ref(), mix_id).unwrap(); + assert_eq!(expected, res.current_saturation.unwrap()); + assert_eq!(expected, res.uncapped_saturation.unwrap()); + assert_eq!(mix_id, res.mix_id); + + // exactly at saturation point + let mut mix_rewarding = rewards_storage::MIXNODE_REWARDING + .load(deps.as_ref().storage, mix_id) + .unwrap(); + mix_rewarding.operator = saturation_point; + rewards_storage::MIXNODE_REWARDING + .save(deps.as_mut().storage, mix_id, &mix_rewarding) + .unwrap(); + + let res = query_stake_saturation(deps.as_ref(), mix_id).unwrap(); + assert_eq!(Decimal::one(), res.current_saturation.unwrap()); + assert_eq!(Decimal::one(), res.uncapped_saturation.unwrap()); + assert_eq!(mix_id, res.mix_id); + + // above the saturation point + let mut mix_rewarding = rewards_storage::MIXNODE_REWARDING + .load(deps.as_ref().storage, mix_id) + .unwrap(); + mix_rewarding.delegates = mix_rewarding.operator * Decimal::percent(150); + rewards_storage::MIXNODE_REWARDING + .save(deps.as_mut().storage, mix_id, &mix_rewarding) + .unwrap(); + + let res = query_stake_saturation(deps.as_ref(), mix_id).unwrap(); + assert_eq!(Decimal::one(), res.current_saturation.unwrap()); + assert_eq!(Decimal::percent(250), res.uncapped_saturation.unwrap()); + assert_eq!(mix_id, res.mix_id); + } +} diff --git a/contracts/mixnet/src/mixnodes/storage.rs b/contracts/mixnet/src/mixnodes/storage.rs index 7a96a6d6c6..45f9356d14 100644 --- a/contracts/mixnet/src/mixnodes/storage.rs +++ b/contracts/mixnet/src/mixnodes/storage.rs @@ -1,259 +1,213 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{StdResult, Storage, Uint128}; -use cw_storage_plus::{Index, IndexList, IndexedSnapshotMap, Map, Strategy, UniqueIndex}; -use mixnet_contract_common::{ - reward_params::NodeEpochRewards, Addr, Coin, IdentityKeyRef, Layer, MixNode, MixNodeBond, +use crate::constants::{ + LAYER_DISTRIBUTION_KEY, MIXNODES_IDENTITY_IDX_NAMESPACE, MIXNODES_OWNER_IDX_NAMESPACE, + MIXNODES_PK_NAMESPACE, MIXNODES_SPHINX_IDX_NAMESPACE, NODE_ID_COUNTER_KEY, + UNBONDED_MIXNODES_IDENTITY_IDX_NAMESPACE, UNBONDED_MIXNODES_OWNER_IDX_NAMESPACE, + UNBONDED_MIXNODES_PK_NAMESPACE, }; -use mixnet_contract_common::{SphinxKey, U128}; -use serde::{Deserialize, Serialize}; -use std::fmt::{Display, Formatter}; +use cosmwasm_std::{StdResult, Storage}; +use cw_storage_plus::{Index, IndexList, IndexedMap, Item, MultiIndex, UniqueIndex}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::mixnode::UnbondedMixnode; +use mixnet_contract_common::SphinxKey; +use mixnet_contract_common::{Addr, IdentityKey, Layer, LayerDistribution, MixNodeBond, NodeId}; -use crate::mixnet_contract_settings::storage::mix_denom; +// keeps track of `node_id -> IdentityKey, Owner, unbonding_height` so we'd known a bit more about past mixnodes +// if we ever decide it's too bloaty, we can deprecate it and start removing all data in +// subsequent migrations -// storage prefixes -const TOTAL_DELEGATION_NAMESPACE: &str = "td"; -const MIXNODES_PK_NAMESPACE: &str = "mn"; -const MIXNODES_PK_CHECKPOINTS: &str = "mn__check"; -const MIXNODES_PK_CHANGELOG: &str = "mn__change"; -const MIXNODES_OWNER_IDX_NAMESPACE: &str = "mno"; -const MIXNODES_SPHINX_IDX_NAMESPACE: &str = "mns"; -const MIXNODES_BOND_BLACKLIST_NAMESPACE: &str = "mbb"; +pub(crate) struct UnbondedMixnodeIndex<'a> { + pub(crate) owner: MultiIndex<'a, Addr, UnbondedMixnode, NodeId>, -const LAST_PM_UPDATE_NAMESPACE: &str = "lpm"; - -// paged retrieval limits for all queries and transactions -pub(crate) const BOND_PAGE_MAX_LIMIT: u32 = 75; -pub(crate) const BOND_PAGE_DEFAULT_LIMIT: u32 = 50; - -pub(crate) const TOTAL_DELEGATION: Map<'_, IdentityKeyRef<'_>, Uint128> = - Map::new(TOTAL_DELEGATION_NAMESPACE); - -pub(crate) const LAST_PM_UPDATE_TIME: Map<'_, IdentityKeyRef<'_>, u64> = - Map::new(LAST_PM_UPDATE_NAMESPACE); - -pub(crate) const MIXNODES_BOND_BLACKLIST: Map<'_, IdentityKeyRef<'_>, u8> = - Map::new(MIXNODES_BOND_BLACKLIST_NAMESPACE); - -pub(crate) struct MixnodeBondIndex<'a> { - pub(crate) owner: UniqueIndex<'a, Addr, StoredMixnodeBond>, - - pub(crate) sphinx_key: UniqueIndex<'a, SphinxKey, StoredMixnodeBond>, + pub(crate) identity_key: MultiIndex<'a, IdentityKey, UnbondedMixnode, NodeId>, } -// IndexList is just boilerplate code for fetching a struct's indexes -// note that from my understanding this will be converted into a macro at some point in the future -impl<'a> IndexList for MixnodeBondIndex<'a> { - fn get_indexes(&'_ self) -> Box> + '_> { - let v: Vec<&dyn Index> = vec![&self.owner, &self.sphinx_key]; +impl<'a> IndexList for UnbondedMixnodeIndex<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.owner, &self.identity_key]; Box::new(v.into_iter()) } } -// mixnodes() is the storage access function. -pub(crate) fn mixnodes<'a>( -) -> IndexedSnapshotMap<'a, IdentityKeyRef<'a>, StoredMixnodeBond, MixnodeBondIndex<'a>> { +pub(crate) fn unbonded_mixnodes<'a>( +) -> IndexedMap<'a, NodeId, UnbondedMixnode, UnbondedMixnodeIndex<'a>> { + let indexes = UnbondedMixnodeIndex { + owner: MultiIndex::new( + |d| d.owner.clone(), + UNBONDED_MIXNODES_PK_NAMESPACE, + UNBONDED_MIXNODES_OWNER_IDX_NAMESPACE, + ), + identity_key: MultiIndex::new( + |d| d.identity_key.clone(), + UNBONDED_MIXNODES_PK_NAMESPACE, + UNBONDED_MIXNODES_IDENTITY_IDX_NAMESPACE, + ), + }; + IndexedMap::new(UNBONDED_MIXNODES_PK_NAMESPACE, indexes) +} + +pub(crate) const LAYERS: Item<'_, LayerDistribution> = Item::new(LAYER_DISTRIBUTION_KEY); +pub const MIXNODE_ID_COUNTER: Item = Item::new(NODE_ID_COUNTER_KEY); + +pub(crate) struct MixnodeBondIndex<'a> { + pub(crate) owner: UniqueIndex<'a, Addr, MixNodeBond>, + + pub(crate) identity_key: UniqueIndex<'a, IdentityKey, MixNodeBond>, + + pub(crate) sphinx_key: UniqueIndex<'a, SphinxKey, MixNodeBond>, +} + +// IndexList is just boilerplate code for fetching a struct's indexes +// note that from my understanding this will be converted into a macro at some point in the future +impl<'a> IndexList for MixnodeBondIndex<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = + vec![&self.owner, &self.identity_key, &self.sphinx_key]; + Box::new(v.into_iter()) + } +} + +// mixnode_bonds() is the storage access function. +pub(crate) fn mixnode_bonds<'a>() -> IndexedMap<'a, NodeId, MixNodeBond, MixnodeBondIndex<'a>> { let indexes = MixnodeBondIndex { owner: UniqueIndex::new(|d| d.owner.clone(), MIXNODES_OWNER_IDX_NAMESPACE), + identity_key: UniqueIndex::new( + |d| d.mix_node.identity_key.clone(), + MIXNODES_IDENTITY_IDX_NAMESPACE, + ), sphinx_key: UniqueIndex::new( |d| d.mix_node.sphinx_key.clone(), MIXNODES_SPHINX_IDX_NAMESPACE, ), }; - IndexedSnapshotMap::new( - MIXNODES_PK_NAMESPACE, - MIXNODES_PK_CHECKPOINTS, - MIXNODES_PK_CHANGELOG, - Strategy::Selected, - indexes, - ) + IndexedMap::new(MIXNODES_PK_NAMESPACE, indexes) } -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] -pub struct StoredMixnodeBond { - pub pledge_amount: Coin, - pub owner: Addr, - pub layer: Layer, - pub block_height: u64, - pub mix_node: MixNode, - pub proxy: Option, - pub accumulated_rewards: Option, - pub epoch_rewards: Option, +pub fn decrement_layer_count( + storage: &mut dyn Storage, + layer: Layer, +) -> Result<(), MixnetContractError> { + let mut layers = LAYERS.load(storage)?; + layers.decrement_layer_count(layer)?; + Ok(LAYERS.save(storage, &layers)?) } -impl From for StoredMixnodeBond { - fn from(mixnode_bond: MixNodeBond) -> StoredMixnodeBond { - StoredMixnodeBond { - pledge_amount: mixnode_bond.pledge_amount, - owner: mixnode_bond.owner, - layer: mixnode_bond.layer, - block_height: mixnode_bond.block_height, - mix_node: mixnode_bond.mix_node, - proxy: mixnode_bond.proxy, - accumulated_rewards: mixnode_bond.accumulated_rewards, - epoch_rewards: None, - } - } +pub(crate) fn assign_layer(store: &mut dyn Storage) -> StdResult { + // load current distribution + let mut layers = LAYERS.load(store)?; + + // choose the one with fewest nodes + let fewest = layers.choose_with_fewest(); + + // increment the existing count + layers.increment_layer_count(fewest); + + // and resave it + LAYERS.save(store, &layers)?; + Ok(fewest) } -impl StoredMixnodeBond { - #[allow(clippy::too_many_arguments)] - pub(crate) fn new( - pledge_amount: Coin, - owner: Addr, - layer: Layer, - block_height: u64, - mix_node: MixNode, - proxy: Option, - accumulated_rewards: Option, - epoch_rewards: Option, - ) -> Self { - StoredMixnodeBond { - pledge_amount, - owner, - layer, - block_height, - mix_node, - proxy, - accumulated_rewards, - epoch_rewards, - } - } - - pub(crate) fn accumulated_rewards(&self) -> Uint128 { - self.accumulated_rewards.unwrap_or_else(Uint128::zero) - } - - pub(crate) fn attach_delegation(self, total_delegation: Uint128) -> MixNodeBond { - MixNodeBond { - total_delegation: Coin { - denom: self.pledge_amount.denom.clone(), - amount: total_delegation, - }, - pledge_amount: self.pledge_amount, - owner: self.owner, - layer: self.layer, - block_height: self.block_height, - mix_node: self.mix_node, - proxy: self.proxy, - accumulated_rewards: self.accumulated_rewards, - } - } - - pub(crate) fn identity(&self) -> &String { - &self.mix_node.identity_key - } - - pub(crate) fn pledge_amount(&self) -> Coin { - self.pledge_amount.clone() - } - - pub fn profit_margin(&self) -> U128 { - U128::from_num(self.mix_node.profit_margin_percent) / U128::from_num(100) - } +pub(crate) fn next_mixnode_id_counter(store: &mut dyn Storage) -> StdResult { + let id: NodeId = MIXNODE_ID_COUNTER.may_load(store)?.unwrap_or_default() + 1; + MIXNODE_ID_COUNTER.save(store, &id)?; + Ok(id) } -impl Display for StoredMixnodeBond { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "amount: {}, owner: {}, identity: {}", - self.pledge_amount, self.owner, self.mix_node.identity_key - ) - } -} - -pub(crate) fn read_full_mixnode_bond( - storage: &dyn Storage, - mix_identity: IdentityKeyRef<'_>, -) -> StdResult> { - let stored_bond = mixnodes().may_load(storage, mix_identity)?; - match stored_bond { - None => Ok(None), - Some(stored_bond) => { - let total_delegation = TOTAL_DELEGATION.may_load(storage, mix_identity)?; - Ok(Some(MixNodeBond { - pledge_amount: stored_bond.pledge_amount, - total_delegation: Coin { - denom: mix_denom(storage)?, - amount: total_delegation.unwrap_or_default(), - }, - owner: stored_bond.owner, - layer: stored_bond.layer, - block_height: stored_bond.block_height, - mix_node: stored_bond.mix_node, - proxy: stored_bond.proxy, - accumulated_rewards: stored_bond.accumulated_rewards, - })) - } - } +pub(crate) fn initialise_storage(storage: &mut dyn Storage) -> StdResult<()> { + LAYERS.save(storage, &LayerDistribution::default()) } #[cfg(test)] mod tests { - use super::super::storage; use super::*; - use crate::support::tests; - use crate::support::tests::fixtures::TEST_COIN_DENOM; - use crate::support::tests::test_helpers::init_contract; - use cosmwasm_std::testing::MockStorage; - use cosmwasm_std::{coin, Addr, Uint128}; - use mixnet_contract_common::{IdentityKey, MixNode}; + use crate::support::tests::test_helpers; + use cosmwasm_std::testing::mock_dependencies; #[test] - fn mixnode_single_read_retrieval() { - let mut storage = MockStorage::new(); - let bond1 = tests::fixtures::stored_mixnode_bond_fixture("owner1"); - let bond2 = tests::fixtures::stored_mixnode_bond_fixture("owner2"); - mixnodes().save(&mut storage, "bond1", &bond1, 1).unwrap(); - mixnodes().save(&mut storage, "bond2", &bond2, 1).unwrap(); + fn decrementing_layer() { + let mut deps = test_helpers::init_contract(); - let res1 = mixnodes().load(&storage, "bond1").unwrap(); - let res2 = mixnodes().load(&storage, "bond2").unwrap(); - assert_eq!(bond1, res1); - assert_eq!(bond2, res2); + // we never underflow, if it were to happen we're going to return an error instead + assert_eq!( + Err(MixnetContractError::OverflowSubtraction { + minuend: 0, + subtrahend: 1 + }), + decrement_layer_count(deps.as_mut().storage, Layer::One) + ); + + LAYERS + .save( + deps.as_mut().storage, + &LayerDistribution { + layer1: 3, + layer2: 2, + layer3: 1, + }, + ) + .unwrap(); + + assert!(decrement_layer_count(deps.as_mut().storage, Layer::One).is_ok()); + assert!(decrement_layer_count(deps.as_mut().storage, Layer::Two).is_ok()); + assert!(decrement_layer_count(deps.as_mut().storage, Layer::Three).is_ok()); + + assert!(decrement_layer_count(deps.as_mut().storage, Layer::One).is_ok()); + assert!(decrement_layer_count(deps.as_mut().storage, Layer::Two).is_ok()); + assert!(decrement_layer_count(deps.as_mut().storage, Layer::Three).is_err()); + + assert!(decrement_layer_count(deps.as_mut().storage, Layer::One).is_ok()); + assert!(decrement_layer_count(deps.as_mut().storage, Layer::Two).is_err()); + assert!(decrement_layer_count(deps.as_mut().storage, Layer::Three).is_err()); + + assert!(decrement_layer_count(deps.as_mut().storage, Layer::One).is_err()); + assert!(decrement_layer_count(deps.as_mut().storage, Layer::Two).is_err()); + assert!(decrement_layer_count(deps.as_mut().storage, Layer::Three).is_err()); } #[test] - fn reading_mixnode_bond() { - let mut mock_storage = init_contract().storage; - let node_owner: Addr = Addr::unchecked("node-owner"); - let node_identity: IdentityKey = "nodeidentity".into(); + fn assigning_layer() { + let mut deps = test_helpers::init_contract(); - // produces a None if target mixnode doesn't exist - let res = storage::read_full_mixnode_bond(&mock_storage, &node_identity).unwrap(); - assert!(res.is_none()); - - // returns appropriate value otherwise - let pledge_value = 1000000000; - - let mixnode_bond = StoredMixnodeBond { - pledge_amount: coin(pledge_value, TEST_COIN_DENOM), - owner: node_owner, - layer: Layer::One, - block_height: 12_345, - mix_node: MixNode { - identity_key: node_identity.clone(), - ..tests::fixtures::mix_node_fixture() - }, - proxy: None, - accumulated_rewards: None, - epoch_rewards: None, + let layers = LayerDistribution { + layer1: 3, + layer2: 2, + layer3: 1, }; + LAYERS.save(deps.as_mut().storage, &layers).unwrap(); - storage::mixnodes() - .save(&mut mock_storage, &node_identity, &mixnode_bond, 1) - .unwrap(); + // always assigns layer with fewest nodes + assert_eq!(Layer::Three, assign_layer(deps.as_mut().storage).unwrap()); + assert_eq!(2, LAYERS.load(deps.as_ref().storage).unwrap().layer3); + // we have 3, 2, 2, so the 2nd layer should get chosen now + assert_eq!(Layer::Two, assign_layer(deps.as_mut().storage).unwrap()); + assert_eq!(3, LAYERS.load(deps.as_ref().storage).unwrap().layer2); + + // 3, 3, 2, so 3rd one again + assert_eq!(Layer::Three, assign_layer(deps.as_mut().storage).unwrap()); + assert_eq!(3, LAYERS.load(deps.as_ref().storage).unwrap().layer3); + } + + #[test] + fn next_id() { + let mut deps = test_helpers::init_contract(); + + for i in 1u32..1000 { + assert_eq!(i, next_mixnode_id_counter(deps.as_mut().storage).unwrap()); + } + } + + #[test] + fn initialising() { + let mut deps = mock_dependencies(); + assert!(LAYERS.load(deps.as_ref().storage).is_err()); + + initialise_storage(deps.as_mut().storage).unwrap(); assert_eq!( - Uint128::new(pledge_value), - storage::read_full_mixnode_bond(&mock_storage, node_identity.as_str()) - .unwrap() - .unwrap() - .pledge_amount - .amount + LayerDistribution::default(), + LAYERS.load(deps.as_ref().storage).unwrap() ); } } diff --git a/contracts/mixnet/src/mixnodes/transactions.rs b/contracts/mixnet/src/mixnodes/transactions.rs index 0b8b6153e3..c32f944d6c 100644 --- a/contracts/mixnet/src/mixnodes/transactions.rs +++ b/contracts/mixnet/src/mixnodes/transactions.rs @@ -1,60 +1,40 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::storage::{self, LAST_PM_UPDATE_TIME}; -use crate::error::ContractError; -use crate::mixnet_contract_settings::storage::{self as mixnet_params_storage, mix_denom}; -use crate::mixnodes::layer_queries::query_layer_distribution; -use crate::mixnodes::storage::StoredMixnodeBond; -use crate::support::helpers::{ensure_no_existing_bond, validate_node_identity_signature}; -use cosmwasm_std::{ - wasm_execute, Addr, Api, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, Storage, Uint128, +use super::storage; +use crate::interval::storage as interval_storage; +use crate::interval::storage::push_new_interval_event; +use crate::mixnet_contract_settings::storage as mixnet_params_storage; +use crate::mixnodes::helpers::{must_get_mixnode_bond_by_owner, save_new_mixnode}; +use crate::support::helpers::{ + ensure_bonded, ensure_no_existing_bond, ensure_proxy_match, validate_node_identity_signature, + validate_pledge, }; +use cosmwasm_std::{Addr, Coin, DepsMut, Env, MessageInfo, Response}; +use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ - new_checkpoint_mixnodes_event, new_mixnode_bonding_event, new_mixnode_unbonding_event, + new_mixnode_bonding_event, new_mixnode_config_update_event, + new_mixnode_pending_cost_params_update_event, new_pending_mixnode_unbonding_event, }; -use mixnet_contract_common::{IdentityKeyRef, MixNode}; -use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; -use vesting_contract_common::one_ucoin; - -const MIN_PM_UPDATE_INTERVAL: u64 = 60 * 60 * 24 * 30; // one month roughly - -pub fn try_checkpoint_mixnodes( - storage: &mut dyn Storage, - block_height: u64, - info: MessageInfo, -) -> Result { - let state = mixnet_params_storage::CONTRACT_STATE.load(storage)?; - // check if this is executed by the permitted validator, if not reject the transaction - if info.sender != state.rewarding_validator_address { - return Err(ContractError::Unauthorized); - } - - crate::mixnodes::storage::mixnodes().add_checkpoint(storage, block_height)?; - - Ok(Response::new().add_event(new_checkpoint_mixnodes_event(block_height))) -} +use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; +use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData}; +use mixnet_contract_common::MixNode; pub fn try_add_mixnode( deps: DepsMut<'_>, env: Env, info: MessageInfo, mix_node: MixNode, + cost_params: MixNodeCostParams, owner_signature: String, -) -> Result { - // check if the pledge contains any funds of the appropriate denomination - let minimum_pledge = mixnet_params_storage::CONTRACT_STATE - .load(deps.storage)? - .params - .minimum_mixnode_pledge; - let pledge = validate_mixnode_pledge(info.funds, minimum_pledge, mix_denom(deps.storage)?)?; - +) -> Result { _try_add_mixnode( deps, env, mix_node, - pledge, - info.sender.as_str(), + cost_params, + info.funds, + info.sender, owner_signature, None, ) @@ -65,872 +45,578 @@ pub fn try_add_mixnode_on_behalf( env: Env, info: MessageInfo, mix_node: MixNode, + cost_params: MixNodeCostParams, owner: String, owner_signature: String, -) -> Result { - // check if the pledge contains any funds of the appropriate denomination - let minimum_pledge = mixnet_params_storage::CONTRACT_STATE - .load(deps.storage)? - .params - .minimum_mixnode_pledge; - let pledge = validate_mixnode_pledge(info.funds, minimum_pledge, mix_denom(deps.storage)?)?; - +) -> Result { let proxy = info.sender; + let owner = deps.api.addr_validate(&owner)?; _try_add_mixnode( deps, env, mix_node, - pledge, - &owner, + cost_params, + info.funds, + owner, owner_signature, Some(proxy), ) } -pub fn is_blacklisted( - storage: &dyn Storage, - identity: IdentityKeyRef, -) -> Result { - Ok(storage::MIXNODES_BOND_BLACKLIST - .may_load(storage, identity)? - .is_some()) -} - +// I'm not entirely sure how to deal with this warning at the current moment +#[allow(clippy::too_many_arguments)] fn _try_add_mixnode( deps: DepsMut<'_>, env: Env, - mix_node: MixNode, - pledge_amount: Coin, - owner: &str, + mixnode: MixNode, + cost_params: MixNodeCostParams, + pledge: Vec, + owner: Addr, owner_signature: String, proxy: Option, -) -> Result { - let owner = deps.api.addr_validate(owner)?; +) -> Result { + // 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)?; + // 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(deps.storage, &owner)?; - if is_blacklisted(deps.storage, &mix_node.identity_key)? { - return Err(ContractError::MixnodeBlacklisted { - identity: mix_node.identity_key, - }); - }; - - // We don't have to check lower bound as its an u8 - if mix_node.profit_margin_percent > 100 { - return Err(ContractError::InvalidProfitMarginPercent( - mix_node.profit_margin_percent, - )); - } - - // check if somebody else has already bonded a mixnode with this identity - if let Some(existing_bond) = - storage::mixnodes().may_load(deps.storage, &mix_node.identity_key)? - { - if existing_bond.owner != owner { - return Err(ContractError::DuplicateMixnode { - owner: existing_bond.owner, - }); - } - } + // there's no need to explicitly check whether there already exists mixnode with the same + // identity or sphinx keys as this is going to be done implicitly when attempting to save + // the bond information due to `UniqueIndex` constraint defined on those fields. // check if this sender actually owns the mixnode by checking the signature validate_node_identity_signature( deps.as_ref(), &owner, owner_signature, - &mix_node.identity_key, + &mixnode.identity_key, )?; - let layer_distribution = query_layer_distribution(deps.as_ref())?; - let layer = layer_distribution.choose_with_fewest(); - - let stored_bond = StoredMixnodeBond::new( - pledge_amount.clone(), + let node_identity = mixnode.identity_key.clone(); + let (node_id, layer) = save_new_mixnode( + deps.storage, + env, + mixnode, + cost_params, owner.clone(), - layer, - env.block.height, - mix_node, proxy.clone(), - None, - None, - ); - - // technically we don't have to set the total_delegation bucket, but it makes things easier - // in different places that we can guarantee that if node exists, so does the data behind the total delegation - let identity = stored_bond.identity(); - storage::mixnodes().save(deps.storage, identity, &stored_bond, env.block.height)?; - - // if this is a fresh mixnode - write 0 total delegation, otherwise, don't touch it since the node has just rebonded - if storage::TOTAL_DELEGATION - .may_load(deps.storage, identity)? - .is_none() - { - storage::TOTAL_DELEGATION.save(deps.storage, identity, &Uint128::zero())?; - } - - storage::LAST_PM_UPDATE_TIME.save(deps.storage, identity, &env.block.time.seconds())?; - - mixnet_params_storage::increment_layer_count(deps.storage, stored_bond.layer)?; + pledge.clone(), + )?; Ok(Response::new().add_event(new_mixnode_bonding_event( &owner, &proxy, - &pledge_amount, - identity, - stored_bond.layer, + &pledge, + &node_identity, + node_id, + layer, ))) } pub fn try_remove_mixnode_on_behalf( - env: &Env, - storage: &mut dyn Storage, - api: &dyn Api, + deps: DepsMut<'_>, info: MessageInfo, owner: String, -) -> Result { +) -> Result { let proxy = info.sender; - _try_remove_mixnode(env, storage, api, &owner, Some(proxy), true) + let owner = deps.api.addr_validate(&owner)?; + _try_remove_mixnode(deps, owner, Some(proxy)) } pub fn try_remove_mixnode( - env: &Env, - storage: &mut dyn Storage, - api: &dyn Api, + deps: DepsMut<'_>, info: MessageInfo, -) -> Result { - _try_remove_mixnode(env, storage, api, info.sender.as_ref(), None, true) +) -> Result { + _try_remove_mixnode(deps, info.sender, None) } pub(crate) fn _try_remove_mixnode( - env: &Env, - storage: &mut dyn Storage, - api: &dyn Api, - owner: &str, + deps: DepsMut<'_>, + owner: Addr, proxy: Option, - collect_rewards: bool, -) -> Result { - let owner = api.addr_validate(owner)?; +) -> Result { + let existing_bond = storage::mixnode_bonds() + .idx + .owner + .item(deps.storage, owner.clone())? + .ok_or(MixnetContractError::NoAssociatedMixNodeBond { owner })? + .1; - if collect_rewards { - crate::rewards::transactions::_try_compound_operator_reward( - storage, - api, - env.block.height, - &owner, - None, - )?; - } + // see if the proxy matches + ensure_proxy_match(&proxy, &existing_bond.proxy)?; + ensure_bonded(&existing_bond)?; - // try to find the node of the sender - let mixnode_bond = match storage::mixnodes().idx.owner.item(storage, owner.clone())? { - Some(record) => record.1, - None => return Err(ContractError::NoAssociatedMixNodeBond { owner }), + // set `is_unbonding` field + let mut updated_bond = existing_bond.clone(); + updated_bond.is_unbonding = true; + storage::mixnode_bonds().replace( + deps.storage, + existing_bond.id, + Some(&updated_bond), + Some(&existing_bond), + )?; + + // push the event to execute it at the end of the epoch + let epoch_event = PendingEpochEventData::UnbondMixnode { + mix_id: existing_bond.id, }; + interval_storage::push_new_epoch_event(deps.storage, &epoch_event)?; - if proxy != mixnode_bond.proxy { - return Err(ContractError::ProxyMismatch { - existing: mixnode_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(), - amount: vec![mixnode_bond.pledge_amount()], - }; - - // remove the bond - storage::mixnodes().remove(storage, mixnode_bond.identity(), env.block.height)?; - - // decrement layer count - mixnet_params_storage::decrement_layer_count(storage, mixnode_bond.layer)?; - - let mut response = Response::new(); - - if let Some(proxy) = &proxy { - let msg = VestingContractExecuteMsg::TrackUnbondMixnode { - owner: owner.as_str().to_string(), - amount: mixnode_bond.pledge_amount(), - }; - - let track_unbond_message = wasm_execute(proxy, &msg, vec![one_ucoin(mix_denom(storage)?)])?; - response = response.add_message(track_unbond_message); - } - - let response = response.add_message(return_tokens); - - Ok(response.add_event(new_mixnode_unbonding_event( - &owner, - &proxy, - &mixnode_bond.pledge_amount, - mixnode_bond.identity(), - ))) + Ok( + Response::new().add_event(new_pending_mixnode_unbonding_event( + &existing_bond.owner, + &existing_bond.proxy, + existing_bond.identity(), + existing_bond.id, + )), + ) } pub(crate) fn try_update_mixnode_config( deps: DepsMut<'_>, - env: Env, info: MessageInfo, - profit_margin_percent: u8, -) -> Result { - let owner = deps.api.addr_validate(info.sender.as_ref())?; - _try_update_mixnode_config(deps, env, profit_margin_percent, owner, None) + 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, - env: Env, info: MessageInfo, - profit_margin_percent: u8, + new_config: MixNodeConfigUpdate, owner: String, -) -> Result { +) -> Result { let owner = deps.api.addr_validate(&owner)?; - let proxy = deps.api.addr_validate(info.sender.as_ref())?; - _try_update_mixnode_config(deps, env, profit_margin_percent, owner, Some(proxy)) + let proxy = info.sender; + _try_update_mixnode_config(deps, new_config, owner, Some(proxy)) } pub(crate) fn _try_update_mixnode_config( deps: DepsMut, - env: Env, - profit_margin_percent: u8, + new_config: MixNodeConfigUpdate, owner: Addr, proxy: Option, -) -> Result { - let mixnode_bond = storage::mixnodes() - .idx - .owner - .item(deps.storage, owner.clone())? - .ok_or(ContractError::NoAssociatedMixNodeBond { owner })? - .1; - let mix_denom = mix_denom(deps.storage)?; +) -> Result { + let existing_bond = must_get_mixnode_bond_by_owner(deps.storage, &owner)?; - if proxy != mixnode_bond.proxy { - return Err(ContractError::ProxyMismatch { - existing: mixnode_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()), - }); - } + ensure_bonded(&existing_bond)?; + ensure_proxy_match(&proxy, &existing_bond.proxy)?; - let last_update_time = storage::LAST_PM_UPDATE_TIME - .load(deps.storage, mixnode_bond.identity()) - .unwrap_or(0); + let cfg_update_event = + new_mixnode_config_update_event(existing_bond.id, &owner, &proxy, &new_config); - let current_block_time = env.block.time.seconds(); + 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; + updated_bond.mix_node.verloc_port = new_config.verloc_port; + updated_bond.mix_node.http_api_port = new_config.http_api_port; + updated_bond.mix_node.version = new_config.version; - if current_block_time - last_update_time < MIN_PM_UPDATE_INTERVAL { - return Err(ContractError::UpdatePMTooSoon { - last_update_time, - current_block_time, - }); - } - - // We don't have to check lower bound as its an u8 - if profit_margin_percent > 100 { - return Err(ContractError::InvalidProfitMarginPercent( - profit_margin_percent, - )); - } - - storage::mixnodes().update( + storage::mixnode_bonds().replace( deps.storage, - mixnode_bond.identity(), - env.block.height, - |mixnode_bond_opt| { - mixnode_bond_opt - .map(|mut mixnode_bond| { - mixnode_bond.mix_node.profit_margin_percent = profit_margin_percent; - mixnode_bond.block_height = env.block.height; - mixnode_bond - }) - .ok_or(ContractError::NoBondFound { - mix_denom: mix_denom.clone(), - }) - }, + existing_bond.id, + Some(&updated_bond), + Some(&existing_bond), )?; - LAST_PM_UPDATE_TIME.save(deps.storage, mixnode_bond.identity(), ¤t_block_time)?; - - let mut response = Response::new(); - - if let Some(proxy) = proxy { - // Returns one_ucoin proxy had to send in order to execute the contract to contract transaction, this is potentially leaky as anyone can say that they're a proxy, - // and they could potentially leak 1 unym per transaction, altough I'm pretty sure transaction fees make that silly. - let return_one_ucoint = BankMsg::Send { - to_address: proxy.as_str().to_string(), - amount: vec![one_ucoin(mix_denom)], - }; - response = response.add_message(return_one_ucoint); - } - - Ok(response) + Ok(Response::new().add_event(cfg_update_event)) } -fn validate_mixnode_pledge( - mut pledge: Vec, - minimum_pledge: Uint128, - mix_denom: String, -) -> Result { - // check if anything was put as bond - if pledge.is_empty() { - return Err(ContractError::NoBondFound { mix_denom }); - } +pub(crate) fn try_update_mixnode_cost_params( + deps: DepsMut<'_>, + info: MessageInfo, + new_costs: MixNodeCostParams, +) -> Result { + let owner = info.sender; + _try_update_mixnode_cost_params(deps, new_costs, owner, None) +} - if pledge.len() > 1 { - return Err(ContractError::MultipleDenoms); - } +pub(crate) fn try_update_mixnode_cost_params_on_behalf( + deps: DepsMut, + info: MessageInfo, + new_costs: MixNodeCostParams, + owner: String, +) -> Result { + let owner = deps.api.addr_validate(&owner)?; + let proxy = info.sender; + _try_update_mixnode_cost_params(deps, new_costs, owner, Some(proxy)) +} - // check that the denomination is correct - if pledge[0].denom != mix_denom { - return Err(ContractError::WrongDenom { mix_denom }); - } +pub(crate) fn _try_update_mixnode_cost_params( + deps: DepsMut, + 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)?; - // check that we have at least MIXNODE_BOND coins in our pledge - if pledge[0].amount < minimum_pledge { - return Err(ContractError::InsufficientMixNodeBond { - received: pledge[0].amount.into(), - minimum: minimum_pledge.into(), - }); - } + ensure_proxy_match(&proxy, &existing_bond.proxy)?; + ensure_bonded(&existing_bond)?; - Ok(pledge.pop().unwrap()) + let cosmos_event = + new_mixnode_pending_cost_params_update_event(existing_bond.id, &owner, &proxy, &new_costs); + + // push the interval event + let interval_event = PendingIntervalEventData::ChangeMixCostParams { + mix_id: existing_bond.id, + new_costs, + }; + push_new_interval_event(deps.storage, &interval_event)?; + + Ok(Response::new().add_event(cosmos_event)) } #[cfg(test)] pub mod tests { use super::*; - use crate::contract::{execute, query, INITIAL_MIXNODE_PLEDGE}; - use crate::error::ContractError; - use crate::mixnodes::transactions::validate_mixnode_pledge; - use crate::support::tests; - use crate::support::tests::fixtures::TEST_COIN_DENOM; - use crate::support::tests::test_helpers; + use crate::contract::execute; + use crate::mixnet_contract_settings::storage::minimum_mixnode_pledge; + use crate::mixnodes::helpers::get_mixnode_details_by_id; + use crate::support::tests::fixtures::{good_mixnode_pledge, TEST_COIN_DENOM}; + use crate::support::tests::{fixtures, test_helpers}; use cosmwasm_std::testing::{mock_env, mock_info}; - use cosmwasm_std::{coins, BankMsg, Response}; - use cosmwasm_std::{from_binary, Addr, Uint128}; - use mixnet_contract_common::{ - ExecuteMsg, Layer, LayerDistribution, MixNode, PagedMixnodeResponse, QueryMsg, - }; - use rand::thread_rng; + use cosmwasm_std::{Order, StdResult, Uint128}; + use mixnet_contract_common::{ExecuteMsg, Layer, LayerDistribution, Percent}; #[test] fn mixnode_add() { let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + let sender = "alice"; + let minimum_pledge = minimum_mixnode_pledge(deps.as_ref().storage).unwrap(); + let mut insufficient_pledge = minimum_pledge.clone(); + insufficient_pledge.amount -= Uint128::new(1000); // if we don't send enough funds - let insufficient_bond = Into::::into(INITIAL_MIXNODE_PLEDGE) - 1; - let info = mock_info("anyone", &coins(insufficient_bond, TEST_COIN_DENOM)); - let (msg, _) = tests::messages::valid_bond_mixnode_msg("anyone"); + let info = mock_info(sender, &[insufficient_pledge.clone()]); + let (mixnode, sig) = test_helpers::mixnode_with_signature(&mut rng, sender); + let cost_params = fixtures::mix_node_cost_params_fixture(); // we are informed that we didn't send enough funds - let result = execute(deps.as_mut(), mock_env(), info, msg); + let result = try_add_mixnode( + deps.as_mut(), + env.clone(), + info, + mixnode.clone(), + cost_params.clone(), + sig.clone(), + ); assert_eq!( result, - Err(ContractError::InsufficientMixNodeBond { - received: insufficient_bond, - minimum: INITIAL_MIXNODE_PLEDGE.into(), + Err(MixnetContractError::InsufficientPledge { + received: insufficient_pledge, + minimum: minimum_pledge.clone(), }) ); - // no mixnode was inserted into the topology - let res = query( - deps.as_ref(), - mock_env(), - QueryMsg::GetMixNodes { - start_after: None, - limit: Option::from(2), - }, - ) - .unwrap(); - let page: PagedMixnodeResponse = from_binary(&res).unwrap(); - assert_eq!(0, page.nodes.len()); + // if the signature provided is invalid, the bonding also fails + let info = mock_info(sender, &[minimum_pledge]); - // if we send enough funds - let info = mock_info("anyone", &tests::fixtures::good_mixnode_pledge()); - let (msg, (identity, sphinx)) = tests::messages::valid_bond_mixnode_msg("anyone"); - - // we get back a message telling us everything was OK - let execute_response = execute(deps.as_mut(), mock_env(), info, msg); - assert!(execute_response.is_ok()); - - // we can query topology and the new node is there - let query_response = query( - deps.as_ref(), - mock_env(), - QueryMsg::GetMixNodes { - start_after: None, - limit: Option::from(2), - }, - ) - .unwrap(); - let page: PagedMixnodeResponse = from_binary(&query_response).unwrap(); - assert_eq!(1, page.nodes.len()); - assert_eq!( - &MixNode { - identity_key: identity, - sphinx_key: sphinx, - ..tests::fixtures::mix_node_fixture() - }, - page.nodes[0].mix_node() + let result = try_add_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + mixnode.clone(), + cost_params.clone(), + "bad-signature".into(), ); + assert!(matches!( + result, + Err(MixnetContractError::MalformedEd25519Signature(..)) + )); // if there was already a mixnode bonded by particular user - let info = mock_info("foomper", &tests::fixtures::good_mixnode_pledge()); - let (msg, _) = tests::messages::valid_bond_mixnode_msg("foomper"); - execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - let info = mock_info("foomper", &tests::fixtures::good_mixnode_pledge()); - let (msg, _) = tests::messages::valid_bond_mixnode_msg("foomper"); + test_helpers::add_mixnode( + &mut rng, + deps.as_mut(), + env.clone(), + sender, + fixtures::good_mixnode_pledge(), + ); // it fails - let execute_response = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(Err(ContractError::AlreadyOwnsMixnode), execute_response); - - // bonding fails if the user already owns a gateway - test_helpers::add_gateway( - "gateway-owner", - tests::fixtures::good_gateway_pledge(), + let result = try_add_mixnode( deps.as_mut(), + env.clone(), + info, + mixnode, + cost_params.clone(), + sig, + ); + assert_eq!(Err(MixnetContractError::AlreadyOwnsMixnode), result); + + // the same holds if the user already owns a gateway + let sender2 = "gateway-owner"; + + test_helpers::add_gateway( + &mut rng, + deps.as_mut(), + env.clone(), + sender2, + tests::fixtures::good_gateway_pledge(), ); - let info = mock_info("gateway-owner", &tests::fixtures::good_mixnode_pledge()); - let (msg, _) = tests::messages::valid_bond_mixnode_msg("gateway-owner"); + let info = mock_info(sender2, &tests::fixtures::good_mixnode_pledge()); + let (mixnode, sig) = test_helpers::mixnode_with_signature(&mut rng, sender2); - let execute_response = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(execute_response, Err(ContractError::AlreadyOwnsGateway)); + let result = try_add_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + mixnode.clone(), + cost_params.clone(), + sig.clone(), + ); + assert_eq!(Err(MixnetContractError::AlreadyOwnsGateway), result); // but after he unbonds it, it's all fine again - let info = mock_info("gateway-owner", &[]); let msg = ExecuteMsg::UnbondGateway {}; - execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); - let info = mock_info("gateway-owner", &tests::fixtures::good_mixnode_pledge()); - let (msg, _) = tests::messages::valid_bond_mixnode_msg("gateway-owner"); + let result = try_add_mixnode(deps.as_mut(), env, info, mixnode, cost_params, sig); + assert!(result.is_ok()); - let execute_response = execute(deps.as_mut(), mock_env(), info, msg); - assert!(execute_response.is_ok()); + // make sure we got assigned the next id (note: we have already bonded a mixnode before in this test) + let bond = must_get_mixnode_bond_by_owner(deps.as_ref().storage, &Addr::unchecked(sender2)) + .unwrap(); + assert_eq!(2, bond.id); - // adding another node from another account, but with the same IP, should fail (or we would have a weird state). Is that right? Think about this, not sure yet. - // if we attempt to register a second node from the same address, should we get an error? It would probably be polite. - } + // and make sure we're on layer 2 (because it was the next empty one) + assert_eq!(Layer::Two, bond.layer); - #[test] - fn adding_mixnode_without_existing_owner_succeeds() { - let mut deps = test_helpers::init_contract(); - - let info = mock_info("mix-owner", &tests::fixtures::good_mixnode_pledge()); - - // before the execution the node had no associated owner - assert!(storage::mixnodes() - .idx - .owner - .item(deps.as_ref().storage, Addr::unchecked("mix-owner")) - .unwrap() - .is_none()); - - let (msg, (identity, _)) = tests::messages::valid_bond_mixnode_msg("mix-owner"); - - // it's all fine, owner is saved - let execute_response = execute(deps.as_mut(), mock_env(), info, msg); - assert!(execute_response.is_ok()); - - assert_eq!( - &identity, - storage::mixnodes() - .idx - .owner - .item(deps.as_ref().storage, Addr::unchecked("mix-owner")) - .unwrap() - .unwrap() - .1 - .identity() - ); - } - - #[test] - fn adding_mixnode_with_existing_owner_fails() { - let mut deps = test_helpers::init_contract(); - - let identity = test_helpers::add_mixnode( - "mix-owner", - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - - // request fails giving the existing owner address in the message - let info = mock_info( - "mix-owner-pretender", - &tests::fixtures::good_mixnode_pledge(), - ); - let msg = ExecuteMsg::BondMixnode { - mix_node: MixNode { - identity_key: identity, - ..tests::fixtures::mix_node_fixture() - }, - owner_signature: "foomp".to_string(), + // and see if the layer distribution matches our expectation + let expected = LayerDistribution { + layer1: 1, + layer2: 1, + layer3: 0, }; - - let execute_response = execute(deps.as_mut(), mock_env(), info, msg); assert_eq!( - Err(ContractError::DuplicateMixnode { - owner: Addr::unchecked("mix-owner") - }), - execute_response - ); - } - - #[test] - fn adding_mixnode_with_existing_unchanged_owner_fails() { - let mut deps = test_helpers::init_contract(); - - test_helpers::add_mixnode( - "mix-owner", - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - - let info = mock_info("mix-owner", &tests::fixtures::good_mixnode_pledge()); - let (msg, _) = tests::messages::valid_bond_mixnode_msg("mix-owner"); - - let res = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(Err(ContractError::AlreadyOwnsMixnode), res); - } - - #[test] - fn adding_mixnode_updates_layer_distribution() { - let mut deps = test_helpers::init_contract(); - - assert_eq!( - LayerDistribution::default(), - mixnet_params_storage::LAYERS.load(&deps.storage).unwrap(), - ); - - test_helpers::add_mixnode( - "mix1", - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - - assert_eq!( - LayerDistribution { - layer1: 1, - ..Default::default() - }, - mixnet_params_storage::LAYERS.load(&deps.storage).unwrap() - ); + expected, + storage::LAYERS.load(deps.as_ref().storage).unwrap() + ) } #[test] fn mixnode_remove() { let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); - // try un-registering when no nodes exist yet - let info = mock_info("anyone", &[]); - let msg = ExecuteMsg::UnbondMixnode {}; - let result = execute(deps.as_mut(), mock_env(), info, msg); - - // we're told that there is no node for our address - assert_eq!( - result, - Err(ContractError::NoAssociatedMixNodeBond { - owner: Addr::unchecked("anyone") - }) - ); - - // let's add a node owned by bob - test_helpers::add_mixnode("bob", tests::fixtures::good_mixnode_pledge(), deps.as_mut()); - - // attempt to un-register fred's node, which doesn't exist - let info = mock_info("fred", &[]); - let msg = ExecuteMsg::UnbondMixnode {}; - let result = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!( - result, - Err(ContractError::NoAssociatedMixNodeBond { - owner: Addr::unchecked("fred") - }) - ); - - // bob's node is still there - let nodes = tests::queries::get_mix_nodes(&mut deps); - assert_eq!(1, nodes.len()); - assert_eq!("bob", nodes[0].owner().clone()); - - // add a node owned by fred - let fred_identity = test_helpers::add_mixnode( - "fred", - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - - // let's make sure we now have 2 nodes: - assert_eq!(2, tests::queries::get_mix_nodes(&mut deps).len()); - - // un-register fred's node - let info = mock_info("fred", &[]); - let msg = ExecuteMsg::UnbondMixnode {}; - let remove_fred = execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); - - // we should see a funds transfer from the contract back to fred - let expected_message = BankMsg::Send { - to_address: String::from(info.sender), - amount: tests::fixtures::good_mixnode_pledge(), - }; - - // run the executor and check that we got back the correct results - let expected_response = - Response::new() - .add_message(expected_message) - .add_event(new_mixnode_unbonding_event( - &Addr::unchecked("fred"), - &None, - &tests::fixtures::good_mixnode_pledge()[0], - &fred_identity, - )); - - assert_eq!(expected_response, remove_fred); - - // only 1 node now exists, owned by bob: - let mix_node_bonds = tests::queries::get_mix_nodes(&mut deps); - - assert_eq!(1, mix_node_bonds.len()); - assert_eq!(&Addr::unchecked("bob"), mix_node_bonds[0].owner()); - } - - #[test] - fn removing_mixnode_clears_ownership() { - let mut deps = test_helpers::init_contract(); - - let info = mock_info("mix-owner", &tests::fixtures::good_mixnode_pledge()); - let (bond_msg, (identity, _)) = tests::messages::valid_bond_mixnode_msg("mix-owner"); - execute(deps.as_mut(), mock_env(), info, bond_msg.clone()).unwrap(); - - assert_eq!( - &identity, - storage::mixnodes() - .idx - .owner - .item(deps.as_ref().storage, Addr::unchecked("mix-owner")) - .unwrap() - .unwrap() - .1 - .identity() - ); - - let info = mock_info("mix-owner", &[]); - let msg = ExecuteMsg::UnbondMixnode {}; - - let response = execute(deps.as_mut(), mock_env(), info, msg); - - assert!(response.is_ok()); - - assert!(storage::mixnodes() - .idx - .owner - .item(deps.as_ref().storage, Addr::unchecked("mix-owner")) - .unwrap() - .is_none()); - - // and since it's removed, it can be reclaimed - let info = mock_info("mix-owner", &tests::fixtures::good_mixnode_pledge()); - - assert!(execute(deps.as_mut(), mock_env(), info, bond_msg).is_ok()); - assert_eq!( - &identity, - storage::mixnodes() - .idx - .owner - .item(deps.as_ref().storage, Addr::unchecked("mix-owner")) - .unwrap() - .unwrap() - .1 - .identity() - ); - } - - #[test] - fn updating_mixnode_config() { - let sender = "bob"; - let mut env = mock_env(); - let mut deps = test_helpers::init_contract(); + let sender = "alice"; let info = mock_info(sender, &[]); - // try updating a non existing mixnode bond - let msg = ExecuteMsg::UpdateMixnodeConfig { - profit_margin_percent: 10, - }; - let ret = execute(deps.as_mut(), env.clone(), info.clone(), msg); + // trying to remove your mixnode fails if you never had one in the first place + let res = try_remove_mixnode(deps.as_mut(), info.clone()); assert_eq!( - ret, - Err(ContractError::NoAssociatedMixNodeBond { + res, + Err(MixnetContractError::NoAssociatedMixNodeBond { owner: Addr::unchecked(sender) }) ); - test_helpers::add_mixnode( + let mix_id = + test_helpers::add_mixnode(&mut rng, deps.as_mut(), env, sender, good_mixnode_pledge()); + + // attempted to remove on behalf with invalid proxy (current is `None`) + let res = try_remove_mixnode_on_behalf( + deps.as_mut(), + mock_info("proxy", &[]), + sender.to_string(), + ); + assert_eq!( + res, + Err(MixnetContractError::ProxyMismatch { + existing: "None".to_string(), + incoming: "proxy".to_string() + }) + ); + + // "normal" unbonding succeeds and unbonding event is pushed to the pending epoch events + let res = try_remove_mixnode(deps.as_mut(), info.clone()); + assert!(res.is_ok()); + let mut pending_events = interval_storage::PENDING_EPOCH_EVENTS + .range(deps.as_ref().storage, None, None, Order::Ascending) + .collect::>>() + .unwrap(); + assert_eq!(pending_events.len(), 1); + let event = pending_events.pop().unwrap(); + assert_eq!(1, event.0); + assert_eq!(PendingEpochEventData::UnbondMixnode { mix_id }, event.1); + + // but fails if repeated (since the node is already in the "unbonding" state)( + let res = try_remove_mixnode(deps.as_mut(), info); + assert_eq!( + res, + Err(MixnetContractError::MixnodeIsUnbonding { node_id: mix_id }) + ) + } + + #[test] + fn updating_mixnode_config() { + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + let sender = "alice"; + let info = mock_info(sender, &[]); + 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(), + }; + + // try updating a non existing mixnode bond + let res = try_update_mixnode_config(deps.as_mut(), info.clone(), update.clone()); + assert_eq!( + res, + Err(MixnetContractError::NoAssociatedMixNodeBond { + owner: Addr::unchecked(sender) + }) + ); + + let mix_id = test_helpers::add_mixnode( + &mut rng, + deps.as_mut(), + env, sender, tests::fixtures::good_mixnode_pledge(), + ); + + // attempted to remove on behalf with invalid proxy (current is `None`) + let res = try_update_mixnode_config_on_behalf( deps.as_mut(), + mock_info("proxy", &[]), + update.clone(), + sender.to_string(), ); - - // check the initial profit margin is set to the fixture value - let fixture_profit_margin = tests::fixtures::mix_node_fixture().profit_margin_percent; assert_eq!( - fixture_profit_margin, - storage::mixnodes() - .idx - .owner - .item(deps.as_ref().storage, Addr::unchecked("bob")) - .unwrap() - .unwrap() - .1 - .mix_node - .profit_margin_percent + res, + Err(MixnetContractError::ProxyMismatch { + existing: "None".to_string(), + incoming: "proxy".to_string() + }) ); + // "normal" update succeeds + let res = try_update_mixnode_config(deps.as_mut(), info.clone(), update.clone()); + assert!(res.is_ok()); - env.block.time = env.block.time.plus_seconds(MIN_PM_UPDATE_INTERVAL + 1); + // and the config has actually been updated + let mix = must_get_mixnode_bond_by_owner(deps.as_ref().storage, &Addr::unchecked(sender)) + .unwrap(); + assert_eq!(mix.mix_node.host, update.host); + assert_eq!(mix.mix_node.mix_port, update.mix_port); + assert_eq!(mix.mix_node.verloc_port, update.verloc_port); + assert_eq!(mix.mix_node.http_api_port, update.http_api_port); + assert_eq!(mix.mix_node.version, update.version); - // try updating with an invalid value - let profit_margin_percent = 101; - let msg = ExecuteMsg::UpdateMixnodeConfig { - profit_margin_percent, - }; - let ret = execute(deps.as_mut(), env.clone(), info.clone(), msg); + // but we cannot perform any updates whilst the mixnode is already unbonding + try_remove_mixnode(deps.as_mut(), info.clone()).unwrap(); + let res = try_update_mixnode_config(deps.as_mut(), info, update); assert_eq!( - ret, - Err(ContractError::InvalidProfitMarginPercent( - profit_margin_percent - )) - ); - - let profit_margin_percent = fixture_profit_margin + 10; - let msg = ExecuteMsg::UpdateMixnodeConfig { - profit_margin_percent, - }; - execute(deps.as_mut(), env, info, msg).unwrap(); - assert_eq!( - profit_margin_percent, - storage::mixnodes() - .idx - .owner - .item(deps.as_ref().storage, Addr::unchecked("bob")) - .unwrap() - .unwrap() - .1 - .mix_node - .profit_margin_percent - ); + res, + Err(MixnetContractError::MixnodeIsUnbonding { node_id: mix_id }) + ) } #[test] - fn validating_mixnode_bond() { - // you must send SOME funds - let result = validate_mixnode_pledge( - Vec::new(), - INITIAL_MIXNODE_PLEDGE, - TEST_COIN_DENOM.to_string(), - ); - assert_eq!( - result, - Err(ContractError::NoBondFound { - mix_denom: TEST_COIN_DENOM.to_string() - }) - ); - - // you must send at least 100 coins... - let mut bond = tests::fixtures::good_mixnode_pledge(); - bond[0].amount = INITIAL_MIXNODE_PLEDGE.checked_sub(Uint128::new(1)).unwrap(); - let result = validate_mixnode_pledge( - bond.clone(), - INITIAL_MIXNODE_PLEDGE, - TEST_COIN_DENOM.to_string(), - ); - assert_eq!( - result, - Err(ContractError::InsufficientMixNodeBond { - received: Into::::into(INITIAL_MIXNODE_PLEDGE) - 1, - minimum: INITIAL_MIXNODE_PLEDGE.into(), - }) - ); - - // more than that is still fine - let mut bond = tests::fixtures::good_mixnode_pledge(); - bond[0].amount = INITIAL_MIXNODE_PLEDGE + Uint128::new(1); - let result = validate_mixnode_pledge( - bond.clone(), - INITIAL_MIXNODE_PLEDGE, - TEST_COIN_DENOM.to_string(), - ); - assert!(result.is_ok()); - - // it must be sent in the defined denom! - let mut bond = tests::fixtures::good_mixnode_pledge(); - bond[0].denom = "baddenom".to_string(); - let result = validate_mixnode_pledge( - bond.clone(), - INITIAL_MIXNODE_PLEDGE, - TEST_COIN_DENOM.to_string(), - ); - assert_eq!( - result, - Err(ContractError::WrongDenom { - mix_denom: TEST_COIN_DENOM.to_string() - }) - ); - - let mut bond = tests::fixtures::good_mixnode_pledge(); - bond[0].denom = "foomp".to_string(); - let result = validate_mixnode_pledge( - bond.clone(), - INITIAL_MIXNODE_PLEDGE, - TEST_COIN_DENOM.to_string(), - ); - assert_eq!( - result, - Err(ContractError::WrongDenom { - mix_denom: TEST_COIN_DENOM.to_string() - }) - ); - } - - #[test] - fn choose_layer_mix_node() { + fn updating_mixnode_cost_params() { let mut deps = test_helpers::init_contract(); - let alice_identity = test_helpers::add_mixnode( - "alice", - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + let sender = "alice"; + let info = mock_info(sender, &[]); + let update = MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(42).unwrap(), + interval_operating_cost: Coin::new(12345678, TEST_COIN_DENOM), + }; + + // try updating a non existing mixnode bond + let res = try_update_mixnode_cost_params(deps.as_mut(), info.clone(), update.clone()); + assert_eq!( + res, + Err(MixnetContractError::NoAssociatedMixNodeBond { + owner: Addr::unchecked(sender) + }) ); - let bob_identity = - test_helpers::add_mixnode("bob", tests::fixtures::good_mixnode_pledge(), deps.as_mut()); - let bonded_mix_nodes = tests::queries::get_mix_nodes(&mut deps); - let alice_node = bonded_mix_nodes - .iter() - .find(|m| m.owner == "alice") - .cloned() - .unwrap(); - let bob_node = bonded_mix_nodes - .iter() - .find(|m| m.owner == "bob") - .cloned() - .unwrap(); + let mix_id = test_helpers::add_mixnode( + &mut rng, + deps.as_mut(), + env.clone(), + sender, + tests::fixtures::good_mixnode_pledge(), + ); - assert_eq!(alice_node.mix_node.identity_key, alice_identity); - assert_eq!(alice_node.layer, Layer::One); - assert_eq!(bob_node.mix_node.identity_key, bob_identity); - assert_eq!(bob_node.layer, mixnet_contract_common::Layer::Two); + // attempted to remove on behalf with invalid proxy (current is `None`) + let res = try_update_mixnode_cost_params_on_behalf( + deps.as_mut(), + mock_info("proxy", &[]), + update.clone(), + sender.to_string(), + ); + assert_eq!( + res, + Err(MixnetContractError::ProxyMismatch { + existing: "None".to_string(), + incoming: "proxy".to_string() + }) + ); + // "normal" update succeeds + let res = try_update_mixnode_cost_params(deps.as_mut(), info.clone(), update.clone()); + assert!(res.is_ok()); + + // see if the event has been pushed onto the queue + let mut pending_events = interval_storage::PENDING_INTERVAL_EVENTS + .range(deps.as_ref().storage, None, None, Order::Ascending) + .collect::>>() + .unwrap(); + assert_eq!(pending_events.len(), 1); + let event = pending_events.pop().unwrap(); + assert_eq!(1, event.0); + assert_eq!( + PendingIntervalEventData::ChangeMixCostParams { + mix_id: mix_id, + new_costs: update.clone() + }, + event.1 + ); + + // execute the event + test_helpers::execute_all_pending_events(deps.as_mut(), env); + + // and see if the config has actually been updated + let mix = get_mixnode_details_by_id(deps.as_ref().storage, mix_id) + .unwrap() + .unwrap(); + assert_eq!(mix.rewarding_details.cost_params, update); + + // but we cannot perform any updates whilst the mixnode is already unbonding + try_remove_mixnode(deps.as_mut(), info.clone()).unwrap(); + let res = try_update_mixnode_cost_params(deps.as_mut(), info, update); + assert_eq!( + res, + Err(MixnetContractError::MixnodeIsUnbonding { node_id: mix_id }) + ) } #[test] fn adding_mixnode_with_duplicate_sphinx_key_errors_out() { let mut deps = test_helpers::init_contract(); + let mut rng = test_helpers::test_rng(); - let keypair1 = crypto::asymmetric::identity::KeyPair::new(&mut thread_rng()); - let keypair2 = crypto::asymmetric::identity::KeyPair::new(&mut thread_rng()); + let keypair1 = crypto::asymmetric::identity::KeyPair::new(&mut rng); + let keypair2 = crypto::asymmetric::identity::KeyPair::new(&mut rng); let sig1 = keypair1.private_key().sign_text("alice"); let sig2 = keypair1.private_key().sign_text("bob"); @@ -942,68 +628,35 @@ pub mod tests { mix_port: 1234, verloc_port: 1234, http_api_port: 1234, - sphinx_key: crypto::asymmetric::encryption::KeyPair::new(&mut thread_rng()) + sphinx_key: crypto::asymmetric::encryption::KeyPair::new(&mut rng) .public_key() .to_base58_string(), identity_key: keypair1.public_key().to_base58_string(), version: "v0.1.2.3".to_string(), - profit_margin_percent: 10, - }; - - assert!( - try_add_mixnode(deps.as_mut(), mock_env(), info_alice, mixnode.clone(), sig1).is_ok() - ); - - mixnode.identity_key = keypair2.public_key().to_base58_string(); - - // change identity but reuse sphinx key - assert!(try_add_mixnode(deps.as_mut(), mock_env(), info_bob, mixnode, sig2).is_err()); - } - - #[test] - fn updating_pm_too_often_fails() { - use super::MIN_PM_UPDATE_INTERVAL; - - let mut deps = test_helpers::init_contract(); - let mut env = mock_env(); - - let keypair1 = crypto::asymmetric::identity::KeyPair::new(&mut thread_rng()); - let sig1 = keypair1.private_key().sign_text("alice"); - - let info_alice = mock_info("alice", &tests::fixtures::good_mixnode_pledge()); - - let mixnode = MixNode { - host: "1.2.3.4".to_string(), - mix_port: 1234, - verloc_port: 1234, - http_api_port: 1234, - sphinx_key: crypto::asymmetric::encryption::KeyPair::new(&mut thread_rng()) - .public_key() - .to_base58_string(), - identity_key: keypair1.public_key().to_base58_string(), - version: "v0.1.2.3".to_string(), - profit_margin_percent: 10, }; + let cost_params = fixtures::mix_node_cost_params_fixture(); assert!(try_add_mixnode( deps.as_mut(), mock_env(), - info_alice.clone(), + info_alice, mixnode.clone(), + cost_params.clone(), sig1 ) .is_ok()); - env.block.time = env.block.time.plus_seconds(MIN_PM_UPDATE_INTERVAL - 1); + mixnode.identity_key = keypair2.public_key().to_base58_string(); - // fails if too soon after bonding - assert!( - try_update_mixnode_config(deps.as_mut(), env.clone(), info_alice.clone(), 20).is_err() - ); - - env.block.time = env.block.time.plus_seconds(2); - - // succeds after some time - assert!(try_update_mixnode_config(deps.as_mut(), env, info_alice, 20).is_ok()); + // change identity but reuse sphinx key + assert!(try_add_mixnode( + deps.as_mut(), + mock_env(), + info_bob, + mixnode, + cost_params, + sig2 + ) + .is_err()); } } diff --git a/contracts/mixnet/src/rewards/helpers.rs b/contracts/mixnet/src/rewards/helpers.rs index bd0fc5df7d..43cbcfa1ae 100644 --- a/contracts/mixnet/src/rewards/helpers.rs +++ b/contracts/mixnet/src/rewards/helpers.rs @@ -1,22 +1,278 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use super::storage; -use crate::error::ContractError; -use cosmwasm_std::Storage; -use mixnet_contract_common::{IdentityKey, RewardingResult, RewardingStatus}; +use crate::delegations::storage as delegations_storage; +use crate::interval::storage as interval_storage; +use cosmwasm_std::{Coin, Decimal, Storage}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::mixnode::{MixNodeDetails, MixNodeRewarding}; +use mixnet_contract_common::Delegation; -pub(crate) fn update_rewarding_status( - storage: &mut dyn Storage, - interval_id: u32, - mix_identity: IdentityKey, - rewarding_result: RewardingResult, -) -> Result<(), ContractError> { - storage::REWARDING_STATUS.save( - storage, - (interval_id, mix_identity), - &RewardingStatus::Complete(rewarding_result), - )?; +/// Recomputes rewarding parameters (such as staking supply, saturation point, etc) based on +/// pending changes currently stored in `PENDING_REWARD_POOL_CHANGE`. +pub(crate) fn apply_reward_pool_changes( + store: &mut dyn Storage, +) -> Result<(), MixnetContractError> { + let mut rewarding_params = storage::REWARDING_PARAMS.load(store)?; + let pending_pool_change = storage::PENDING_REWARD_POOL_CHANGE.load(store)?; + let interval = interval_storage::current_interval(store)?; + + let reward_pool = rewarding_params.interval.reward_pool - pending_pool_change.removed + + pending_pool_change.added; + let staking_supply = rewarding_params.interval.staking_supply + pending_pool_change.removed; + let epoch_reward_budget = reward_pool + / Decimal::from_atomics(interval.epochs_in_interval(), 0).unwrap() + * rewarding_params.interval.interval_pool_emission; + let stake_saturation_point = + staking_supply / Decimal::from_atomics(rewarding_params.rewarded_set_size, 0).unwrap(); + + rewarding_params.interval.reward_pool = reward_pool; + rewarding_params.interval.staking_supply = staking_supply; + rewarding_params.interval.epoch_reward_budget = epoch_reward_budget; + rewarding_params.interval.stake_saturation_point = stake_saturation_point; + + storage::PENDING_REWARD_POOL_CHANGE.save(store, &Default::default())?; + storage::REWARDING_PARAMS.save(store, &rewarding_params)?; Ok(()) } + +pub(crate) fn withdraw_operator_reward( + store: &mut dyn Storage, + mix_details: MixNodeDetails, +) -> Result { + let mix_id = mix_details.mix_id(); + let mut mix_rewarding = mix_details.rewarding_details; + let original_pledge = mix_details.bond_information.original_pledge; + let reward = mix_rewarding.withdraw_operator_reward(&original_pledge); + + // save updated rewarding info + storage::MIXNODE_REWARDING.save(store, mix_id, &mix_rewarding)?; + Ok(reward) +} + +pub(crate) fn withdraw_delegator_reward( + store: &mut dyn Storage, + delegation: Delegation, + mut mix_rewarding: MixNodeRewarding, +) -> Result { + let mix_id = delegation.node_id; + let mut updated_delegation = delegation.clone(); + let reward = mix_rewarding.withdraw_delegator_reward(&mut updated_delegation)?; + + // save updated delegation and mix rewarding info + delegations_storage::delegations().replace( + store, + delegation.storage_key(), + Some(&updated_delegation), + Some(&delegation), + )?; + storage::MIXNODE_REWARDING.save(store, mix_id, &mix_rewarding)?; + Ok(reward) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mixnodes::helpers::get_mixnode_details_by_id; + use crate::rewards::models::RewardPoolChange; + use crate::support::tests::test_helpers::{assert_decimals, performance, TestSetup}; + use cosmwasm_std::Uint128; + use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; + + #[test] + fn applying_reward_pool_changes() { + let mut test = TestSetup::new(); + + let epochs_in_interval = test.current_interval().epochs_in_interval(); + let epochs_in_interval_dec = Decimal::from_atomics(epochs_in_interval, 0).unwrap(); + let start_rewarding_params = test.rewarding_params(); + + // nothing changes if pending changes are empty + apply_reward_pool_changes(test.deps_mut().storage).unwrap(); + assert_eq!(start_rewarding_params, test.rewarding_params()); + + // normal case of having distributed some rewards + let distributed_rewards = Decimal::from_atomics(100_000_000u32, 0).unwrap(); + storage::PENDING_REWARD_POOL_CHANGE + .save( + test.deps_mut().storage, + &RewardPoolChange { + removed: distributed_rewards, + added: Default::default(), + }, + ) + .unwrap(); + apply_reward_pool_changes(test.deps_mut().storage).unwrap(); + + let updated_rewarding_params = test.rewarding_params(); + + // updates reward pool + assert_eq!( + updated_rewarding_params.interval.reward_pool, + start_rewarding_params.interval.reward_pool - distributed_rewards + ); + + // updates staking supply + assert_eq!( + updated_rewarding_params.interval.staking_supply, + start_rewarding_params.interval.staking_supply + distributed_rewards + ); + + // updates epoch rewarding budget + assert_eq!( + updated_rewarding_params.interval.epoch_reward_budget, + updated_rewarding_params.interval.reward_pool / epochs_in_interval_dec + * updated_rewarding_params.interval.interval_pool_emission + ); + + // updates stake saturation point + assert_eq!( + updated_rewarding_params.interval.stake_saturation_point, + updated_rewarding_params.interval.staking_supply + / Decimal::from_atomics(updated_rewarding_params.rewarded_set_size, 0).unwrap() + ); + + // resets changes back to 0 + assert_eq!( + storage::PENDING_REWARD_POOL_CHANGE + .load(test.deps().storage) + .unwrap(), + Default::default() + ); + + // future case of having to also increase the reward pool + let added_credentials = Decimal::from_atomics(50_000_000u32, 0).unwrap(); + storage::PENDING_REWARD_POOL_CHANGE + .save( + test.deps_mut().storage, + &RewardPoolChange { + removed: distributed_rewards, + added: added_credentials, + }, + ) + .unwrap(); + apply_reward_pool_changes(test.deps_mut().storage).unwrap(); + + let updated_rewarding_params2 = test.rewarding_params(); + + // updates reward pool + assert_eq!( + updated_rewarding_params2.interval.reward_pool, + updated_rewarding_params.interval.reward_pool - distributed_rewards + added_credentials + ); + + // updates staking supply + assert_eq!( + updated_rewarding_params2.interval.staking_supply, + updated_rewarding_params.interval.staking_supply + distributed_rewards + ); + + // updates epoch rewarding budget + assert_eq!( + updated_rewarding_params2.interval.epoch_reward_budget, + updated_rewarding_params2.interval.reward_pool / epochs_in_interval_dec + * updated_rewarding_params2.interval.interval_pool_emission + ); + + // updates stake saturation point + assert_eq!( + updated_rewarding_params2.interval.stake_saturation_point, + updated_rewarding_params2.interval.staking_supply + / Decimal::from_atomics(updated_rewarding_params2.rewarded_set_size, 0).unwrap() + ); + + // resets changes back to 0 + assert_eq!( + storage::PENDING_REWARD_POOL_CHANGE + .load(test.deps().storage) + .unwrap(), + Default::default() + ); + } + + #[test] + fn withdrawing_operator_reward() { + let mut test = TestSetup::new(); + + let pledge = Uint128::new(250_000_000); + let pledge_dec = Decimal::from_atomics(250_000_000u32, 0).unwrap(); + let mix_id = test.add_dummy_mixnode("mix-owner", Some(pledge)); + + // no rewards + let mix_details = get_mixnode_details_by_id(test.deps().storage, mix_id) + .unwrap() + .unwrap(); + let res = withdraw_operator_reward(test.deps_mut().storage, mix_details).unwrap(); + assert_eq!(res.amount, Uint128::zero()); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + let dist1 = test.reward_with_distribution(mix_id, performance(100.0)); + + test.skip_to_next_epoch_end(); + let dist2 = test.reward_with_distribution(mix_id, performance(100.0)); + + test.skip_to_next_epoch_end(); + let dist3 = test.reward_with_distribution(mix_id, performance(100.0)); + + let mix_details = get_mixnode_details_by_id(test.deps().storage, mix_id) + .unwrap() + .unwrap(); + let res = withdraw_operator_reward(test.deps_mut().storage, mix_details).unwrap(); + assert_eq!( + res.amount, + truncate_reward_amount(dist1.operator + dist2.operator + dist3.operator) + ); + let updated = test.mix_rewarding(mix_id); + assert_eq!(updated.operator, pledge_dec) + } + + #[test] + fn withdrawing_delegator_reward() { + let mut test = TestSetup::new(); + + let delegation_amount = Uint128::new(2500_000_000); + let delegation_dec = Decimal::from_atomics(2500_000_000u32, 0).unwrap(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + let delegator = "delegator"; + test.add_immediate_delegation(delegator, delegation_amount, mix_id); + + // no rewards + let delegation = test.delegation(mix_id, delegator, &None); + let mix_rewarding = test.mix_rewarding(mix_id); + let res = + withdraw_delegator_reward(test.deps_mut().storage, delegation, mix_rewarding).unwrap(); + assert_eq!(res.amount, Uint128::zero()); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + let dist1 = test.reward_with_distribution(mix_id, performance(100.0)); + + test.skip_to_next_epoch_end(); + let dist2 = test.reward_with_distribution(mix_id, performance(100.0)); + + test.skip_to_next_epoch_end(); + let dist3 = test.reward_with_distribution(mix_id, performance(100.0)); + + let delegation_pre = test.delegation(mix_id, delegator, &None); + let mix_rewarding = test.mix_rewarding(mix_id); + let res = withdraw_delegator_reward( + test.deps_mut().storage, + delegation_pre.clone(), + mix_rewarding, + ) + .unwrap(); + let reward = dist1.delegates + dist2.delegates + dist3.delegates; + assert_eq!(res.amount, truncate_reward_amount(reward)); + let updated = test.mix_rewarding(mix_id); + assert_decimals(updated.delegates, delegation_dec); + + let delegation_post = test.delegation(mix_id, delegator, &None); + assert_ne!( + delegation_pre.cumulative_reward_ratio, + delegation_post.cumulative_reward_ratio + ) + } +} diff --git a/contracts/mixnet/src/rewards/mod.rs b/contracts/mixnet/src/rewards/mod.rs index f3a3c9941f..2bf969c292 100644 --- a/contracts/mixnet/src/rewards/mod.rs +++ b/contracts/mixnet/src/rewards/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 pub mod helpers; +pub mod models; pub mod queries; pub mod storage; pub mod transactions; diff --git a/contracts/mixnet/src/rewards/models.rs b/contracts/mixnet/src/rewards/models.rs new file mode 100644 index 0000000000..77a7342291 --- /dev/null +++ b/contracts/mixnet/src/rewards/models.rs @@ -0,0 +1,18 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::Decimal; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Default, Debug, Clone, Copy, Eq, PartialEq)] +pub(crate) struct RewardPoolChange { + /// Indicates amount that shall get moved from the reward pool to the staking supply + /// upon the current interval finishing. + pub removed: Decimal, + + // this will be used once coconut credentials are in use; + /// Indicates amount that shall get added to the reward pool and not touch the staking supply + /// upon the current interval finishing. + #[allow(unused)] + pub added: Decimal, +} diff --git a/contracts/mixnet/src/rewards/queries.rs b/contracts/mixnet/src/rewards/queries.rs index 9abcd66a0f..af01a75bab 100644 --- a/contracts/mixnet/src/rewards/queries.rs +++ b/contracts/mixnet/src/rewards/queries.rs @@ -1,226 +1,1068 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use super::storage; -use crate::error::ContractError; -use cosmwasm_std::Uint128; -use cosmwasm_std::{Deps, StdResult}; -use mixnet_contract_common::{IdentityKey, MixnodeRewardingStatusResponse}; +use crate::delegations::storage as delegations_storage; +use crate::interval::storage as interval_storage; +use crate::mixnodes; +use crate::mixnodes::storage as mixnodes_storage; +use cosmwasm_std::{coin, Coin, Decimal, Deps, StdResult}; +use mixnet_contract_common::mixnode::MixNodeDetails; +use mixnet_contract_common::reward_params::{NodeRewardParams, Performance, RewardingParams}; +use mixnet_contract_common::rewarding::helpers::truncate_reward; +use mixnet_contract_common::rewarding::{ + EstimatedCurrentEpochRewardResponse, PendingRewardResponse, +}; +use mixnet_contract_common::{Delegation, NodeId}; -pub(crate) fn query_reward_pool(deps: Deps<'_>) -> StdResult { - storage::REWARD_POOL.load(deps.storage) +pub(crate) fn query_rewarding_params(deps: Deps<'_>) -> StdResult { + storage::REWARDING_PARAMS.load(deps.storage) } -pub(crate) fn query_circulating_supply(deps: Deps<'_>) -> StdResult { - storage::circulating_supply(deps.storage) +fn pending_operator_reward(mix_details: Option) -> PendingRewardResponse { + match mix_details { + Some(mix_details) => PendingRewardResponse { + amount_staked: Some(mix_details.original_pledge().clone()), + amount_earned: Some(mix_details.pending_operator_reward()), + amount_earned_detailed: Some(mix_details.pending_detailed_operator_reward()), + mixnode_still_fully_bonded: !mix_details.is_unbonding(), + }, + None => PendingRewardResponse::default(), + } } -pub(crate) fn query_staking_supply(deps: Deps<'_>) -> StdResult { - storage::staking_supply(deps.storage) -} - -pub(crate) fn query_rewarding_status( - deps: Deps<'_>, - mix_identity: IdentityKey, - interval_id: u32, -) -> StdResult { - let status = storage::REWARDING_STATUS.may_load(deps.storage, (interval_id, mix_identity))?; - - Ok(MixnodeRewardingStatusResponse { status }) -} - -pub fn query_operator_reward(deps: Deps, owner: String) -> Result { - let owner_address = deps.api.addr_validate(&owner)?; - let bond = match crate::mixnodes::storage::mixnodes() - .idx - .owner - .item(deps.storage, owner_address.clone())? - { - Some(record) => record.1, - None => { - // Return if bond does not exist - return Ok(Uint128::zero()); - } - }; - - super::transactions::calculate_operator_reward(deps.storage, deps.api, &owner_address, &bond) -} - -pub fn query_delegator_reward( +pub fn query_pending_operator_reward( deps: Deps, owner: String, - mix_identity: IdentityKey, +) -> StdResult { + let owner_address = deps.api.addr_validate(&owner)?; + // in order to determine operator's reward we need to know its original pledge and thus + // we have to load the entire thing + let mix_details = mixnodes::helpers::get_mixnode_details_by_owner(deps.storage, owner_address)?; + Ok(pending_operator_reward(mix_details)) +} + +pub fn query_pending_mixnode_operator_reward( + deps: Deps, + mix_id: NodeId, +) -> StdResult { + // in order to determine operator's reward we need to know its original pledge and thus + // we have to load the entire thing + let mix_details = mixnodes::helpers::get_mixnode_details_by_id(deps.storage, mix_id)?; + Ok(pending_operator_reward(mix_details)) +} + +pub fn query_pending_delegator_reward( + deps: Deps, + owner: String, + mix_id: NodeId, proxy: Option, -) -> Result { - let proxy = match proxy { - Some(proxy) => Some( - deps.api - .addr_validate(&proxy) - .map_err(|_| ContractError::InvalidAddress(proxy))?, - ), - None => None, +) -> StdResult { + let owner_address = deps.api.addr_validate(&owner)?; + let proxy = proxy + .map(|proxy| deps.api.addr_validate(&proxy)) + .transpose()?; + + let mix_rewarding = match storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)? { + Some(mix_rewarding) => mix_rewarding, + None => return Ok(PendingRewardResponse::default()), }; - let key = mixnet_contract_common::delegation::generate_storage_key( - &deps.api.addr_validate(&owner)?, - proxy.as_ref(), + let storage_key = Delegation::generate_storage_key(mix_id, &owner_address, proxy.as_ref()); + let delegation = match delegations_storage::delegations().may_load(deps.storage, storage_key)? { + Some(delegation) => delegation, + None => return Ok(PendingRewardResponse::default()), + }; + + let detailed_reward = mix_rewarding.determine_delegation_reward(&delegation); + let delegator_reward = mix_rewarding.pending_delegator_reward(&delegation); + + // check if the mixnode isnt in the process of unbonding (or has already unbonded) + let is_bonded = matches!(mixnodes_storage::mixnode_bonds().may_load(deps.storage, mix_id)?, Some(mix_bond) if !mix_bond.is_unbonding); + + Ok(PendingRewardResponse { + amount_staked: Some(delegation.amount), + amount_earned: Some(delegator_reward), + amount_earned_detailed: Some(detailed_reward), + mixnode_still_fully_bonded: is_bonded, + }) +} + +fn zero_reward( + original_stake: Coin, + current_value: Decimal, +) -> EstimatedCurrentEpochRewardResponse { + EstimatedCurrentEpochRewardResponse { + estimation: Some(coin(0, &original_stake.denom)), + detailed_estimation_amount: Some(Decimal::zero()), + current_stake_value: Some(truncate_reward(current_value, &original_stake.denom)), + current_stake_value_detailed_amount: Some(current_value), + original_stake: Some(original_stake), + } +} + +pub(crate) fn query_estimated_current_epoch_operator_reward( + deps: Deps<'_>, + mix_id: NodeId, + estimated_performance: Performance, +) -> StdResult { + let mix_details = match mixnodes::helpers::get_mixnode_details_by_id(deps.storage, mix_id)? { + None => return Ok(EstimatedCurrentEpochRewardResponse::empty_response()), + Some(mix_details) => mix_details, + }; + + let amount_staked = mix_details.original_pledge().clone(); + let mix_rewarding = mix_details.rewarding_details; + let current_value = mix_rewarding.operator; + + // if node is currently not in the rewarded set, the performance is 0, + // or the node has either unbonded or is in the process of unbonding, + // the calculations are trivial - the rewards are 0 + if mix_details.bond_information.is_unbonding { + return Ok(zero_reward(amount_staked, current_value)); + } + + let node_status = match interval_storage::REWARDED_SET.may_load(deps.storage, mix_id)? { + None => return Ok(zero_reward(amount_staked, current_value)), + Some(node_status) => node_status, + }; + + if estimated_performance.is_zero() { + return Ok(zero_reward(amount_staked, current_value)); + } + + let rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; + let interval = interval_storage::current_interval(deps.storage)?; + + let node_reward_params = NodeRewardParams::new(estimated_performance, node_status.is_active()); + let node_reward = mix_rewarding.node_reward(&rewarding_params, node_reward_params); + let reward_distribution = mix_rewarding.determine_reward_split( + node_reward, + estimated_performance, + interval.epochs_in_interval(), ); - super::transactions::calculate_delegator_reward(deps.storage, deps.api, key, &mix_identity) + + Ok(EstimatedCurrentEpochRewardResponse { + estimation: Some(truncate_reward( + reward_distribution.operator, + &amount_staked.denom, + )), + detailed_estimation_amount: Some(reward_distribution.operator), + current_stake_value: Some(truncate_reward(current_value, &amount_staked.denom)), + current_stake_value_detailed_amount: Some(current_value), + original_stake: Some(amount_staked), + }) +} + +pub(crate) fn query_estimated_current_epoch_delegator_reward( + deps: Deps<'_>, + owner: String, + mix_id: NodeId, + proxy: Option, + estimated_performance: Performance, +) -> StdResult { + let owner_address = deps.api.addr_validate(&owner)?; + let proxy = proxy + .map(|proxy| deps.api.addr_validate(&proxy)) + .transpose()?; + + let mix_rewarding = match storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)? { + Some(mix_rewarding) => mix_rewarding, + None => return Ok(EstimatedCurrentEpochRewardResponse::empty_response()), + }; + + let storage_key = Delegation::generate_storage_key(mix_id, &owner_address, proxy.as_ref()); + let delegation = match delegations_storage::delegations().may_load(deps.storage, storage_key)? { + Some(delegation) => delegation, + None => return Ok(EstimatedCurrentEpochRewardResponse::empty_response()), + }; + + let staked_dec = Decimal::from_atomics(delegation.amount.amount, 0).unwrap(); + let current_value = staked_dec + mix_rewarding.determine_delegation_reward(&delegation); + let amount_staked = delegation.amount; + + // check if the mixnode isnt in the process of unbonding (or has already unbonded) + let is_bonded = matches!(mixnodes_storage::mixnode_bonds().may_load(deps.storage, mix_id)?, Some(mix_bond) if !mix_bond.is_unbonding); + + if !is_bonded { + return Ok(zero_reward(amount_staked, current_value)); + } + + // if node is currently not in the rewarded set, the performance is 0, + // or the node has either unbonded or is in the process of unbonding, + // the calculations are trivial - the rewards are 0 + let node_status = match interval_storage::REWARDED_SET.may_load(deps.storage, mix_id)? { + None => return Ok(zero_reward(amount_staked, current_value)), + Some(node_status) => node_status, + }; + + if estimated_performance.is_zero() { + return Ok(zero_reward(amount_staked, current_value)); + } + + let rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; + let interval = interval_storage::current_interval(deps.storage)?; + + let node_reward_params = NodeRewardParams::new(estimated_performance, node_status.is_active()); + let node_reward = mix_rewarding.node_reward(&rewarding_params, node_reward_params); + let reward_distribution = mix_rewarding.determine_reward_split( + node_reward, + estimated_performance, + interval.epochs_in_interval(), + ); + + if reward_distribution.delegates.is_zero() { + return Ok(zero_reward(amount_staked, current_value)); + } + + let reward_share = current_value / mix_rewarding.delegates * reward_distribution.delegates; + + Ok(EstimatedCurrentEpochRewardResponse { + estimation: Some(truncate_reward(reward_share, &amount_staked.denom)), + detailed_estimation_amount: Some(reward_share), + current_stake_value: Some(truncate_reward(current_value, &amount_staked.denom)), + current_stake_value_detailed_amount: Some(current_value), + original_stake: Some(amount_staked), + }) } #[cfg(test)] -pub(crate) mod tests { +mod tests { use super::*; - use crate::mixnet_contract_settings::storage as mixnet_params_storage; - use crate::support::tests; use crate::support::tests::test_helpers; - use cosmwasm_std::testing::{mock_env, mock_info}; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::Uint128; + + #[test] + fn querying_for_rewarding_params() { + // not much to test here. after contract is initialised, the query must always be valid + let deps = test_helpers::init_contract(); + let res = query_rewarding_params(deps.as_ref()); + + assert!(res.is_ok()) + } #[cfg(test)] - mod querying_for_rewarding_status { + mod querying_for_pending_operator_reward { use super::*; - use crate::constants; - use crate::delegations::transactions::try_delegate_to_mixnode; - use crate::interval::storage::{save_epoch, save_epoch_reward_params}; - use crate::rewards::transactions::try_reward_mixnode; + use crate::mixnodes::transactions::try_remove_mixnode; use crate::support::tests::fixtures::TEST_COIN_DENOM; - use cosmwasm_std::{coin, Addr}; - use mixnet_contract_common::{ - Interval, RewardingResult, RewardingStatus, MIXNODE_DELEGATORS_PAGE_LIMIT, - }; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::{coin, Decimal}; + use mixnet_contract_common::rewarding::helpers::truncate_reward; #[test] - fn returns_empty_status_for_unrewarded_nodes() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - let rewarding_validator_address = current_state.rewarding_validator_address; + fn for_non_existent_node() { + let test = TestSetup::new(); + let owner = "mix-owner"; - let node_identity = test_helpers::add_mixnode( - "bob", - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); + let res = query_pending_operator_reward(test.deps(), owner.into()).unwrap(); + let res2 = query_pending_mixnode_operator_reward(test.deps(), 42).unwrap(); + assert_eq!(res, res2); - assert!( - query_rewarding_status(deps.as_ref(), node_identity.clone(), 1) - .unwrap() - .status - .is_none() - ); - - // node was rewarded but for different interval - let info = mock_info(rewarding_validator_address.as_ref(), &[]); - try_reward_mixnode( - deps.as_mut(), - env, - info.clone(), - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), - ) - .unwrap(); - - assert!(query_rewarding_status(deps.as_ref(), node_identity, 2) - .unwrap() - .status - .is_none()); + assert!(res.amount_earned.is_none()); + assert!(res.amount_earned_detailed.is_none()); + assert!(res.amount_staked.is_none()); + assert!(!res.mixnode_still_fully_bonded); } #[test] + fn for_unrewarded_node() { + let mut test = TestSetup::new(); + let owner = "mix-owner"; - fn returns_complete_status_for_fully_rewarded_node() { - // with single page - let mut deps = test_helpers::init_contract(); - let mut env = mock_env(); - let current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - let rewarding_validator_address = current_state.rewarding_validator_address; + let initial_stake = Uint128::new(1_000_000_000_000); + let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); - let node_owner: Addr = Addr::unchecked("bob"); - let node_identity = test_helpers::add_mixnode( - node_owner.as_str(), - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); + let res = query_pending_operator_reward(test.deps(), owner.into()).unwrap(); + let res2 = query_pending_mixnode_operator_reward(test.deps(), mix_id).unwrap(); + assert_eq!(res, res2); - env.block.height += constants::MINIMUM_BLOCK_AGE_FOR_REWARDING; + let expected_actual = coin(0, TEST_COIN_DENOM); - let info = mock_info(rewarding_validator_address.as_ref(), &[]); + assert_eq!(res.amount_earned.unwrap(), expected_actual); + assert_eq!(res.amount_earned_detailed.unwrap(), Decimal::zero()); + assert_eq!(res.amount_staked.unwrap().amount, initial_stake); + assert!(res.mixnode_still_fully_bonded); + } - let epoch = Interval::init_epoch(env.clone()); - save_epoch(&mut deps.storage, &epoch).unwrap(); - save_epoch_reward_params(epoch.id(), &mut deps.storage).unwrap(); + #[test] + fn for_node_with_pending_reward() { + let mut test = TestSetup::new(); + let owner = "mix-owner"; + let initial_stake = Uint128::new(1_000_000_000_000); + let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); - try_reward_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + + let mut total_earned = Decimal::zero(); + let dist = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + total_earned += dist.operator; + + let res = query_pending_operator_reward(test.deps(), owner.into()).unwrap(); + let res2 = query_pending_mixnode_operator_reward(test.deps(), mix_id).unwrap(); + assert_eq!(res, res2); + + let expected_actual = truncate_reward(total_earned, TEST_COIN_DENOM); + + assert_eq!(res.amount_earned.unwrap(), expected_actual); + assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); + assert_eq!(res.amount_staked.unwrap().amount, initial_stake); + assert!(res.mixnode_still_fully_bonded); + + // reward it few more times for good measure + for _ in 0..10 { + test.skip_to_next_epoch_end(); + let dist = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + total_earned += dist.operator; + + let res = query_pending_operator_reward(test.deps(), owner.into()).unwrap(); + let res2 = query_pending_mixnode_operator_reward(test.deps(), mix_id).unwrap(); + assert_eq!(res, res2); + + let expected_actual = truncate_reward(total_earned, TEST_COIN_DENOM); + + assert_eq!(res.amount_earned.unwrap(), expected_actual); + assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); + assert_eq!(res.amount_staked.unwrap().amount, initial_stake); + assert!(res.mixnode_still_fully_bonded); + } + } + + #[test] + fn for_node_that_is_unbonding() { + let mut test = TestSetup::new(); + let owner = "mix-owner"; + let initial_stake = Uint128::new(1_000_000_000_000); + let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + + let mut total_earned = Decimal::zero(); + let dist = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + total_earned += dist.operator; + + let sender = mock_info(owner, &[]); + try_remove_mixnode(test.deps_mut(), sender).unwrap(); + + let res = query_pending_operator_reward(test.deps(), owner.into()).unwrap(); + let res2 = query_pending_mixnode_operator_reward(test.deps(), mix_id).unwrap(); + assert_eq!(res, res2); + + let expected_actual = truncate_reward(total_earned, TEST_COIN_DENOM); + assert_eq!(res.amount_earned.unwrap(), expected_actual); + assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); + assert_eq!(res.amount_staked.unwrap().amount, initial_stake); + assert!(!res.mixnode_still_fully_bonded); + } + + #[test] + fn for_node_that_has_unbonded() { + let mut test = TestSetup::new(); + let owner = "mix-owner"; + let initial_stake = Uint128::new(1_000_000_000_000); + let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let sender = mock_info(owner, &[]); + try_remove_mixnode(test.deps_mut(), sender).unwrap(); + test.execute_all_pending_events(); + + let res = query_pending_operator_reward(test.deps(), owner.into()).unwrap(); + let res2 = query_pending_mixnode_operator_reward(test.deps(), mix_id).unwrap(); + assert_eq!(res, res2); + + // if you unbonded, you don't have any pending stuff as you've already claimed it + // by unbonding + assert!(res.amount_earned.is_none()); + assert!(res.amount_earned_detailed.is_none()); + assert!(res.amount_staked.is_none()); + assert!(!res.mixnode_still_fully_bonded); + } + } + + #[cfg(test)] + mod querying_for_pending_delegator_reward { + use super::*; + use crate::mixnodes::transactions::try_remove_mixnode; + use crate::rewards::transactions::try_withdraw_delegator_reward; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use crate::support::tests::test_helpers::get_bank_send_msg; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::{coin, Decimal}; + use mixnet_contract_common::rewarding::helpers::truncate_reward; + + #[test] + fn for_non_existent_delegation() { + let test = TestSetup::new(); + let delegator = "delegator"; + + let res = + query_pending_delegator_reward(test.deps(), delegator.into(), 42, None).unwrap(); + + assert!(res.amount_earned.is_none()); + assert!(res.amount_earned_detailed.is_none()); + assert!(res.amount_staked.is_none()); + assert!(!res.mixnode_still_fully_bonded); + } + + #[test] + fn for_unrewarded_delegator() { + let mut test = TestSetup::new(); + let owner = "delegator"; + + let initial_stake = Uint128::new(100_000_000); + let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + test.add_immediate_delegation(owner, initial_stake, mix_id); + + let res = + query_pending_delegator_reward(test.deps(), owner.into(), mix_id, None).unwrap(); + + let expected_actual = coin(0, TEST_COIN_DENOM); + + assert_eq!(res.amount_earned.unwrap(), expected_actual); + assert_eq!(res.amount_earned_detailed.unwrap(), Decimal::zero()); + assert_eq!(res.amount_staked.unwrap().amount, initial_stake); + assert!(res.mixnode_still_fully_bonded); + } + + #[test] + fn for_delegator_with_pending_reward() { + let mut test = TestSetup::new(); + let owner = "delegator"; + + let initial_stake = Uint128::new(100_000_000); + let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + test.add_immediate_delegation(owner, initial_stake, mix_id); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + + let mut total_earned = Decimal::zero(); + let dist = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + total_earned += dist.delegates; + + let res = + query_pending_delegator_reward(test.deps(), owner.into(), mix_id, None).unwrap(); + + let expected_actual = truncate_reward(total_earned, TEST_COIN_DENOM); + + assert_eq!(res.amount_earned.unwrap(), expected_actual); + assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); + assert_eq!(res.amount_staked.unwrap().amount, initial_stake); + assert!(res.mixnode_still_fully_bonded); + + // reward it few more times for good measure + for _ in 0..10 { + test.skip_to_next_epoch_end(); + let dist = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + total_earned += dist.delegates; + + let res = query_pending_delegator_reward(test.deps(), owner.into(), mix_id, None) + .unwrap(); + + let expected_actual = truncate_reward(total_earned, TEST_COIN_DENOM); + + assert_eq!(res.amount_earned.unwrap(), expected_actual); + assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); + assert_eq!(res.amount_staked.unwrap().amount, initial_stake); + assert!(res.mixnode_still_fully_bonded); + } + } + + #[test] + fn for_node_that_is_unbonding() { + let mut test = TestSetup::new(); + let owner = "delegator"; + + let initial_stake = Uint128::new(100_000_000); + let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + test.add_immediate_delegation(owner, initial_stake, mix_id); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + + let mut total_earned = Decimal::zero(); + let dist = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + total_earned += dist.delegates; + + let sender = mock_info("mix-owner", &[]); + try_remove_mixnode(test.deps_mut(), sender).unwrap(); + + let res = + query_pending_delegator_reward(test.deps(), owner.into(), mix_id, None).unwrap(); + + let expected_actual = truncate_reward(total_earned, TEST_COIN_DENOM); + assert_eq!(res.amount_earned.unwrap(), expected_actual); + assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); + assert_eq!(res.amount_staked.unwrap().amount, initial_stake); + assert!(!res.mixnode_still_fully_bonded); + } + + #[test] + fn for_node_that_has_unbonded() { + let mut test = TestSetup::new(); + let owner = "delegator"; + + let initial_stake = Uint128::new(100_000_000); + let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + test.add_immediate_delegation(owner, initial_stake, mix_id); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + + let mut total_earned = Decimal::zero(); + let dist = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + total_earned += dist.delegates; + + let sender = mock_info("mix-owner", &[]); + try_remove_mixnode(test.deps_mut(), sender).unwrap(); + test.execute_all_pending_events(); + + let res = + query_pending_delegator_reward(test.deps(), owner.into(), mix_id, None).unwrap(); + + let expected_actual = truncate_reward(total_earned, TEST_COIN_DENOM); + assert_eq!(res.amount_earned.unwrap(), expected_actual); + assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); + assert_eq!(res.amount_staked.unwrap().amount, initial_stake); + assert!(!res.mixnode_still_fully_bonded); + } + + #[test] + fn always_equals_to_what_can_be_withdrawn() { + // we've already tested withdraw reward to calculate values correctly + // even if there are multiple delegators joined at different times when the reward has to be split + let mut test = TestSetup::new(); + let del1 = "delegator1"; + let del2 = "delegator2"; + let del3 = "delegator3"; + let del4 = "delegator4"; + + let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + test.add_immediate_delegation(del1, 123_456_789u32, mix_id); + test.add_immediate_delegation(del2, 150_000_000u32, mix_id); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + test.add_immediate_delegation(del3, 500_000_000u32, mix_id); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(85.0)); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(5.0)); + + test.add_immediate_delegation(del4, 5_000_000u32, mix_id); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + test.add_immediate_delegation(del2, 250_000_000u32, mix_id); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(98.0)); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + test.remove_immediate_delegation(del3, mix_id); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(98.0)); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let pending1 = + query_pending_delegator_reward(test.deps(), del1.into(), mix_id, None).unwrap(); + let pending2 = + query_pending_delegator_reward(test.deps(), del2.into(), mix_id, None).unwrap(); + let pending3 = + query_pending_delegator_reward(test.deps(), del3.into(), mix_id, None).unwrap(); + let pending4 = + query_pending_delegator_reward(test.deps(), del4.into(), mix_id, None).unwrap(); + + let actual1_res = + try_withdraw_delegator_reward(test.deps_mut(), mock_info(del1, &[]), mix_id) + .unwrap(); + let (_, actual1) = get_bank_send_msg(&actual1_res).unwrap(); + assert_eq!(pending1.amount_earned.unwrap(), actual1[0]); + + let actual2_res = + try_withdraw_delegator_reward(test.deps_mut(), mock_info(del2, &[]), mix_id) + .unwrap(); + let (_, actual2) = get_bank_send_msg(&actual2_res).unwrap(); + assert_eq!(pending2.amount_earned.unwrap(), actual2[0]); + + // the amount is none because we have removed our delegation! + assert!(pending3.amount_earned.is_none()); + + let actual4_res = + try_withdraw_delegator_reward(test.deps_mut(), mock_info(del4, &[]), mix_id) + .unwrap(); + let (_, actual4) = get_bank_send_msg(&actual4_res).unwrap(); + assert_eq!(pending4.amount_earned.unwrap(), actual4[0]); + } + } + + #[cfg(test)] + mod querying_for_estimated_current_epoch_operator_reward { + use super::*; + use crate::mixnodes::transactions::try_remove_mixnode; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use cosmwasm_std::testing::mock_info; + + fn expected_current_operator( + test: &TestSetup, + mix_id: NodeId, + initial_stake: Uint128, + ) -> EstimatedCurrentEpochRewardResponse { + let mix_rewarding = test.mix_rewarding(mix_id); + EstimatedCurrentEpochRewardResponse { + estimation: Some(coin(0, TEST_COIN_DENOM)), + detailed_estimation_amount: Some(Decimal::zero()), + current_stake_value: Some(truncate_reward(mix_rewarding.operator, TEST_COIN_DENOM)), + current_stake_value_detailed_amount: Some(mix_rewarding.operator), + original_stake: Some(coin(initial_stake.u128(), TEST_COIN_DENOM)), + } + } + + #[test] + fn when_node_doesnt_exist() { + let test = TestSetup::new(); + let res = query_estimated_current_epoch_operator_reward( + test.deps(), + 42, + test_helpers::performance(100.0), + ) + .unwrap(); + assert_eq!(res, EstimatedCurrentEpochRewardResponse::empty_response()) + } + + #[test] + fn when_node_is_unbonding() { + let mut test = TestSetup::new(); + let initial_stake = Uint128::new(1_000_000_000_000); + let owner = "mix-owner"; + let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let sender = mock_info(owner, &[]); + try_remove_mixnode(test.deps_mut(), sender).unwrap(); + + let res = query_estimated_current_epoch_operator_reward( + test.deps(), + mix_id, + test_helpers::performance(100.0), ) .unwrap(); - let res = query_rewarding_status(deps.as_ref(), node_identity, 0).unwrap(); - assert!(matches!(res.status, Some(RewardingStatus::Complete(..)))); + let expected = expected_current_operator(&test, mix_id, initial_stake); + assert_eq!(res, expected) + } - match res.status.unwrap() { - RewardingStatus::Complete(result) => { - assert_ne!(RewardingResult::default().node_reward, result.node_reward); - } - _ => unreachable!(), - } + #[test] + fn when_node_has_already_unbonded() { + let mut test = TestSetup::new(); + let initial_stake = Uint128::new(1_000_000_000_000); + let owner = "mix-owner"; + let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); - // with multiple pages - let node_owner: Addr = Addr::unchecked("alice"); - let node_identity = test_helpers::add_mixnode( - node_owner.as_str(), - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); - for i in 0..MIXNODE_DELEGATORS_PAGE_LIMIT + 123 { - try_delegate_to_mixnode( - deps.as_mut(), - env.clone(), - mock_info( - &*format!("delegator{:04}", i), - &[coin(200_000000, TEST_COIN_DENOM)], - ), - node_identity.clone(), - ) - .unwrap(); - } + let sender = mock_info(owner, &[]); + try_remove_mixnode(test.deps_mut(), sender).unwrap(); + test.execute_all_pending_events(); - env.block.height += constants::MINIMUM_BLOCK_AGE_FOR_REWARDING; - test_helpers::update_env_and_progress_interval(&mut env, deps.as_mut().storage); + let res = query_estimated_current_epoch_operator_reward( + test.deps(), + mix_id, + test_helpers::performance(100.0), + ) + .unwrap(); + assert_eq!(res, EstimatedCurrentEpochRewardResponse::empty_response()) + } - let info = mock_info(rewarding_validator_address.as_ref(), &[]); + #[test] + fn when_node_is_not_in_the_rewarded_set() { + let mut test = TestSetup::new(); + let initial_stake = Uint128::new(1_000_000_000_000); + let mix_id = test.add_dummy_mixnode("mix-owner", Some(initial_stake)); - try_reward_mixnode( - deps.as_mut(), - env, - info, - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + test.update_rewarded_set(vec![]); + + let res = query_estimated_current_epoch_operator_reward( + test.deps(), + mix_id, + test_helpers::performance(100.0), ) .unwrap(); - // rewards all pending - // try_reward_next_mixnode_delegators(deps.as_mut(), info, node_identity.to_string(), 1) - // .unwrap(); + let expected = expected_current_operator(&test, mix_id, initial_stake); + assert_eq!(res, expected) + } - let res = query_rewarding_status(deps.as_ref(), node_identity, 1).unwrap(); - assert!(matches!(res.status, Some(RewardingStatus::Complete(..)))); + #[test] + fn when_estimated_performance_is_zero() { + let mut test = TestSetup::new(); + let initial_stake = Uint128::new(1_000_000_000_000); + let mix_id = test.add_dummy_mixnode("mix-owner", Some(initial_stake)); - match res.status.unwrap() { - RewardingStatus::Complete(result) => { - assert_ne!(RewardingResult::default().node_reward, result.node_reward); - } - _ => unreachable!(), + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let res = query_estimated_current_epoch_operator_reward( + test.deps(), + mix_id, + test_helpers::performance(0.0), + ) + .unwrap(); + + let expected = expected_current_operator(&test, mix_id, initial_stake); + assert_eq!(res, expected) + } + + #[test] + fn with_correct_parameters_matches_actual_distribution() { + let mut test = TestSetup::new(); + let initial_stake = Uint128::new(1_000_000_000_000); + let mix_id = test.add_dummy_mixnode("mix-owner", Some(initial_stake)); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let mix_rewarding = test.mix_rewarding(mix_id); + let res = query_estimated_current_epoch_operator_reward( + test.deps(), + mix_id, + test_helpers::performance(95.0), + ) + .unwrap(); + + test.skip_to_next_epoch_end(); + let dist = test.reward_with_distribution(mix_id, test_helpers::performance(95.0)); + + let expected = EstimatedCurrentEpochRewardResponse { + original_stake: Some(coin(initial_stake.u128(), TEST_COIN_DENOM)), + current_stake_value: Some(truncate_reward(mix_rewarding.operator, TEST_COIN_DENOM)), + current_stake_value_detailed_amount: Some(mix_rewarding.operator), + estimation: Some(truncate_reward(dist.operator, TEST_COIN_DENOM)), + detailed_estimation_amount: Some(dist.operator), + }; + + assert_eq!(res, expected) + } + } + + #[cfg(test)] + mod querying_for_estimated_current_epoch_delegator_reward { + use super::*; + use crate::mixnodes::transactions::try_remove_mixnode; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use cosmwasm_std::testing::mock_info; + + fn expected_current_delegator( + test: &TestSetup, + mix_id: NodeId, + owner: &str, + ) -> EstimatedCurrentEpochRewardResponse { + let mix_rewarding = test.mix_rewarding(mix_id); + let delegation = test.delegation(mix_id, owner, &None); + + let staked_dec = Decimal::from_atomics(delegation.amount.amount, 0).unwrap(); + let current_value = staked_dec + mix_rewarding.determine_delegation_reward(&delegation); + let amount_staked = delegation.amount; + + EstimatedCurrentEpochRewardResponse { + estimation: Some(coin(0, TEST_COIN_DENOM)), + detailed_estimation_amount: Some(Decimal::zero()), + current_stake_value: Some(truncate_reward(current_value, TEST_COIN_DENOM)), + current_stake_value_detailed_amount: Some(current_value), + original_stake: Some(amount_staked), } } + + #[test] + fn when_delegation_doesnt_exist() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let res = query_estimated_current_epoch_delegator_reward( + test.deps(), + "foomper".into(), + mix_id, + None, + test_helpers::performance(100.0), + ) + .unwrap(); + + assert_eq!(res, EstimatedCurrentEpochRewardResponse::empty_response()) + } + + #[test] + fn when_node_is_unbonding() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let initial_stake = Uint128::new(1_000_000_000); + let owner = "delegator"; + test.add_immediate_delegation(owner, initial_stake, mix_id); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let sender = mock_info("mix-owner", &[]); + try_remove_mixnode(test.deps_mut(), sender).unwrap(); + + let res = query_estimated_current_epoch_delegator_reward( + test.deps(), + owner.into(), + mix_id, + None, + test_helpers::performance(100.0), + ) + .unwrap(); + + let expected = expected_current_delegator(&test, mix_id, owner); + assert_eq!(res, expected) + } + + #[test] + fn when_node_has_already_unbonded() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let initial_stake = Uint128::new(1_000_000_000); + let owner = "delegator"; + test.add_immediate_delegation(owner, initial_stake, mix_id); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let sender = mock_info("mix-owner", &[]); + try_remove_mixnode(test.deps_mut(), sender).unwrap(); + test.execute_all_pending_events(); + + let res = query_estimated_current_epoch_delegator_reward( + test.deps(), + owner.into(), + mix_id, + None, + test_helpers::performance(100.0), + ) + .unwrap(); + + let expected = expected_current_delegator(&test, mix_id, owner); + assert_eq!(res, expected) + } + + #[test] + fn when_node_is_not_in_the_rewarded_set() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let initial_stake = Uint128::new(1_000_000_000); + let owner = "delegator"; + test.add_immediate_delegation(owner, initial_stake, mix_id); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + test.update_rewarded_set(vec![]); + + let res = query_estimated_current_epoch_delegator_reward( + test.deps(), + owner.into(), + mix_id, + None, + test_helpers::performance(100.0), + ) + .unwrap(); + + let expected = expected_current_delegator(&test, mix_id, owner); + assert_eq!(res, expected) + } + + #[test] + fn when_estimated_performance_is_zero() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let initial_stake = Uint128::new(1_000_000_000); + let owner = "delegator"; + test.add_immediate_delegation(owner, initial_stake, mix_id); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let res = query_estimated_current_epoch_delegator_reward( + test.deps(), + owner.into(), + mix_id, + None, + test_helpers::performance(0.0), + ) + .unwrap(); + + let expected = expected_current_delegator(&test, mix_id, owner); + assert_eq!(res, expected) + } + + #[test] + fn with_correct_parameters_matches_actual_distribution_for_single_delegator() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let initial_stake = Uint128::new(1_000_000_000); + let owner = "delegator"; + test.add_immediate_delegation(owner, initial_stake, mix_id); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + + let mix_rewarding = test.mix_rewarding(mix_id); + let res = query_estimated_current_epoch_delegator_reward( + test.deps(), + owner.into(), + mix_id, + None, + test_helpers::performance(95.0), + ) + .unwrap(); + + test.skip_to_next_epoch_end(); + let dist = test.reward_with_distribution(mix_id, test_helpers::performance(95.0)); + + let expected = EstimatedCurrentEpochRewardResponse { + original_stake: Some(coin(initial_stake.u128(), TEST_COIN_DENOM)), + current_stake_value: Some(truncate_reward( + mix_rewarding.delegates, + TEST_COIN_DENOM, + )), + current_stake_value_detailed_amount: Some(mix_rewarding.delegates), + estimation: Some(truncate_reward(dist.delegates, TEST_COIN_DENOM)), + detailed_estimation_amount: Some(dist.delegates), + }; + + assert_eq!(res, expected) + } + + #[test] + fn with_correct_parameters_matches_actual_distribution_for_three_delegators() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let initial_stake1 = Uint128::new(1_000_000_000); + let initial_stake2 = Uint128::new(45_000_000_000); + let initial_stake3 = Uint128::new(8_500_000_000); + + let initial_stake1_dec = Decimal::from_atomics(initial_stake1, 0).unwrap(); + let initial_stake2_dec = Decimal::from_atomics(initial_stake2, 0).unwrap(); + let initial_stake3_dec = Decimal::from_atomics(initial_stake3, 0).unwrap(); + let del1 = "delegator1"; + let del2 = "delegator2"; + let del3 = "delegator3"; + test.add_immediate_delegation(del1, initial_stake1, mix_id); + test.add_immediate_delegation(del2, initial_stake2, mix_id); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + test.reward_with_distribution(mix_id, test_helpers::performance(95.0)); + + test.add_immediate_delegation(del3, initial_stake3, mix_id); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(85.0)); + + let mix_rewarding = test.mix_rewarding(mix_id); + + let ress = [del1, del2, del3] + .iter() + .map(|owner| { + query_estimated_current_epoch_delegator_reward( + test.deps(), + owner.to_string(), + mix_id, + None, + test_helpers::performance(95.0), + ) + .unwrap() + }) + .collect::>(); + + // as verified by other tests those values are correct + let est1 = query_pending_delegator_reward(test.deps(), del1.into(), mix_id, None) + .unwrap() + .amount_earned_detailed + .unwrap(); + let est2 = query_pending_delegator_reward(test.deps(), del2.into(), mix_id, None) + .unwrap() + .amount_earned_detailed + .unwrap(); + let est3 = query_pending_delegator_reward(test.deps(), del3.into(), mix_id, None) + .unwrap() + .amount_earned_detailed + .unwrap(); + + let cur1 = initial_stake1_dec + est1; + let cur2 = initial_stake2_dec + est2; + let cur3 = initial_stake3_dec + est3; + + test.skip_to_next_epoch_end(); + let dist = test.reward_with_distribution(mix_id, test_helpers::performance(95.0)); + + let share1 = cur1 / mix_rewarding.delegates * dist.delegates; + let share2 = cur2 / mix_rewarding.delegates * dist.delegates; + let share3 = cur3 / mix_rewarding.delegates * dist.delegates; + + let expected1 = EstimatedCurrentEpochRewardResponse { + original_stake: Some(coin(initial_stake1.u128(), TEST_COIN_DENOM)), + current_stake_value: Some(truncate_reward(cur1, TEST_COIN_DENOM)), + current_stake_value_detailed_amount: Some(cur1), + estimation: Some(truncate_reward(share1, TEST_COIN_DENOM)), + detailed_estimation_amount: Some(share1), + }; + assert_eq!(ress[0], expected1); + + let expected2 = EstimatedCurrentEpochRewardResponse { + original_stake: Some(coin(initial_stake2.u128(), TEST_COIN_DENOM)), + current_stake_value: Some(truncate_reward(cur2, TEST_COIN_DENOM)), + current_stake_value_detailed_amount: Some(cur2), + estimation: Some(truncate_reward(share2, TEST_COIN_DENOM)), + detailed_estimation_amount: Some(share2), + }; + assert_eq!(ress[1], expected2); + + let expected3 = EstimatedCurrentEpochRewardResponse { + original_stake: Some(coin(initial_stake3.u128(), TEST_COIN_DENOM)), + current_stake_value: Some(truncate_reward(cur3, TEST_COIN_DENOM)), + current_stake_value_detailed_amount: Some(cur3), + estimation: Some(truncate_reward(share3, TEST_COIN_DENOM)), + detailed_estimation_amount: Some(share3), + }; + assert_eq!(ress[2], expected3); + } } } diff --git a/contracts/mixnet/src/rewards/storage.rs b/contracts/mixnet/src/rewards/storage.rs index b62e89ba0d..09cbd57351 100644 --- a/contracts/mixnet/src/rewards/storage.rs +++ b/contracts/mixnet/src/rewards/storage.rs @@ -1,81 +1,39 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::error::ContractError; -use crate::mixnet_contract_settings::storage as settings_storage; -use config::defaults::TOTAL_SUPPLY; -use cosmwasm_std::{StdResult, Storage, Uint128}; +use crate::constants::{ + MIXNODES_REWARDING_PK_NAMESPACE, PENDING_REWARD_POOL_KEY, REWARDING_PARAMS_KEY, +}; +use crate::rewards::models::RewardPoolChange; +use cosmwasm_std::{Decimal, StdResult, Storage}; use cw_storage_plus::{Item, Map}; -use mixnet_contract_common::{reward_params::EpochRewardParams, IdentityKey, RewardingStatus}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::mixnode::MixNodeRewarding; +use mixnet_contract_common::reward_params::RewardingParams; +use mixnet_contract_common::NodeId; -type BlockHeight = u64; -type Address = String; +// current parameters used for rewarding purposes +pub(crate) const REWARDING_PARAMS: Item<'_, RewardingParams> = Item::new(REWARDING_PARAMS_KEY); +pub(crate) const PENDING_REWARD_POOL_CHANGE: Item<'_, RewardPoolChange> = + Item::new(PENDING_REWARD_POOL_KEY); -pub(crate) const REWARD_POOL: Item<'_, Uint128> = Item::new("pool"); -// TODO: Do we need a migration for this? -pub(crate) const REWARDING_STATUS: Map<'_, (u32, IdentityKey), RewardingStatus> = Map::new("rm"); +pub const MIXNODE_REWARDING: Map = + Map::new(MIXNODES_REWARDING_PK_NAMESPACE); -// This has to be a byte vector due to proxy delegastions and rewarding -pub(crate) const DELEGATOR_REWARD_CLAIMED_HEIGHT: Map<'_, (Vec, IdentityKey), BlockHeight> = - Map::new("drc"); -pub(crate) const OPERATOR_REWARD_CLAIMED_HEIGHT: Map<'_, (Address, IdentityKey), BlockHeight> = - Map::new("orc"); - -type EpochId = u32; - -pub(crate) const EPOCH_REWARD_PARAMS: Map<'_, EpochId, EpochRewardParams> = Map::new("epr"); - -pub fn epoch_reward_params_for_id( - storage: &dyn Storage, - id: EpochId, -) -> StdResult { - EPOCH_REWARD_PARAMS.load(storage, id) -} - -#[allow(dead_code)] -pub fn incr_reward_pool( - amount: Uint128, +pub fn reward_accounting( storage: &mut dyn Storage, -) -> Result { - REWARD_POOL.update(storage, |mut current_pool| { - current_pool += amount; - Ok(current_pool) - }) + amount: Decimal, +) -> Result<(), MixnetContractError> { + let mut pending_changes = PENDING_REWARD_POOL_CHANGE.load(storage)?; + pending_changes.removed += amount; + + Ok(PENDING_REWARD_POOL_CHANGE.save(storage, &pending_changes)?) } -pub fn reward_accounting(storage: &mut dyn Storage, amount: Uint128) -> Result<(), ContractError> { - decr_reward_pool(storage, amount)?; - incr_staking_supply(storage, amount)?; - Ok(()) -} - -fn incr_staking_supply(storage: &mut dyn Storage, amount: Uint128) -> Result<(), ContractError> { - let mut contract_state = - crate::mixnet_contract_settings::storage::CONTRACT_STATE.load(storage)?; - contract_state.params.staking_supply += amount; - crate::mixnet_contract_settings::storage::CONTRACT_STATE.save(storage, &contract_state)?; - Ok(()) -} - -fn decr_reward_pool(storage: &mut dyn Storage, amount: Uint128) -> Result { - REWARD_POOL.update(storage, |current_pool| { - let stake = current_pool - .checked_sub(amount) - .map_err(|_| ContractError::OutOfFunds { - to_remove: amount.u128(), - reward_pool: current_pool.u128(), - })?; - - Ok(stake) - }) -} - -pub fn circulating_supply(storage: &dyn Storage) -> StdResult { - let reward_pool = REWARD_POOL.load(storage)?; - Ok(Uint128::new(TOTAL_SUPPLY).saturating_sub(reward_pool)) -} - -pub fn staking_supply(storage: &dyn Storage) -> StdResult { - let state = settings_storage::CONTRACT_STATE.load(storage)?; - Ok(state.params.staking_supply) +pub(crate) fn initialise_storage( + storage: &mut dyn Storage, + reward_params: RewardingParams, +) -> StdResult<()> { + REWARDING_PARAMS.save(storage, &reward_params)?; + PENDING_REWARD_POOL_CHANGE.save(storage, &RewardPoolChange::default()) } diff --git a/contracts/mixnet/src/rewards/transactions.rs b/contracts/mixnet/src/rewards/transactions.rs index 13965f91a1..02bc1e1b82 100644 --- a/contracts/mixnet/src/rewards/transactions.rs +++ b/contracts/mixnet/src/rewards/transactions.rs @@ -1,1885 +1,1562 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::storage::{ - self, epoch_reward_params_for_id, DELEGATOR_REWARD_CLAIMED_HEIGHT, - OPERATOR_REWARD_CLAIMED_HEIGHT, -}; -use crate::constants; -use crate::contract::debug_with_visibility; +use super::storage; use crate::delegations::storage as delegations_storage; -use crate::error::ContractError; -use crate::mixnet_contract_settings::storage::mix_denom; -use crate::mixnodes::storage::mixnodes; -use crate::mixnodes::storage::{self as mixnodes_storage, StoredMixnodeBond}; -use crate::mixnodes::transactions::is_blacklisted; +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::support::helpers::{is_authorized, operator_cost_at_epoch}; -use cosmwasm_std::{ - coins, wasm_execute, Addr, Api, BankMsg, Coin, DepsMut, Env, MessageInfo, Order, Response, - StdResult, Storage, Uint128, +use crate::support::helpers::{ + ensure_bonded, ensure_is_authorized, ensure_is_owner, ensure_proxy_match, + send_to_proxy_or_owner, }; -use cw_storage_plus::Bound; +use cosmwasm_std::{wasm_execute, Addr, DepsMut, Env, MessageInfo, Response}; +use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ - new_claim_delegator_reward_event, new_claim_operator_reward_event, - new_compound_delegator_reward_event, new_compound_operator_reward_event, - new_mix_operator_rewarding_event, new_not_found_mix_operator_rewarding_event, - new_too_fresh_bond_mix_operator_rewarding_event, new_zero_uptime_mix_operator_rewarding_event, + new_active_set_update_event, new_mix_rewarding_event, + new_not_found_mix_operator_rewarding_event, new_pending_active_set_update_event, + new_pending_rewarding_params_update_event, new_rewarding_params_update_event, + new_withdraw_delegator_reward_event, new_withdraw_operator_reward_event, + new_zero_uptime_mix_operator_rewarding_event, }; -use mixnet_contract_common::mixnode::StoredNodeRewardResult; -use mixnet_contract_common::reward_params::{NodeEpochRewards, NodeRewardParams, RewardParams}; -use mixnet_contract_common::{Delegation, IdentityKey, RewardingStatus}; - -use mixnet_contract_common::RewardingResult; +use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData}; +use mixnet_contract_common::reward_params::{ + IntervalRewardingParamsUpdate, NodeRewardParams, Performance, +}; +use mixnet_contract_common::{Delegation, NodeId}; use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; -use vesting_contract_common::one_ucoin; - -// All four of the below methods need to do the following things: -// 1. Calculate currently available rewards -// 2. Send the rewards back to whoever claimed them -// 3. Set the LAST_CLAIMED_HEIGHT to the current height -pub fn try_claim_operator_reward( - deps: DepsMut<'_>, - env: &Env, - info: &MessageInfo, -) -> Result { - _try_claim_operator_reward(deps.storage, deps.api, env, info.sender.as_ref(), None) -} - -pub fn try_claim_operator_reward_on_behalf( - deps: DepsMut<'_>, - env: &Env, - info: &MessageInfo, - owner: String, -) -> Result { - _try_claim_operator_reward( - deps.storage, - deps.api, - env, - &owner, - Some(info.sender.clone()), - ) -} - -fn _try_claim_operator_reward( - storage: &mut dyn Storage, - api: &dyn Api, - env: &Env, - owner: &str, - proxy: Option, -) -> Result { - let owner = api.addr_validate(owner)?; - let mix_denom = mix_denom(storage)?; - - let bond = match crate::mixnodes::storage::mixnodes() - .idx - .owner - .item(storage, owner.clone())? - { - Some(record) => record.1, - None => return Err(ContractError::NoAssociatedMixNodeBond { owner }), - }; - - if proxy != bond.proxy { - return Err(ContractError::ProxyMismatch { - existing: 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()), - }); - } - - let reward = calculate_operator_reward(storage, api, &owner, &bond)?; - - OPERATOR_REWARD_CLAIMED_HEIGHT.save( - storage, - (owner.to_string(), bond.identity().to_string()), - &env.block.height, - )?; - - if reward.is_zero() { - return Err(ContractError::NoRewardsToClaim { - identity: bond.identity().to_string(), - address: owner.to_string(), - }); - } - - let return_tokens = BankMsg::Send { - to_address: proxy.as_ref().unwrap_or(&owner).to_string(), - amount: coins(reward.u128(), mix_denom.clone()), - }; - - let mut response = Response::default() - .add_message(return_tokens) - .add_event(new_claim_operator_reward_event(&owner, reward)); - - if let Some(proxy) = proxy { - let msg = Some(VestingContractExecuteMsg::TrackReward { - address: owner.to_string(), - amount: Coin::new(reward.u128(), mix_denom.clone()), - }); - - let wasm_msg = wasm_execute(proxy, &msg, vec![one_ucoin(mix_denom)])?; - response = response.add_message(wasm_msg); - } - - Ok(response) -} - -pub fn _try_claim_delegator_reward( - storage: &mut dyn Storage, - api: &dyn Api, - env: &Env, - owner: &str, - mix_identity: &str, - proxy: Option, -) -> Result { - let owner = api.addr_validate(owner)?; - let mix_denom = mix_denom(storage)?; - - let key = mixnet_contract_common::delegation::generate_storage_key(&owner, proxy.as_ref()); - let reward = calculate_delegator_reward(storage, api, key.clone(), mix_identity)?; - - DELEGATOR_REWARD_CLAIMED_HEIGHT.save( - storage, - (key, mix_identity.to_string()), - &env.block.height, - )?; - - if reward.is_zero() { - return Err(ContractError::NoRewardsToClaim { - identity: mix_identity.to_string(), - address: owner.to_string(), - }); - } - - let return_tokens = BankMsg::Send { - to_address: proxy.as_ref().unwrap_or(&owner).to_string(), - amount: coins(reward.u128(), mix_denom.clone()), - }; - - let mut response = - Response::default() - .add_message(return_tokens) - .add_event(new_claim_delegator_reward_event( - &owner, - &proxy, - reward, - mix_identity, - )); - - if let Some(proxy) = proxy { - let msg = Some(VestingContractExecuteMsg::TrackReward { - address: owner.to_string(), - amount: Coin::new(reward.u128(), mix_denom.clone()), - }); - - let wasm_msg = wasm_execute(proxy, &msg, vec![one_ucoin(mix_denom)])?; - response = response.add_message(wasm_msg); - } - - Ok(response) -} - -pub fn try_claim_delegator_reward_on_behalf( - deps: DepsMut<'_>, - env: &Env, - info: &MessageInfo, - owner: String, - mix_identity: &str, -) -> Result { - _try_claim_delegator_reward( - deps.storage, - deps.api, - env, - &owner, - mix_identity, - Some(info.sender.clone()), - ) -} - -pub fn try_claim_delegator_reward( - deps: DepsMut<'_>, - env: &Env, - info: &MessageInfo, - mix_identity: &str, -) -> Result { - _try_claim_delegator_reward( - deps.storage, - deps.api, - env, - info.sender.as_ref(), - mix_identity, - None, - ) -} - -pub fn try_compound_operator_reward_on_behalf( - deps: DepsMut, - env: Env, - info: MessageInfo, - owner: String, -) -> Result { - let proxy = deps.api.addr_validate(info.sender.as_str())?; - let owner = deps.api.addr_validate(&owner)?; - - let reward = _try_compound_operator_reward( - deps.storage, - deps.api, - env.block.height, - &owner, - Some(proxy), - )?; - - Ok(Response::new().add_event(new_compound_operator_reward_event(&owner, reward))) -} - -pub fn try_compound_operator_reward( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, -) -> Result { - let owner = deps.api.addr_validate(info.sender.as_str())?; - let reward = - _try_compound_operator_reward(deps.storage, deps.api, env.block.height, &owner, None)?; - - Ok(Response::new().add_event(new_compound_operator_reward_event(&owner, reward))) -} - -pub fn _try_compound_operator_reward( - storage: &mut dyn Storage, - api: &dyn Api, - block_height: u64, - owner: &Addr, - proxy: Option, -) -> Result { - let bond = match mixnodes().idx.owner.item(storage, owner.to_owned())? { - Some(record) => record.1, - None => { - // Return if bond does not exist - return Ok(Uint128::zero()); - } - }; - - if bond.proxy != proxy { - // Return if proxy is not the same as the bond proxy - return Ok(Uint128::zero()); - } - - let mut updated_bond = bond.clone(); - let reward = calculate_operator_reward(storage, api, owner, &bond)?; - updated_bond.accumulated_rewards = - Some(updated_bond.accumulated_rewards().saturating_sub(reward)); - updated_bond.pledge_amount.amount += reward; - mixnodes().replace( - storage, - bond.identity(), - Some(&updated_bond), - Some(&bond), - block_height, - )?; - - OPERATOR_REWARD_CLAIMED_HEIGHT.save( - storage, - (owner.to_string(), bond.identity().to_string()), - &block_height, - )?; - - Ok(reward) -} - -pub fn calculate_operator_reward( - storage: &dyn Storage, - api: &dyn Api, - owner: &Addr, - bond: &StoredMixnodeBond, -) -> Result { - let last_claimed_height = storage::OPERATOR_REWARD_CLAIMED_HEIGHT - .load(storage, (owner.to_string(), bond.identity().to_string())) - .unwrap_or(0); - - let accumulated_rewards = mixnodes() - .changelog() - .prefix(bond.identity()) - .keys( - storage, - Some(Bound::inclusive(last_claimed_height)), - None, - Order::Ascending, - ) - .filter_map(|height| height.ok()) - .fold( - Ok(Uint128::zero()), - |acc, height| -> Result { - let accumulated_reward = acc?; - if let Some(bond) = mixnodes() - .may_load_at_height(storage, bond.identity().as_str(), height) - .ok() - .flatten() - { - if let Some(ref epoch_rewards) = bond.epoch_rewards { - // Compound rewards from previous heights - let operator_cost = - operator_cost_at_epoch(storage, epoch_rewards.epoch_id())?; - match epoch_rewards.operator_reward(bond.profit_margin(), operator_cost) { - Ok(reward) => return Ok(accumulated_reward + reward), - Err(err) => { - debug_with_visibility( - api, - format!("Failed to calculate operator reward: {:?}", err), - ); - } - }; - } - }; - Ok(accumulated_reward) - }, - )?; - Ok(accumulated_rewards) -} - -// calculate_delegator_reward -// - figure out when is the last time reward go claimed -// - calculate current rewards -// compound_delegator_reward -// - decrease node accumulated rewards -// - increase delegation - -pub fn try_compound_delegator_reward_on_behalf( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - owner: String, - mix_identity: IdentityKey, -) -> Result { - let proxy = deps.api.addr_validate(info.sender.as_str())?; - let owner = deps.api.addr_validate(&owner)?; - let reward = _try_compound_delegator_reward( - env.block.height, - deps, - owner.as_str(), - &mix_identity, - Some(proxy.clone()), - )?; - - Ok( - Response::new().add_event(new_compound_delegator_reward_event( - &owner, - &Some(proxy), - reward, - &mix_identity, - )), - ) -} - -pub fn try_compound_reward( - deps: DepsMut<'_>, - env: Env, - operator: Option, - delegator: Option, - mix_identity: Option, - proxy: Option, -) -> Result { - let proxy = proxy.and_then(|p| deps.api.addr_validate(&p).ok()); - if let Some(operator_address) = operator { - let operator_address = deps.api.addr_validate(&operator_address)?; - let reward = _try_compound_operator_reward( - deps.storage, - deps.api, - env.block.height, - &operator_address, - proxy, - )?; - Ok( - Response::default().add_event(new_compound_operator_reward_event( - &operator_address, - reward, - )), - ) - } else if let Some(delegator_address) = delegator { - if mix_identity.is_none() { - return Err(ContractError::MissingMixIdentity); - } - - let delegator_address = deps.api.addr_validate(&delegator_address)?; - let reward = _try_compound_delegator_reward( - env.block.height, - deps, - delegator_address.as_str(), - mix_identity.as_ref().unwrap(), - proxy, - )?; - Ok( - Response::default().add_event(new_compound_delegator_reward_event( - &delegator_address, - &None, - reward, - &mix_identity.unwrap(), - )), - ) - } else { - Ok(Response::default()) - } -} - -pub fn try_compound_delegator_reward( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - mix_identity: IdentityKey, -) -> Result { - let owner = deps.api.addr_validate(info.sender.as_str())?; - let reward = _try_compound_delegator_reward( - env.block.height, - deps, - owner.as_str(), - &mix_identity, - None, - )?; - - Ok( - Response::new().add_event(new_compound_delegator_reward_event( - &info.sender, - &None, - reward, - &mix_identity, - )), - ) -} - -pub fn _try_compound_delegator_reward( - block_height: u64, - deps: DepsMut<'_>, - owner_address: &str, - mix_identity: &str, - proxy: Option, -) -> Result { - let delegation_map = crate::delegations::storage::delegations(); - let mix_denom = mix_denom(deps.storage)?; - let owner = deps.api.addr_validate(owner_address)?; - let key = mixnet_contract_common::delegation::generate_storage_key(&owner, proxy.as_ref()); - let reward = calculate_delegator_reward(deps.storage, deps.api, key.clone(), mix_identity)?; - let mut total_delegation_delegate = Uint128::zero(); - - // Might want to introduce paging here - let delegation_heights = delegation_map - .prefix((mix_identity.to_string(), key.clone())) - .keys(deps.storage, None, None, cosmwasm_std::Order::Ascending) - .collect::>>()?; - - for h in delegation_heights { - let delegation = - delegation_map.load(deps.storage, (mix_identity.to_string(), key.clone(), h))?; - total_delegation_delegate += delegation.amount.amount; - delegation_map.replace( - deps.storage, - (mix_identity.to_string(), key.clone(), h), - None, - Some(&delegation), - )?; - } - - let compounded_delegation = total_delegation_delegate + reward; - - if compounded_delegation != Uint128::zero() { - mixnodes_storage::TOTAL_DELEGATION.update::<_, ContractError>( - deps.storage, - mix_identity, - |total_delegation| { - // since we know that the target node exists and because the total_delegation bucket - // entry is created whenever the node itself is added, the unwrap here is fine - // as the entry MUST exist - Ok(total_delegation.unwrap() + reward) - }, - )?; - - // let's simplify the entire procedure. Rather than creating a fresh delegation on the mixnode - // via `_try_delegate_to_mixnode` and then waiting for reconcile to happen, - // just save it directly to the storage right now. - // my reasoning for that is simple: `_try_delegate_to_mixnode` could fail if the node the - // delegator has delegated to no longer exists. - let delegation = Delegation::new( - owner, - mix_identity.into(), - Coin { - amount: compounded_delegation, - denom: mix_denom, - }, - block_height, - proxy, - ); - - delegation_map.save( - deps.storage, - (mix_identity.into(), key.clone(), block_height), - &delegation, - )?; - } - - if let Some(mut bond) = mixnodes().may_load(deps.storage, mix_identity)? { - bond.accumulated_rewards = Some(bond.accumulated_rewards().saturating_sub(reward)); - mixnodes().save(deps.storage, mix_identity, &bond, block_height)?; - } - - DELEGATOR_REWARD_CLAIMED_HEIGHT.save( - deps.storage, - (key, mix_identity.to_string()), - &block_height, - )?; - - Ok(reward) -} - -// TODO: Test -// + last_reward_claimed_height is updated -// + last_reward_claimed height is correctly used -pub fn calculate_delegator_reward( - storage: &dyn Storage, - api: &dyn Api, - key: Vec, - mix_identity: &str, -) -> Result { - if is_blacklisted(storage, mix_identity)? { - return Ok(Uint128::zero()); - }; - - let last_claimed_height = storage::DELEGATOR_REWARD_CLAIMED_HEIGHT - .load(storage, (key.clone(), mix_identity.to_string())) - .unwrap_or(0); - - let mut delegations = delegations_storage::delegations() - .prefix((mix_identity.to_string(), key)) - .range(storage, None, None, Order::Ascending) - .filter_map(|record| record.ok()) - .map(|(_, delegation)| delegation) - .collect::>(); - - // If last_claimed_height is 0 iterate from earlest_delegation, try avoiding gas query limit - let iter_from = if last_claimed_height == 0 { - delegations.iter().fold(0, |acc, x| acc.min(x.block_height)) - } else { - last_claimed_height - }; - - // Accumulate outside of the loop to gain some speed, on a log of checkpoints - let mut delegation_at_height = Uint128::zero(); - - // This is a bit gnarly, but we want to avoid loading all heights, the loading mixnodes, so we're doing it all in the iterator - let accumulated_rewards = mixnodes() - .changelog() - .prefix(mix_identity) - .keys( - storage, - Some(Bound::inclusive(iter_from)), - None, - Order::Ascending, - ) - .filter_map(|height| height.ok()) - // Get all checkpoints greater then last claimed delegation height - .fold( - Ok(Uint128::zero()), - |acc, height| -> Result { - let accumulated_reward = acc?; - delegation_at_height = delegations - .iter() - .filter(|d| d.block_height <= height) - .fold(delegation_at_height, |total, delegation| { - total + delegation.amount.amount - }); - // Drop what we've processed - // This should be replaced with drain_filter once it stabilizes - delegations.retain(|d| d.block_height > height); - // debug_with_visibility( - // api, - // format!("delegation at height {} - {}", height, delegation_at_height), - // ); - if delegation_at_height != Uint128::zero() { - // debug_with_visibility( - // api, - // format!("Loading bond {} at height {}", mix_identity, height), - // ); - if let Some(bond) = mixnodes() - .may_load_at_height(storage, mix_identity, height) - .ok() - .flatten() - { - if let Some(ref epoch_rewards) = bond.epoch_rewards { - let operator_cost = - operator_cost_at_epoch(storage, epoch_rewards.epoch_id()).unwrap(); - // Compound rewards from previous heights - match epoch_reward_params_for_id(storage, epoch_rewards.epoch_id()) { - Ok(params) => { - let reward_at_height = match epoch_rewards.delegation_reward( - delegation_at_height + accumulated_reward, - bond.profit_margin(), - operator_cost, - params, - ) { - Ok(reward) => { - // debug_with_visibility( - // api, - // format!("Reward at height {} - {}", height, reward), - // ); - reward - } - Err(err) => { - debug_with_visibility( - api, - format!( - "Error calculating reward at {} - {}", - height, err - ), - ); - Uint128::zero() - } - }; - return Ok(accumulated_reward + reward_at_height); - } - Err(_err) => { - debug_with_visibility( - api, - format!("No epoch reward params for epoch {}", height), - ); - } - } - } - } - }; - Ok(accumulated_reward) - }, - )?; - - Ok(accumulated_rewards) -} pub(crate) fn try_reward_mixnode( deps: DepsMut<'_>, env: Env, info: MessageInfo, - mix_identity: IdentityKey, - params: NodeRewardParams, -) -> Result { - is_authorized(info.sender.to_string(), deps.storage)?; - // verify_rewarding_state(deps.storage, info, epoch_id)?; - let epoch = crate::interval::storage::current_epoch(deps.storage)?; + node_id: NodeId, + node_performance: Performance, +) -> Result { + ensure_is_authorized(info.sender, deps.storage)?; - // check if the mixnode hasn't been rewarded in this rewarding interval already - match storage::REWARDING_STATUS.may_load(deps.storage, (epoch.id(), mix_identity.clone()))? { - None => (), - Some(RewardingStatus::Complete(_)) => { - return Err(ContractError::MixnodeAlreadyRewarded { - identity: mix_identity, - }) + // see if the epoch has finished + let interval = interval_storage::current_interval(deps.storage)?; + if !interval.is_current_epoch_over(&env) { + return Err(MixnetContractError::EpochInProgress { + current_block_time: env.block.time.seconds(), + epoch_start: interval.current_epoch_start_unix_timestamp(), + epoch_end: interval.current_epoch_end_unix_timestamp(), + }); + } + + // there's a chance of this failing to load the details if the mixnode unbonded before rewards + // were distributed and all of its delegators are also gone + let mut mix_rewarding = match storage::MIXNODE_REWARDING.may_load(deps.storage, node_id)? { + Some(mix_rewarding) if mix_rewarding.still_bonded() => mix_rewarding, + // don't fail if the node has unbonded as we don't want to fail the underlying transaction + _ => { + return Ok( + Response::new().add_event(new_not_found_mix_operator_rewarding_event( + interval, node_id, + )), + ); } - Some(RewardingStatus::PendingNextDelegatorPage(_)) => { - return Err(ContractError::DelegatorsPendingReward { - identity: mix_identity, - }) - } - } - - // check if the bond even exists - let mut current_bond = - match mixnodes_storage::read_full_mixnode_bond(deps.storage, &mix_identity)? { - Some(bond) => bond, - None => { - return Ok( - Response::new().add_event(new_not_found_mix_operator_rewarding_event( - epoch.id(), - &mix_identity, - )), - ) - } - }; - - // check if node is old enough for rewarding - if current_bond.block_height + constants::MINIMUM_BLOCK_AGE_FOR_REWARDING > env.block.height { - storage::REWARDING_STATUS.save( - deps.storage, - (epoch.id(), mix_identity.clone()), - &RewardingStatus::Complete(Default::default()), - )?; - - return Ok( - Response::new().add_event(new_too_fresh_bond_mix_operator_rewarding_event( - epoch.id(), - &mix_identity, - )), - ); - } - - let node_pledge = current_bond.pledge_amount.amount; - let node_delegation = current_bond.total_delegation.amount; - - // check if it has non-zero uptime - if params.uptime() == Uint128::zero() { - storage::REWARDING_STATUS.save( - deps.storage, - (epoch.id(), mix_identity.clone()), - &RewardingStatus::Complete(Default::default()), - )?; - - return Ok( - Response::new().add_event(new_zero_uptime_mix_operator_rewarding_event( - epoch.id(), - &mix_identity, - )), - ); - } - - let mut node_reward_params = params; - node_reward_params.set_reward_blockstamp(env.block.height); - - let epoch_reward_params = crate::interval::storage::current_epoch_reward_params(deps.storage)?; - let reward_params = RewardParams::new(epoch_reward_params, node_reward_params); - let node_reward_result = current_bond.reward(&reward_params); - let stored_node_result: StoredNodeRewardResult = node_reward_result.try_into()?; - - current_bond.accumulated_rewards = - Some(current_bond.accumulated_rewards() + stored_node_result.reward()); - let mut stored_bond: StoredMixnodeBond = current_bond.into(); - // technically we don't have to set the total_delegation bucket, but it makes things easier - // in different places that we can guarantee that if node exists, so does the data behind the total delegation - - stored_bond.epoch_rewards = Some(NodeEpochRewards::new( - node_reward_params, - stored_node_result, - epoch.id(), - )); - - let identity = stored_bond.identity(); - - crate::mixnodes::storage::mixnodes().save( - deps.storage, - identity, - &stored_bond, - env.block.height, - )?; - - // Take rewards out of the rewarding pool - storage::reward_accounting(deps.storage, stored_node_result.reward())?; - - let rewarding_result = RewardingResult { - node_reward: stored_node_result.reward(), }; - helpers::update_rewarding_status( - deps.storage, - epoch.id(), - mix_identity.clone(), - rewarding_result, - )?; + // check if this node has already been rewarded for the current epoch. + // unlike the previous check, this one should be a hard error since this cannot be + // influenced by users actions + let absolute_epoch_id = interval.current_epoch_absolute_id(); + if absolute_epoch_id == mix_rewarding.last_rewarded_epoch { + return Err(MixnetContractError::MixnodeAlreadyRewarded { + node_id, + absolute_epoch_id, + }); + } - Ok(Response::new().add_event(new_mix_operator_rewarding_event( - epoch.id(), - &mix_identity, - node_reward_result, - node_pledge, - node_delegation, + // again a hard error since the rewarding validator should have known not to reward this node + let node_status = interval_storage::REWARDED_SET + .load(deps.storage, node_id) + .map_err(|_| MixnetContractError::MixnodeNotInRewardedSet { + node_id, + absolute_epoch_id, + })?; + + // no need to calculate anything as rewards are going to be 0 for everything + // however, we still need to update last_rewarded_epoch field + if node_performance.is_zero() { + mix_rewarding.last_rewarded_epoch = absolute_epoch_id; + storage::MIXNODE_REWARDING.save(deps.storage, node_id, &mix_rewarding)?; + return Ok( + Response::new().add_event(new_zero_uptime_mix_operator_rewarding_event( + interval, node_id, + )), + ); + } + + let rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; + let node_reward_params = NodeRewardParams::new(node_performance, node_status.is_active()); + + // calculate each step separate for easier accounting + let node_reward = mix_rewarding.node_reward(&rewarding_params, node_reward_params); + let reward_distribution = mix_rewarding.determine_reward_split( + node_reward, + node_performance, + interval.epochs_in_interval(), + ); + mix_rewarding.distribute_rewards(reward_distribution, absolute_epoch_id); + + // persist changes happened to the storage + storage::MIXNODE_REWARDING.save(deps.storage, node_id, &mix_rewarding)?; + storage::reward_accounting(deps.storage, node_reward)?; + + Ok(Response::new().add_event(new_mix_rewarding_event( + interval, + node_id, + reward_distribution, ))) } +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 { + 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 + // 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( + MixnetContractError::NoAssociatedMixNodeBond { + owner: owner.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)?; + 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); + } + } + } + + Ok(response.add_event(new_withdraw_operator_reward_event( + &owner, &proxy, reward, mix_id, + ))) +} + +pub(crate) fn try_withdraw_delegator_reward( + deps: DepsMut<'_>, + info: MessageInfo, + mix_id: NodeId, +) -> 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: NodeId, + owner: String, +) -> Result { + 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: NodeId, + 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 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), + }) + } + Some(delegation) => delegation, + }; + + // grab associated mixnode rewarding details + let mix_rewarding = + storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)?.ok_or(MixnetContractError::InconsistentState { + comment: "mixnode rewarding got removed from the storage whilst there's still an existing delegation" + .into(), + })?; + + // see if the mixnode is not in the process of unbonding or whether it has already unbonded + // (in that case the expected path of getting your tokens back is via undelegation) + match mixnodes_storage::mixnode_bonds().may_load(deps.storage, mix_id)? { + Some(mix_bond) if mix_bond.is_unbonding => { + return Err(MixnetContractError::MixnodeIsUnbonding { node_id: mix_id }) + } + None => return Err(MixnetContractError::MixnodeHasUnbonded { node_id: mix_id }), + _ => (), + }; + + 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); + } + } + } + + Ok(response.add_event(new_withdraw_delegator_reward_event( + &owner, &proxy, reward, mix_id, + ))) +} + +pub(crate) fn try_update_active_set_size( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + active_set_size: u32, + force_immediately: bool, +) -> Result { + ensure_is_owner(info.sender, deps.storage)?; + + let mut rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; + if active_set_size == 0 { + return Err(MixnetContractError::ZeroActiveSet); + } + + if active_set_size > rewarding_params.rewarded_set_size { + return Err(MixnetContractError::InvalidActiveSetSize); + } + + let interval = interval_storage::current_interval(deps.storage)?; + if force_immediately || interval.is_current_epoch_over(&env) { + rewarding_params.try_change_active_set_size(active_set_size)?; + storage::REWARDING_PARAMS.save(deps.storage, &rewarding_params)?; + Ok(Response::new().add_event(new_active_set_update_event(active_set_size))) + } else { + // push the epoch event + let epoch_event = PendingEpochEventData::UpdateActiveSetSize { + new_size: active_set_size, + }; + push_new_epoch_event(deps.storage, &epoch_event)?; + let time_left = interval.secs_until_current_interval_end(&env); + Ok( + Response::new().add_event(new_pending_active_set_update_event( + active_set_size, + time_left, + )), + ) + } +} + +pub(crate) fn try_update_rewarding_params( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + updated_params: IntervalRewardingParamsUpdate, + force_immediately: bool, +) -> Result { + ensure_is_owner(info.sender, deps.storage)?; + + if !updated_params.contains_updates() { + return Err(MixnetContractError::EmptyParamsChangeMsg); + } + + let interval = interval_storage::current_interval(deps.storage)?; + if force_immediately || interval.is_current_interval_over(&env) { + let mut rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; + rewarding_params.try_apply_updates(updated_params, interval.epochs_in_interval())?; + storage::REWARDING_PARAMS.save(deps.storage, &rewarding_params)?; + Ok(Response::new().add_event(new_rewarding_params_update_event( + updated_params, + rewarding_params.interval, + ))) + } else { + // push the interval event + let interval_event = PendingIntervalEventData::UpdateRewardingParams { + update: updated_params, + }; + push_new_interval_event(deps.storage, &interval_event)?; + let time_left = interval.secs_until_current_interval_end(&env); + Ok( + Response::new().add_event(new_pending_rewarding_params_update_event( + updated_params, + time_left, + )), + ) + } +} + #[cfg(test)] pub mod tests { use super::*; - use crate::delegations::transactions::{ - _try_remove_delegation_from_mixnode, try_delegate_to_mixnode, - }; - use crate::error::ContractError; - use crate::interval::storage::{ - current_epoch_reward_params, save_epoch, save_epoch_reward_params, - }; - use crate::interval::transactions::try_advance_epoch; - use crate::mixnet_contract_settings::storage::{ - self as mixnet_params_storage, rewarding_validator_address, - }; use crate::mixnodes::storage as mixnodes_storage; - use crate::mixnodes::storage::StoredMixnodeBond; - use crate::rewards::transactions::try_reward_mixnode; - use crate::support::helpers::{current_operator_epoch_cost, epochs_in_interval}; - use crate::support::tests; - use crate::support::tests::fixtures::TEST_COIN_DENOM; use crate::support::tests::test_helpers; - use az::CheckedCast; - use cosmwasm_std::testing::{mock_env, mock_info}; - use cosmwasm_std::{coin, coins, Addr, StdError, Timestamp, Uint128}; - use mixnet_contract_common::events::{ - must_find_attribute, BOND_TOO_FRESH_VALUE, NO_REWARD_REASON_KEY, - OPERATOR_REWARDING_EVENT_TYPE, - }; - use mixnet_contract_common::reward_params::{ - EpochRewardParams, NodeRewardParams, RewardParams, - }; - use mixnet_contract_common::{Delegation, IdentityKey, Interval, Layer, MixNode}; - use time::OffsetDateTime; + use cosmwasm_std::testing::mock_info; - #[test] - fn rewarding_mixnodes_with_incorrect_interval_id() { - let mut deps = test_helpers::init_contract(); - let mut env = mock_env(); - let sender = rewarding_validator_address(&deps.storage).unwrap(); - let info = mock_info(&sender, &coins(1000, TEST_COIN_DENOM)); - crate::interval::transactions::init_epoch(&mut deps.storage, env.clone()).unwrap(); - - // bond the node - let node_owner: Addr = Addr::unchecked("node-owner"); - let node_identity = test_helpers::add_mixnode( - node_owner.as_str(), - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - - // Reward once - let res = try_reward_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), - ); - assert!(res.is_ok()); - - let res = try_reward_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), - ); - // Fails since mixnode was already rewarded in this epoch - assert!(res.is_err()); - - let rewarding_validator_address = rewarding_validator_address(&deps.storage).unwrap(); - - // Advance epoch - let premature_advance = try_advance_epoch( - env.clone(), - &mut deps.storage, - rewarding_validator_address.clone(), - ); - assert!(premature_advance.is_err()); - - env.block.time = Timestamp::from_seconds(env.block.time.seconds() + 3600); - - let timely_advance = - try_advance_epoch(env.clone(), &mut deps.storage, rewarding_validator_address); - assert!(timely_advance.is_ok()); - - let res = try_reward_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), - ); - assert!(res.is_ok()); - - test_helpers::update_env_and_progress_interval(&mut env, deps.as_mut().storage); - - let res = try_reward_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - node_identity, - tests::fixtures::node_reward_params_fixture(100), - ); - assert!(res.is_ok()); - } - - #[test] - fn attempting_rewarding_mixnode_multiple_times_per_interval() { - let mut deps = test_helpers::init_contract(); - let mut env = mock_env(); - let current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - let rewarding_validator_address = current_state.rewarding_validator_address; - - // bond the node - let node_owner: Addr = Addr::unchecked("node-owner"); - let node_identity = test_helpers::add_mixnode( - node_owner.as_str(), - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - - let info = mock_info(rewarding_validator_address.as_ref(), &[]); - - // first reward goes through just fine - let res = try_reward_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), - ); - assert!(res.is_ok()); - - // but the other one fails - let res = try_reward_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), - ); - assert_eq!( - Err(ContractError::MixnodeAlreadyRewarded { - identity: node_identity.clone() - }), - res - ); - - // but rewarding the same node in the following interval is fine again - test_helpers::update_env_and_progress_interval(&mut env, deps.as_mut().storage); - - let res = try_reward_mixnode( - deps.as_mut(), - env, - info.clone(), - node_identity, - tests::fixtures::node_reward_params_fixture(100), - ); - assert!(res.is_ok()); - } - - #[test] - fn rewarding_mixnode_blockstamp_based() { - let mut deps = test_helpers::init_contract(); - - let mut env = mock_env(); - let current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - let rewarding_validator_address = current_state.rewarding_validator_address; - - let node_owner: Addr = Addr::unchecked("node-owner"); - let node_identity: IdentityKey = "nodeidentity".into(); - - let initial_bond = 10000_000000; - let initial_delegation = 20000_000000; - let mixnode_bond = StoredMixnodeBond { - pledge_amount: coin(initial_bond, TEST_COIN_DENOM), - owner: node_owner, - layer: Layer::One, - block_height: env.block.height, - mix_node: MixNode { - identity_key: node_identity.clone(), - ..tests::fixtures::mix_node_fixture() - }, - proxy: None, - accumulated_rewards: None, - epoch_rewards: None, + #[cfg(test)] + mod mixnode_rewarding { + use super::*; + use crate::interval::pending_events; + use crate::support::tests::test_helpers::{find_attribute, TestSetup}; + use cosmwasm_std::{Decimal, Uint128}; + use mixnet_contract_common::events::{ + MixnetEventType, BOND_NOT_FOUND_VALUE, DELEGATES_REWARD_KEY, NO_REWARD_REASON_KEY, + OPERATOR_REWARD_KEY, ZERO_PERFORMANCE_VALUE, }; + use mixnet_contract_common::RewardedSetNodeStatus; - mixnodes_storage::mixnodes() - .save( - deps.as_mut().storage, - &node_identity, - &mixnode_bond, - env.block.height, - ) - .unwrap(); - mixnodes_storage::TOTAL_DELEGATION - .save( - deps.as_mut().storage, - &node_identity, - &Uint128::new(initial_delegation), - ) - .unwrap(); + #[test] + fn can_only_be_performed_by_specified_rewarding_validator() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + let some_sender = mock_info("foomper", &[]); - // delegation happens later, but not later enough - env.block.height += constants::MINIMUM_BLOCK_AGE_FOR_REWARDING - 1; - env.block.time = Timestamp::from_seconds(OffsetDateTime::now_utc().unix_timestamp() as u64); + // skip time to when the following epoch is over (since mixnodes are not eligible for rewarding + // in the same epoch they're bonded and we need the rewarding epoch to be over) + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + let performance = test_helpers::performance(100.); - delegations_storage::delegations() - .save( - deps.as_mut().storage, - (node_identity.clone(), "delegator".into(), env.block.height), - &Delegation::new( - Addr::unchecked("delegator"), - node_identity.clone(), - coin(initial_delegation, TEST_COIN_DENOM), - env.block.height, - None, - ), - ) - .unwrap(); + let env = test.env(); + let res = try_reward_mixnode(test.deps_mut(), env, some_sender, mix_id, performance); + assert_eq!(res, Err(MixnetContractError::Unauthorized)); - let info = mock_info(rewarding_validator_address.as_ref(), &[]); + // good address (sanity check) + let env = test.env(); + let sender = test.rewarding_validator(); + let res = try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance); + assert!(res.is_ok()); + } - let epoch = Interval::init_epoch(env.clone()); - save_epoch(&mut deps.storage, &epoch).unwrap(); - save_epoch_reward_params(epoch.id(), &mut deps.storage).unwrap(); + #[test] + fn can_only_be_performed_if_node_is_fully_bonded() { + let mut test = TestSetup::new(); + let mix_id_never_existed = 42; + let mix_id_unbonded = test.add_dummy_mixnode("mix-owner-unbonded", None); + let mix_id_unbonded_leftover = + test.add_dummy_mixnode("mix-owner-unbonded-leftover", None); + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![ + mix_id_never_existed, + mix_id_unbonded, + mix_id_unbonded_leftover, + ]); - let epoch_from_storage = crate::interval::storage::current_epoch(&deps.storage).unwrap(); - assert_eq!(epoch_from_storage.id(), 0); + let env = test.env(); - let res = try_reward_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), - ) - .unwrap(); + // note: we don't have to test for cases where `is_unbonding` is set to true on a mixnode + // since before performing the validator-api should clear out the event queue - assert_eq!( - initial_bond, - test_helpers::read_mixnode_pledge_amount(deps.as_ref().storage, &node_identity) - .unwrap() - .u128() - ); - assert_eq!( - initial_delegation, - mixnodes_storage::TOTAL_DELEGATION - .load(deps.as_ref().storage, &node_identity) - .unwrap() - .u128() - ); - assert_eq!(1, res.events.len()); - assert_eq!(OPERATOR_REWARDING_EVENT_TYPE, res.events[0].ty); - assert_eq!( - BOND_TOO_FRESH_VALUE, - must_find_attribute(&res.events[0], NO_REWARD_REASON_KEY) - ); - - // reward can happen now, but only for bonded node - env.block.height += 1; - env.block.time = Timestamp::from_seconds(epoch.next().start_unix_timestamp() as u64); - let sender = - crate::mixnet_contract_settings::storage::rewarding_validator_address(&deps.storage) + // manually adjust delegation info as to indicate the rewarding information shouldnt get removed + let mut rewarding_details = storage::MIXNODE_REWARDING + .load(test.deps().storage, mix_id_unbonded_leftover) .unwrap(); - try_advance_epoch(env.clone(), &mut deps.storage, sender).unwrap(); - - let info = mock_info(rewarding_validator_address.as_ref(), &[]); - - let res = try_reward_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), - ) - .unwrap(); - - let mixnode = crate::mixnodes::storage::mixnodes() - .load(&deps.storage, &node_identity) - .unwrap(); - - assert!(mixnode.accumulated_rewards > Some(Uint128::zero()),); - assert_eq!( - initial_delegation, - mixnodes_storage::TOTAL_DELEGATION - .load(deps.as_ref().storage, &node_identity) - .unwrap() - .u128() - ); - - assert_eq!(1, res.events.len()); - let event = &res.events[0]; - assert_eq!(OPERATOR_REWARDING_EVENT_TYPE, event.ty); - // assert_ne!("0", must_find_attribute(event, TOTAL_MIXNODE_REWARD_KEY)); - // assert_ne!("0", must_find_attribute(event, OPERATOR_REWARD_KEY)); - // assert_eq!( - // "0", - // must_find_attribute(event, DISTRIBUTED_DELEGATION_REWARDS_KEY) - // ); - // assert_eq!( - // false.to_string(), - // must_find_attribute(event, FURTHER_DELEGATIONS_TO_REWARD_KEY) - // ); - - // reward happens now, both for node owner and delegators - env.block.height += constants::MINIMUM_BLOCK_AGE_FOR_REWARDING - 1; - test_helpers::update_env_and_progress_interval(&mut env, deps.as_mut().storage); - - let pledge_before_rewarding = - test_helpers::read_mixnode_pledge_amount(deps.as_ref().storage, &node_identity) - .unwrap() - .u128(); - - let info = mock_info(rewarding_validator_address.as_ref(), &[]); - let res = try_reward_mixnode( - deps.as_mut(), - env, - info.clone(), - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), - ) - .unwrap(); - - // We are in a lazy system, rewarding will not increase pledge or delegations - assert_eq!( - test_helpers::read_mixnode_pledge_amount(deps.as_ref().storage, &node_identity) - .unwrap() - .u128(), - pledge_before_rewarding - ); - assert_eq!( - mixnodes_storage::TOTAL_DELEGATION - .load(deps.as_ref().storage, &node_identity) - .unwrap() - .u128(), - initial_delegation - ); - - assert_eq!(1, res.events.len()); - let event = &res.events[0]; - assert_eq!(OPERATOR_REWARDING_EVENT_TYPE, event.ty); - // assert_ne!("0", must_find_attribute(event, TOTAL_MIXNODE_REWARD_KEY)); - // assert_ne!("0", must_find_attribute(event, OPERATOR_REWARD_KEY)); - // assert_ne!( - // "0", - // must_find_attribute(event, DISTRIBUTED_DELEGATION_REWARDS_KEY) - // ); - // assert_eq!( - // false.to_string(), - // must_find_attribute(event, FURTHER_DELEGATIONS_TO_REWARD_KEY) - // ); - } - - #[test] - fn test_reward_additivity_and_snapshots() { - use crate::constants::INTERVAL_REWARD_PERCENT; - use crate::contract::INITIAL_REWARD_POOL; - use crate::mixnodes::transactions::try_add_mixnode; - use rand::thread_rng; - - let mixnodes = crate::mixnodes::storage::mixnodes(); - - type U128 = fixed::types::U75F53; - - let mut deps = test_helpers::init_contract(); - let mut env = mock_env(); - let current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_ref().storage) - .unwrap(); - let rewarding_validator_address = current_state.rewarding_validator_address; - - let info = mock_info(rewarding_validator_address.as_str(), &[]); - - crate::mixnodes::transactions::try_checkpoint_mixnodes( - &mut deps.storage, - env.block.height, - info.clone(), - ) - .unwrap(); - let checkpoints = mixnodes - .changelog() - .keys(&deps.storage, None, None, Order::Ascending) - .filter_map(|x| x.ok()) - .collect::>(); - assert_eq!(0, checkpoints.len()); - - let period_reward_pool = - (INITIAL_REWARD_POOL / 100 / epochs_in_interval(&deps.storage).unwrap() as u128) - * INTERVAL_REWARD_PERCENT as u128; - assert_eq!(period_reward_pool, 6_944_444_444); - let circulating_supply = storage::circulating_supply(&deps.storage).unwrap().u128(); - assert_eq!(circulating_supply, 750_000_000_000_000u128); - - let staking_supply = storage::staking_supply(&deps.storage).unwrap().u128(); - assert_eq!(staking_supply, 100_000_000_000_000u128); - - let sender = Addr::unchecked("alice"); - let stake = coins(10_000_000_000, TEST_COIN_DENOM); - - let keypair = crypto::asymmetric::identity::KeyPair::new(&mut thread_rng()); - let owner_signature = keypair - .private_key() - .sign(sender.as_bytes()) - .to_base58_string(); - - let legit_sphinx_key = crypto::asymmetric::encryption::KeyPair::new(&mut thread_rng()); - - let info = mock_info(sender.as_str(), &stake); - - let node_identity_1 = keypair.public_key().to_base58_string(); - - try_add_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - MixNode { - identity_key: node_identity_1.clone(), - sphinx_key: legit_sphinx_key.public_key().to_base58_string(), - ..tests::fixtures::mix_node_fixture() - }, - owner_signature, - ) - .unwrap(); - - // tick - - env.block.height += 1; - - let info = mock_info(rewarding_validator_address.as_str(), &[]); - crate::mixnodes::transactions::try_checkpoint_mixnodes( - &mut deps.storage, - env.block.height, - info.clone(), - ) - .unwrap(); - mixnodes - .assert_checkpointed(&deps.storage, env.block.height) - .unwrap(); - let checkpoints = mixnodes - .changelog() - .keys(&deps.storage, None, None, Order::Ascending) - .filter_map(|x| x.ok()) - .collect::>(); - assert_eq!(checkpoints.len(), 1); - - let node_owner: Addr = Addr::unchecked("johnny"); - let node_identity_2 = test_helpers::add_mixnode( - node_owner.as_str(), - coins(10_000_000_000, TEST_COIN_DENOM), - deps.as_mut(), - ); - - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info("alice_d1", &[coin(8000_000000, TEST_COIN_DENOM)]), - node_identity_1.clone(), - ) - .unwrap(); - - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info("alice_d2", &[coin(2000_000000, TEST_COIN_DENOM)]), - node_identity_1.clone(), - ) - .unwrap(); - - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info("bob_d1", &[coin(8000_000000, TEST_COIN_DENOM)]), - node_identity_2.clone(), - ) - .unwrap(); - - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info("bob_d2", &[coin(2000_000000, TEST_COIN_DENOM)]), - node_identity_2.clone(), - ) - .unwrap(); - - let node_owner: Addr = Addr::unchecked("alicebob"); - let node_identity_3 = test_helpers::add_mixnode( - node_owner.as_str(), - coins(10_000_000_000 * 2, TEST_COIN_DENOM), - deps.as_mut(), - ); - - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info("alicebob_d1", &[coin(8000_000000 * 2, TEST_COIN_DENOM)]), - node_identity_3.clone(), - ) - .unwrap(); - - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info("alicebob_d2", &[coin(2000_000000 * 2, TEST_COIN_DENOM)]), - node_identity_3.clone(), - ) - .unwrap(); - - crate::delegations::transactions::_try_reconcile_all_delegation_events( - &mut deps.storage, - &deps.api, - ) - .unwrap(); - - env.block.height += 2 * constants::MINIMUM_BLOCK_AGE_FOR_REWARDING; - - let mix_1 = mixnodes_storage::read_full_mixnode_bond(&deps.storage, &node_identity_1) - .unwrap() - .unwrap(); - let mix_1_uptime = 100; - - let mix_2 = mixnodes_storage::read_full_mixnode_bond(&deps.storage, &node_identity_2) - .unwrap() - .unwrap(); - let mix_2_uptime = 50; - - let mix_3 = mixnodes_storage::read_full_mixnode_bond(&deps.storage, &node_identity_3) - .unwrap() - .unwrap(); - - // average of 1 and 2 - let mix_3_uptime = 75; - - let epoch = Interval::init_epoch(env.clone()); - save_epoch(&mut deps.storage, &epoch).unwrap(); - save_epoch_reward_params(epoch.id(), &mut deps.storage).unwrap(); - - let interval_reward_params = current_epoch_reward_params(&deps.storage).unwrap(); - - let node_reward_params = NodeRewardParams::new(0, mix_1_uptime, true); - let node_reward_params_2 = NodeRewardParams::new(0, mix_2_uptime, true); - let node_reward_params_3 = NodeRewardParams::new(0, mix_3_uptime, true); - - let mut params = RewardParams::new(interval_reward_params, node_reward_params); - let mut params2 = RewardParams::new(interval_reward_params, node_reward_params_2); - let mut params3 = RewardParams::new(interval_reward_params, node_reward_params_3); - - params.set_reward_blockstamp(env.block.height); - params2.set_reward_blockstamp(env.block.height); - params3.set_reward_blockstamp(env.block.height); - - assert_eq!(params.performance(), U128::from_num(1u32)); - assert_eq!(params2.performance(), U128::from_num(0.5f32)); - assert_eq!(params3.performance(), U128::from_num(0.75f32)); - - let mix_1_reward_result = mix_1.reward(¶ms); - - let info = mock_info(rewarding_validator_address.as_str(), &[]); - crate::mixnodes::transactions::try_checkpoint_mixnodes( - &mut deps.storage, - env.block.height, - info.clone(), - ) - .unwrap(); - mixnodes - .assert_checkpointed(&deps.storage, env.block.height) - .unwrap(); - - try_reward_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - node_identity_1.clone(), - node_reward_params, - ) - .unwrap(); - - let mix_after_reward = mixnodes.may_load(&deps.storage, &node_identity_1).unwrap(); - let accumulated1 = mix_after_reward - .as_ref() - .unwrap() - .accumulated_rewards - .unwrap() - .u128(); - // stupid one-off error, but @DU says it's fine - assert_eq!(accumulated1, 1948911 + 1); - - let checkpoints = mixnodes - .changelog() - .prefix(&node_identity_1) - .keys(&deps.storage, None, None, Order::Ascending) - .filter_map(|x| x.ok()) - .count(); - assert_eq!(checkpoints, 2); - - env.block.height += 10000; - env.block.time = env.block.time.plus_seconds(3601); - - try_advance_epoch( - env.clone(), - &mut deps.storage, - rewarding_validator_address.to_string(), - ) - .unwrap(); - - // After two snapshots we should see an increase in delegation - try_delegate_to_mixnode( - deps.as_mut(), - env.clone(), - mock_info("alice_d1", &[coin(8000_000000, TEST_COIN_DENOM)]), - node_identity_1.clone(), - ) - .unwrap(); - - crate::delegations::transactions::_try_reconcile_all_delegation_events( - &mut deps.storage, - &deps.api, - ) - .unwrap(); - - let info = mock_info(rewarding_validator_address.as_str(), &[]); - crate::mixnodes::transactions::try_checkpoint_mixnodes( - &mut deps.storage, - env.block.height, - info.clone(), - ) - .unwrap(); - mixnodes - .assert_checkpointed(&deps.storage, env.block.height) - .unwrap(); - - try_reward_mixnode( - deps.as_mut(), - env.clone(), - info, - node_identity_1.clone(), - node_reward_params, - ) - .unwrap(); - let mix_after_reward_2 = mixnodes.may_load(&deps.storage, &node_identity_1).unwrap(); - let accumulated2 = mix_after_reward_2 - .as_ref() - .unwrap() - .accumulated_rewards - .unwrap() - .u128(); - assert_eq!(accumulated2, accumulated1 + 2728477); - assert_ne!(mix_after_reward, mix_after_reward_2); - - let checkpoints = mixnodes - .changelog() - .prefix(&node_identity_1) - .keys(&deps.storage, None, None, Order::Ascending) - .collect::>>(); - assert_eq!(checkpoints.len(), 3); - - env.block.height += 10000; - env.block.time = env.block.time.plus_seconds(3601); - - try_advance_epoch( - env.clone(), - &mut deps.storage, - rewarding_validator_address.to_string(), - ) - .unwrap(); - - let info = mock_info(rewarding_validator_address.as_str(), &[]); - crate::mixnodes::transactions::try_checkpoint_mixnodes( - &mut deps.storage, - env.block.height, - info.clone(), - ) - .unwrap(); - mixnodes - .assert_checkpointed(&deps.storage, env.block.height) - .unwrap(); - - try_reward_mixnode( - deps.as_mut(), - env.clone(), - info, - node_identity_1.clone(), - node_reward_params, - ) - .unwrap(); - let mix_after_reward_3 = mixnodes.may_load(&deps.storage, &node_identity_1).unwrap(); - let accumulated3 = mix_after_reward_3 - .as_ref() - .unwrap() - .accumulated_rewards - .unwrap() - .u128(); - // off by one : ) - assert_eq!(accumulated3 + 1, accumulated2 + 2728477); - - let checkpoints = mixnodes - .changelog() - .prefix(&node_identity_1) - .keys(&deps.storage, None, None, Order::Ascending) - .filter_map(|x| x.ok()) - .collect::>(); - assert_eq!(checkpoints.len(), 4); - - let delegation_map = crate::delegations::storage::delegations(); - let key = "alice_d1".as_bytes().to_vec(); - - let last_claimed_height = storage::DELEGATOR_REWARD_CLAIMED_HEIGHT - .load(&deps.storage, (key.clone(), node_identity_1.to_string())) - .unwrap_or(0); - - assert_eq!(last_claimed_height, 0); - - let viable_delegations = delegation_map - .prefix((node_identity_1.to_string(), key.clone())) - .range(&deps.storage, None, None, Order::Descending) - .filter_map(|record| record.ok()) - .filter(|(height, _)| last_claimed_height <= *height) - .map(|(_, delegation)| delegation) - .collect::>(); - - assert_eq!(viable_delegations.len(), 2); - - let viable_heights = mixnodes - .changelog() - .prefix(&node_identity_1) - .keys(&deps.storage, None, None, Order::Ascending) - .filter_map(|height| height.ok()) - .filter(|height| last_claimed_height <= *height) - .collect::>(); - - // Should be equal to the number of checkpoints - assert_eq!(viable_heights.len(), 4); - - for (i, h) in viable_heights.into_iter().enumerate() { - let delegation_at_height = viable_delegations - .iter() - .filter(|d| d.block_height <= h) - .fold(Uint128::zero(), |total, delegation| { - total + delegation.amount.amount - }); - if i < 2 { - assert_eq!(delegation_at_height, Uint128::new(8000000000)); - } else { - assert_eq!(delegation_at_height, Uint128::new(16000000000)); + rewarding_details.delegates = Decimal::raw(12345); + rewarding_details.unique_delegations = 1; + storage::MIXNODE_REWARDING + .save( + test.deps_mut().storage, + mix_id_unbonded_leftover, + &rewarding_details, + ) + .unwrap(); + pending_events::unbond_mixnode(test.deps_mut(), &env, mix_id_unbonded).unwrap(); + + pending_events::unbond_mixnode(test.deps_mut(), &env, mix_id_unbonded_leftover) + .unwrap(); + + let env = test.env(); + let sender = test.rewarding_validator(); + let performance = test_helpers::performance(100.0); + + for &mix_id in &[ + mix_id_never_existed, + mix_id_unbonded, + mix_id_unbonded_leftover, + ] { + let res = try_reward_mixnode( + test.deps_mut(), + env.clone(), + sender.clone(), + mix_id, + performance, + ) + .unwrap(); + + let reason = find_attribute( + Some(MixnetEventType::MixnodeRewarding.to_string()), + NO_REWARD_REASON_KEY, + &res, + ); + assert_eq!(BOND_NOT_FOUND_VALUE, reason); } } - let alice_reward = - calculate_delegator_reward(&deps.storage, &deps.api, key.clone(), &node_identity_1) + #[test] + fn can_only_be_performed_once_epoch_is_over() { + let mut test = TestSetup::new(); + + let mix_id = test.add_dummy_mixnode("mix-owner", None); + let sender = test.rewarding_validator(); + + // node is in the active set BUT the current epoch has just begun + test.skip_to_next_epoch(); + test.update_rewarded_set(vec![mix_id]); + let performance = test_helpers::performance(100.); + + let env = test.env(); + let res = try_reward_mixnode(test.deps_mut(), env, sender.clone(), mix_id, performance); + assert!(matches!( + res, + Err(MixnetContractError::EpochInProgress { .. }) + )); + + // epoch is over (sanity check) + test.skip_to_current_epoch_end(); + let env = test.env(); + let res = try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance); + assert!(res.is_ok()); + } + + #[test] + fn can_only_be_performed_for_nodes_in_rewarded_set() { + let mut test = TestSetup::new(); + + let active_mix_id = test.add_dummy_mixnode("mix-owner-active", None); + let standby_mix_id = test.add_dummy_mixnode("mix-owner-standby", None); + let inactive_mix_id = test.add_dummy_mixnode("mix-owner-inactive", None); + let sender = test.rewarding_validator(); + + test.skip_to_next_epoch_end(); + + // manually set the rewarded set so that we'd have 1 active node, 1 standby and 1 inactive + interval_storage::REWARDED_SET + .save( + test.deps_mut().storage, + active_mix_id, + &RewardedSetNodeStatus::Active, + ) + .unwrap(); + interval_storage::REWARDED_SET + .save( + test.deps_mut().storage, + standby_mix_id, + &RewardedSetNodeStatus::Standby, + ) .unwrap(); - // TODO: perform deeper investigation into this number as it seem to not have compounded - // reward on the initial 8000 delegation and only have done it starting from 16000 - assert_eq!(alice_reward, Uint128::new(2737979)); + let performance = test_helpers::performance(100.); + let env = test.env(); + let res_active = try_reward_mixnode( + test.deps_mut(), + env.clone(), + sender.clone(), + active_mix_id, + performance, + ); + let res_standby = try_reward_mixnode( + test.deps_mut(), + env.clone(), + sender.clone(), + standby_mix_id, + performance, + ); + let res_inactive = + try_reward_mixnode(test.deps_mut(), env, sender, inactive_mix_id, performance); - let mix_0 = mixnodes.load(&deps.storage, &node_identity_1).unwrap(); + assert!(res_active.is_ok()); + assert!(res_standby.is_ok()); + assert!(matches!( + res_inactive, + Err(MixnetContractError::MixnodeNotInRewardedSet { node_id, .. }) if node_id == inactive_mix_id + )); + } - _try_compound_delegator_reward( - env.block.height, - deps.as_mut(), - "alice_d1", - &node_identity_1, - None, - ) - .unwrap(); + #[test] + fn can_only_be_performed_once_per_node_per_epoch() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); - crate::delegations::transactions::_try_reconcile_all_delegation_events( - &mut deps.storage, - &deps.api, - ) - .unwrap(); + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + let performance = test_helpers::performance(100.); + let env = test.env(); + let sender = test.rewarding_validator(); - let delegations = crate::delegations::storage::delegations() - .prefix((node_identity_1.to_string(), key.clone())) - .range(&deps.storage, None, None, Order::Ascending) - .filter_map(|x| x.ok()) - .map(|(_, delegation)| delegation) - .collect::>(); - assert_eq!(delegations.len(), 1); + // first rewarding + let res = try_reward_mixnode( + test.deps_mut(), + env.clone(), + sender.clone(), + mix_id, + performance, + ); + assert!(res.is_ok()); - let delegation = delegations.first().unwrap(); - assert_eq!( - delegation.amount.amount, - Uint128::new(16000000000 + 2737979) - ); + // second rewarding + let res = try_reward_mixnode(test.deps_mut(), env, sender.clone(), mix_id, performance); + assert!(matches!( + res, + Err(MixnetContractError::MixnodeAlreadyRewarded { node_id, .. }) if node_id == mix_id + )); - let mix_1 = mixnodes - .load(&deps.storage, &node_identity_1.clone()) + // in the following epoch we're good again + test.skip_to_next_epoch_end(); + let env = test.env(); + let res = try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance); + assert!(res.is_ok()); + } + + #[test] + fn requires_nonzero_performance_score() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + let zero_performance = test_helpers::performance(0.); + let performance = test_helpers::performance(100.0); + let env = test.env(); + let sender = test.rewarding_validator(); + + // first rewarding + let res = try_reward_mixnode( + test.deps_mut(), + env.clone(), + sender.clone(), + mix_id, + zero_performance, + ) .unwrap(); + let reason = find_attribute( + Some(MixnetEventType::MixnodeRewarding.to_string()), + NO_REWARD_REASON_KEY, + &res, + ); + assert_eq!(ZERO_PERFORMANCE_VALUE, reason); - _try_remove_delegation_from_mixnode(deps.as_mut(), env, node_identity_1, "alice_d1", None) - .unwrap(); + // sanity check: it's still treated as rewarding, so you we can't reward the node again + // with different performance for the same epoch + let res = try_reward_mixnode( + test.deps_mut(), + env, + sender.clone(), + mix_id, + zero_performance, + ); + assert!(matches!( + res, + Err(MixnetContractError::MixnodeAlreadyRewarded { node_id, .. }) if node_id == mix_id + )); - crate::delegations::transactions::_try_reconcile_all_delegation_events( - &mut deps.storage, - &deps.api, - ) - .unwrap(); + // but in the next epoch, as always, we're good again + test.skip_to_next_epoch_end(); + let env = test.env(); + let res = + try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance).unwrap(); - assert_eq!( - mix_0.accumulated_rewards(), - mix_1.accumulated_rewards() + alice_reward - ); + // rewards got distributed (in this test we don't care what they were exactly, but they must be non-zero) + let operator = find_attribute( + Some(MixnetEventType::MixnodeRewarding.to_string()), + OPERATOR_REWARD_KEY, + &res, + ); + assert!(!operator.is_empty()); + assert_ne!("0", operator); + let delegates = find_attribute( + Some(MixnetEventType::MixnodeRewarding.to_string()), + DELEGATES_REWARD_KEY, + &res, + ); + assert_eq!("0", delegates); + } - let operator_reward = - calculate_operator_reward(&deps.storage, &deps.api, &Addr::unchecked("alice"), &mix_1) + #[test] + fn correctly_accounts_for_rewards_distributed() { + let mut test = TestSetup::new(); + let mix_id1 = test.add_dummy_mixnode("mix-owner1", None); + let mix_id2 = test.add_dummy_mixnode("mix-owner2", None); + let mix_id3 = test.add_dummy_mixnode("mix-owner3", None); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id1, mix_id2, mix_id3]); + let performance = test_helpers::performance(98.0); + let env = test.env(); + let sender = test.rewarding_validator(); + + test.add_delegation("delegator1", Uint128::new(100_000_000), mix_id2); + + test.add_delegation("delegator1", Uint128::new(100_000_000), mix_id3); + test.add_delegation("delegator2", Uint128::new(123_456_000), mix_id3); + test.add_delegation("delegator3", Uint128::new(9_100_000_000), mix_id3); + + let change = storage::PENDING_REWARD_POOL_CHANGE + .load(test.deps().storage) .unwrap(); - assert_eq!(operator_reward, Uint128::new(2278901)); + assert!(change.removed.is_zero()); + assert!(change.added.is_zero()); - assert_eq!(mix_1_reward_result.sigma(), U128::from_num(0.0002f64)); - assert_eq!(mix_1_reward_result.lambda(), U128::from_num(0.0001f64)); - assert_eq!(mix_1_reward_result.reward().int(), accumulated1); + let mut total_operator = Decimal::zero(); + let mut total_delegates = Decimal::zero(); - let mix_2_reward_result = mix_2.reward(¶ms2); + for &mix_id in &[mix_id1, mix_id2, mix_id3] { + let before = storage::MIXNODE_REWARDING + .load(test.deps().storage, mix_id) + .unwrap(); - assert_eq!(mix_2_reward_result.sigma(), U128::from_num(0.0002f64)); - assert_eq!(mix_2_reward_result.lambda(), U128::from_num(0.0001f64)); - assert_eq!(mix_2_reward_result.reward().int(), 974456u128); + let res = try_reward_mixnode( + test.deps_mut(), + env.clone(), + sender.clone(), + mix_id, + performance, + ) + .unwrap(); + let operator: Decimal = find_attribute( + Some(MixnetEventType::MixnodeRewarding.to_string()), + OPERATOR_REWARD_KEY, + &res, + ) + .parse() + .unwrap(); + let delegates: Decimal = find_attribute( + Some(MixnetEventType::MixnodeRewarding.to_string()), + DELEGATES_REWARD_KEY, + &res, + ) + .parse() + .unwrap(); - let _mix_3_reward_result = mix_3.reward(¶ms3); + let after = storage::MIXNODE_REWARDING + .load(test.deps().storage, mix_id) + .unwrap(); - // assert_eq!(mix_3_reward_result.reward().int(), mix_1_reward_result.reward().int() + mix_2_reward_result.reward().int()); - } + // also the values emitted via events are consistent with the actual values! + let actual_operator = after.operator - before.operator; + let actual_delegates = after.delegates - before.delegates; + assert_eq!(actual_operator, operator); + assert_eq!(actual_delegates, delegates); - #[test] - fn test_tokenomics_rewarding() { - use crate::constants::INTERVAL_REWARD_PERCENT; - use crate::contract::INITIAL_REWARD_POOL; - use crate::support::helpers::epochs_in_interval; + total_operator += operator; + total_delegates += delegates; - type U128 = fixed::types::U75F53; + let change = storage::PENDING_REWARD_POOL_CHANGE + .load(test.deps().storage) + .unwrap(); + assert_eq!(change.removed, total_operator + total_delegates); + assert!(change.added.is_zero()); + } + } - let mut deps = test_helpers::init_contract(); - let mut env = mock_env(); - let current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_ref().storage) - .unwrap(); - let rewarding_validator_address = current_state.rewarding_validator_address; - let period_reward_pool = - (INITIAL_REWARD_POOL / 100 / epochs_in_interval(&deps.storage).unwrap() as u128) - * INTERVAL_REWARD_PERCENT as u128; - assert_eq!(period_reward_pool, 6_944_444_444); - let circulating_supply = storage::circulating_supply(&deps.storage).unwrap().u128(); - assert_eq!(circulating_supply, 750_000_000_000_000u128); + #[test] + fn correctly_splits_the_rewards() { + // we're basing this test on our simulator that we have separate unit tests for + // to determine whether values it spits are equal to what we expect - let node_owner: Addr = Addr::unchecked("alice"); - let node_identity = test_helpers::add_mixnode( - node_owner.as_str(), - coins(10_000_000_000, TEST_COIN_DENOM), - deps.as_mut(), - ); + let operator1 = Uint128::new(100_000_000); + let operator2 = Uint128::new(2_570_000_000); + let operator3 = Uint128::new(12_345_000_000); - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info("alice_d1", &[coin(8000_000000, TEST_COIN_DENOM)]), - node_identity.clone(), - ) - .unwrap(); + let mut test = TestSetup::new(); + let mix_id1 = test.add_dummy_mixnode("mix-owner1", Some(operator1)); + let mix_id2 = test.add_dummy_mixnode("mix-owner2", Some(operator2)); + let mix_id3 = test.add_dummy_mixnode("mix-owner3", Some(operator3)); - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info("alice_d2", &[coin(2000_000000, TEST_COIN_DENOM)]), - node_identity.clone(), - ) - .unwrap(); + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id1, mix_id2, mix_id3]); + let performance = test_helpers::performance(98.0); - crate::delegations::transactions::_try_reconcile_all_delegation_events( - &mut deps.storage, - &deps.api, - ) - .unwrap(); + test.add_delegation("delegator1", Uint128::new(100_000_000), mix_id2); - let info = mock_info(rewarding_validator_address.as_ref(), &[]); - env.block.height += 2 * constants::MINIMUM_BLOCK_AGE_FOR_REWARDING; + test.add_delegation("delegator1", Uint128::new(100_000_000), mix_id3); + test.add_delegation("delegator2", Uint128::new(123_456_000), mix_id3); + test.add_delegation("delegator3", Uint128::new(9_100_000_000), mix_id3); - let mix_1 = mixnodes_storage::read_full_mixnode_bond(&deps.storage, &node_identity) - .unwrap() - .unwrap(); - let mix_1_uptime = 90; + // repeat the rewarding the same set of delegates for few epochs + for _ in 0..10 { + for &mix_id in &[mix_id1, mix_id2, mix_id3] { + let mut sim = test.instantiate_simulator(mix_id); + let dist = test.reward_with_distribution(mix_id, performance); + let node_params = NodeRewardParams { + performance, + in_active_set: true, + }; + let sim_res = sim.simulate_epoch(node_params); + assert_eq!(sim_res, dist); + } + test.skip_to_next_epoch_end(); + } - let epoch = Interval::init_epoch(env.clone()); - save_epoch(&mut deps.storage, &epoch).unwrap(); - save_epoch_reward_params(epoch.id(), &mut deps.storage).unwrap(); + // add few more delegations and repeat it + // (note: we're not concerned about whether particular delegation owner got the correct amount, + // this is checked in other unit tests) + test.add_delegation("delegator1", Uint128::new(50_000_000), mix_id1); + test.add_delegation("delegator1", Uint128::new(200_000_000), mix_id2); - let interval_reward_params = current_epoch_reward_params(&deps.storage).unwrap(); + test.add_delegation("delegator5", Uint128::new(123_000_000), mix_id3); + test.add_delegation("delegator6", Uint128::new(456_000_000), mix_id3); - // ======================= - // TODO: this is a temporary 'workaround' until we can produce "good" numbers for if the rewards - // were calculated based of 100M staking supply as opposed to original 750M circulating supply - let interval_reward_params = EpochRewardParams::new( - interval_reward_params.epoch_reward_pool(), - interval_reward_params.rewarded_set_size(), - interval_reward_params.active_set_size(), - circulating_supply, - interval_reward_params.sybil_resistance_percent(), - interval_reward_params.active_set_work_factor(), - ); - - // this one repeats internals of `save_epoch_reward_params` but with 750M staking supply as opposed to 100M - crate::interval::storage::CURRENT_EPOCH_REWARD_PARAMS - .save(&mut deps.storage, &interval_reward_params) - .unwrap(); - crate::rewards::storage::EPOCH_REWARD_PARAMS - .save(&mut deps.storage, epoch.id(), &interval_reward_params) - .unwrap(); - // ======================= - - let node_reward_params = NodeRewardParams::new(0, mix_1_uptime, true); - - let mut params = RewardParams::new(interval_reward_params, node_reward_params); - - params.set_reward_blockstamp(env.block.height); - - assert_eq!(params.performance(), U128::from_num(0.8999999999999999f64)); - - let mix_1_reward_result = mix_1.reward(¶ms); - - assert_eq!( - mix_1_reward_result.sigma(), - U128::from_num(0.0000266666666666f64) - ); - assert_eq!( - mix_1_reward_result.lambda(), - U128::from_num(0.0000133333333333f64) - ); - assert_eq!(mix_1_reward_result.reward().int(), 233202u128); - - let base_operator_cost = current_operator_epoch_cost(&deps.storage).unwrap(); - - assert_eq!( - mix_1.node_profit(¶ms, base_operator_cost).int(), - 183203u128 - ); - - assert_ne!( - mix_1_reward_result.reward(), - mix_1.node_profit(¶ms, base_operator_cost).int() - ); - - let mix1_operator_reward = mix_1.operator_reward(¶ms, base_operator_cost); - - let mix1_delegator1_reward = - mix_1.reward_delegation(Uint128::new(8000_000000), ¶ms, base_operator_cost); - - let mix1_delegator2_reward = - mix_1.reward_delegation(Uint128::new(2000_000000), ¶ms, base_operator_cost); - - assert_eq!(mix1_operator_reward, 150761); - assert_eq!(mix1_delegator1_reward, 65953); - assert_eq!(mix1_delegator2_reward, 16488); - - assert_eq!( - mix_1_reward_result.reward().int(), - mix1_operator_reward + mix1_delegator1_reward + mix1_delegator2_reward - ); - - assert_eq!( - mix1_operator_reward + mix1_delegator1_reward + mix1_delegator2_reward, - mix_1_reward_result.reward().int() - ); - - let pre_reward_bond = - test_helpers::read_mixnode_pledge_amount(&deps.storage, &node_identity) - .unwrap() - .u128(); - assert_eq!(pre_reward_bond, 10_000_000_000); - - let pre_reward_delegation = mixnodes_storage::TOTAL_DELEGATION - .load(&deps.storage, &node_identity) - .unwrap() - .u128(); - assert_eq!(pre_reward_delegation, 10_000_000_000); - - try_reward_mixnode( - deps.as_mut(), - env, - info, - node_identity.clone(), - node_reward_params, - ) - .unwrap(); - - let mixnode = crate::mixnodes::storage::mixnodes() - .load(&deps.storage, &node_identity) - .unwrap(); - - assert_eq!( - test_helpers::read_mixnode_pledge_amount(&deps.storage, &node_identity) - .unwrap() - .u128() - + mixnode.accumulated_rewards().u128(), - pre_reward_bond - + mix1_operator_reward - + mix1_delegator1_reward - + mix1_delegator2_reward - ); - - assert_eq!( - storage::REWARD_POOL.load(&deps.storage).unwrap().u128(), - INITIAL_REWARD_POOL - - (mix1_operator_reward + mix1_delegator1_reward + mix1_delegator2_reward) - ); - - // it's all correctly saved - match storage::REWARDING_STATUS - .load(deps.as_ref().storage, (0u32, node_identity)) - .unwrap() - { - RewardingStatus::Complete(result) => assert_eq!( - RewardingResult { - node_reward: Uint128::new(mix_1_reward_result.reward().checked_cast().unwrap()), - }, - result - ), - _ => unreachable!(), + let performance = test_helpers::performance(12.3); + for _ in 0..10 { + for &mix_id in &[mix_id1, mix_id2, mix_id3] { + let mut sim = test.instantiate_simulator(mix_id); + let dist = test.reward_with_distribution(mix_id, performance); + let node_params = NodeRewardParams { + performance, + in_active_set: true, + }; + let sim_res = sim.simulate_epoch(node_params); + assert_eq!(sim_res, dist); + } + test.skip_to_next_epoch_end(); + } } } #[cfg(test)] - mod delegator_rewarding_tx { + mod withdrawing_operator_reward { use super::*; + use crate::interval::pending_events; + 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; #[test] - fn cannot_be_called_if_mixnode_is_fully_rewarded() { - // everything was done in a single reward call - let mut deps = test_helpers::init_contract(); - let mut env = mock_env(); - let current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - let rewarding_validator_address = current_state.rewarding_validator_address; + fn can_only_be_done_if_delegation_exists() { + let mut test = TestSetup::new(); + // add relatively huge stake so that the reward would be high enough to offset operating costs + let mix_id1 = + test.add_dummy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); + let mix_id2 = + test.add_dummy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); - let node_owner: Addr = Addr::unchecked("alice"); + let delegator1 = "delegator1"; + let delegator2 = "delegator2"; - #[allow(clippy::inconsistent_digit_grouping)] - let node_identity = test_helpers::add_mixnode( - node_owner.as_str(), - coins(10000_000_000, TEST_COIN_DENOM), - deps.as_mut(), + let sender1 = mock_info(delegator1, &[]); + let sender2 = mock_info(delegator2, &[]); + + // note that there's no delegation from delegator1 towards mix1 + test.add_immediate_delegation(delegator2, 100_000_000u128, mix_id1); + + test.add_immediate_delegation(delegator1, 100_000_000u128, mix_id2); + test.add_immediate_delegation(delegator2, 100_000_000u128, mix_id2); + + // perform some rewarding so that we'd have non-zero rewards + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id1, mix_id2]); + test.reward_with_distribution(mix_id1, test_helpers::performance(100.0)); + test.reward_with_distribution(mix_id2, test_helpers::performance(100.0)); + + let res = try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), mix_id1); + assert_eq!( + res, + Err(MixnetContractError::NoMixnodeDelegationFound { + mix_id: mix_id1, + address: delegator1.to_string(), + proxy: None, + }) ); - env.block.height += constants::MINIMUM_BLOCK_AGE_FOR_REWARDING; + // sanity check for other ones + let res = try_withdraw_delegator_reward(test.deps_mut(), sender1, mix_id2); + assert!(res.is_ok()); - let epoch = Interval::init_epoch(env.clone()); - save_epoch(&mut deps.storage, &epoch).unwrap(); - save_epoch_reward_params(epoch.id(), &mut deps.storage).unwrap(); + let res = try_withdraw_delegator_reward(test.deps_mut(), sender2.clone(), mix_id1); + assert!(res.is_ok()); - let info = mock_info(rewarding_validator_address.as_ref(), &[]); + let res = try_withdraw_delegator_reward(test.deps_mut(), sender2, mix_id2); + assert!(res.is_ok()); + } - try_reward_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), - ) - .unwrap(); + #[test] + fn tokens_are_only_sent_if_reward_is_nonzero() { + let mut test = TestSetup::new(); + // add relatively huge stake so that the reward would be high enough to offset operating costs + let mix_id1 = + test.add_dummy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); + let mix_id2 = + test.add_dummy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); - // there was another page of delegators, but they were already dealt with - let node_owner: Addr = Addr::unchecked("bob"); + // very low stake so operating cost would be higher than total reward + let low_stake_id = + test.add_dummy_mixnode("mix-owner3", Some(Uint128::new(100_000_000))); - #[allow(clippy::inconsistent_digit_grouping)] - let node_identity = test_helpers::add_mixnode( - node_owner.as_str(), - coins(10000_000_000, TEST_COIN_DENOM), - deps.as_mut(), - ); + let delegator = "delegator"; + let sender = mock_info(delegator, &[]); - for i in 0..50 + 1 { - try_delegate_to_mixnode( - deps.as_mut(), - env.clone(), - mock_info( - &*format!("delegator{:04}", i), - &[coin(2000_000000, TEST_COIN_DENOM)], - ), - node_identity.clone(), - ) - .unwrap(); + test.add_immediate_delegation(delegator, 100_000_000u128, mix_id1); + test.add_immediate_delegation(delegator, 100_000_000u128, mix_id2); + test.add_immediate_delegation(delegator, 1_000u128, low_stake_id); + + // reward mix1, but don't reward mix2 + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id1, low_stake_id]); + test.reward_with_distribution(mix_id1, test_helpers::performance(100.0)); + test.reward_with_distribution(low_stake_id, test_helpers::performance(100.0)); + + let res1 = + try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), mix_id1).unwrap(); + assert!(matches!( + &res1.messages[0].msg, + CosmosMsg::Bank(BankMsg::Send { to_address, amount }) if to_address == delegator && !amount[0].amount.is_zero() + ),); + + let res2 = + try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), mix_id2).unwrap(); + assert!(res2.messages.is_empty()); + + let res3 = + try_withdraw_delegator_reward(test.deps_mut(), sender, low_stake_id).unwrap(); + assert!(res3.messages.is_empty()); + } + + #[test] + fn can_only_be_done_for_fully_bonded_nodes() { + // note: if node has unbonded or is in the process of unbonding, the expected + // way of getting back the rewards is to completely undelegate + let mut test = TestSetup::new(); + let mix_id_unbonding = + test.add_dummy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); + let mix_id_unbonded_leftover = + test.add_dummy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); + + let delegator = "delegator"; + let sender = mock_info(delegator, &[]); + + test.add_immediate_delegation(delegator, 100_000_000u128, mix_id_unbonding); + test.add_immediate_delegation(delegator, 100_000_000u128, mix_id_unbonded_leftover); + + let performance = test_helpers::performance(100.0); + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id_unbonding, mix_id_unbonded_leftover]); + + // go through few rewarding cycles before unbonding nodes (partially or fully) + for _ in 0..10 { + test.reward_with_distribution(mix_id_unbonding, performance); + test.reward_with_distribution(mix_id_unbonded_leftover, performance); + + test.skip_to_next_epoch_end(); } - env.block.height += constants::MINIMUM_BLOCK_AGE_FOR_REWARDING; - test_helpers::update_env_and_progress_interval(&mut env, deps.as_mut().storage); + // start unbonding the first node and fully unbond the other + let mut bond = mixnodes_storage::mixnode_bonds() + .load(test.deps().storage, mix_id_unbonding) + .unwrap(); + bond.is_unbonding = true; + mixnodes_storage::mixnode_bonds() + .save(test.deps_mut().storage, mix_id_unbonding, &bond) + .unwrap(); - let info = mock_info(rewarding_validator_address.as_ref(), &[]); - try_reward_mixnode( - deps.as_mut(), - env, - info.clone(), - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), + let env = test.env(); + pending_events::unbond_mixnode(test.deps_mut(), &env, mix_id_unbonded_leftover) + .unwrap(); + + let res = + try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), mix_id_unbonding); + assert_eq!( + res, + Err(MixnetContractError::MixnodeIsUnbonding { + node_id: mix_id_unbonding + }) + ); + + let res = + try_withdraw_delegator_reward(test.deps_mut(), sender, mix_id_unbonded_leftover); + assert_eq!( + res, + Err(MixnetContractError::MixnodeHasUnbonded { + node_id: mix_id_unbonded_leftover + }) + ); + } + + #[test] + fn correctly_determines_earned_share_and_resets_reward_ratio() { + let mut test = TestSetup::new(); + let mix_id_single = + test.add_dummy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); + let mix_id_quad = + test.add_dummy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); + + let delegator1 = "delegator1"; + let delegator2 = "delegator2"; + let delegator3 = "delegator3"; + let delegator4 = "delegator4"; + let sender1 = mock_info(delegator1, &[]); + let sender2 = mock_info(delegator2, &[]); + let sender3 = mock_info(delegator3, &[]); + let sender4 = mock_info(delegator4, &[]); + + let amount_single = 100_000_000u128; + + let amount_quad1 = 50_000_000u128; + let amount_quad2 = 200_000_000u128; + let amount_quad3 = 250_000_000u128; + let amount_quad4 = 500_000_000u128; + + test.add_immediate_delegation(delegator1, amount_single, mix_id_single); + + test.add_immediate_delegation(delegator1, amount_quad1, mix_id_quad); + test.add_immediate_delegation(delegator2, amount_quad2, mix_id_quad); + test.add_immediate_delegation(delegator3, amount_quad3, mix_id_quad); + test.add_immediate_delegation(delegator4, amount_quad4, mix_id_quad); + + let performance = test_helpers::performance(100.0); + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id_single, mix_id_quad]); + + // accumulate some rewards + let mut accumulated_single = Decimal::zero(); + let mut accumulated_quad = Decimal::zero(); + for _ in 0..10 { + let dist = test.reward_with_distribution(mix_id_single, performance); + // sanity check to make sure test is actually doing what it's supposed to be doing + assert!(!dist.delegates.is_zero()); + + accumulated_single += dist.delegates; + let dist = test.reward_with_distribution(mix_id_quad, performance); + accumulated_quad += dist.delegates; + + test.skip_to_next_epoch_end(); + } + + let before = test.read_delegation(mix_id_single, delegator1, None); + assert_eq!(before.cumulative_reward_ratio, Decimal::zero()); + let res1 = + try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), mix_id_single) + .unwrap(); + let (_, reward) = test_helpers::get_bank_send_msg(&res1).unwrap(); + assert_eq!(truncate_reward_amount(accumulated_single), reward[0].amount); + let after = test.read_delegation(mix_id_single, delegator1, None); + assert_ne!(after.cumulative_reward_ratio, Decimal::zero()); + assert_eq!( + after.cumulative_reward_ratio, + test.mix_rewarding(mix_id_single).total_unit_reward + ); + + // withdraw first two rewards. note that due to scaling we expect second reward to be 4x the first one + let before1 = test.read_delegation(mix_id_quad, delegator1, None); + let before2 = test.read_delegation(mix_id_quad, delegator2, None); + assert_eq!(before1.cumulative_reward_ratio, Decimal::zero()); + assert_eq!(before2.cumulative_reward_ratio, Decimal::zero()); + let res1 = try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), mix_id_quad) + .unwrap(); + let (_, reward1) = test_helpers::get_bank_send_msg(&res1).unwrap(); + let res2 = try_withdraw_delegator_reward(test.deps_mut(), sender2.clone(), mix_id_quad) + .unwrap(); + let (_, reward2) = test_helpers::get_bank_send_msg(&res2).unwrap(); + // the seeming "error" comes from reward truncation, + // say "actual" reward1 was `100.9`, while reward2 was 4x that, i.e. `403.6 + // however, upon truncating and conversion to coins they'd be `100` and `403` respectively + // (clearly no longer holding the 4x ratio exactly), but this is NOT a bug, + // this is the expected behaviour. + // what we must assert here is that |a - b| <= ratio, rather than just a == b + // assert_eq!(reward1[0].amount * Uint128::new(4), reward2[0].amount); + assert_eq_with_leeway( + reward1[0].amount * Uint128::new(4), + reward2[0].amount, + Uint128::new(4), + ); + + let after1 = test.read_delegation(mix_id_quad, delegator1, None); + assert_ne!(after1.cumulative_reward_ratio, Decimal::zero()); + assert_eq!( + after1.cumulative_reward_ratio, + test.mix_rewarding(mix_id_quad).total_unit_reward + ); + let after2 = test.read_delegation(mix_id_quad, delegator2, None); + assert_ne!(after2.cumulative_reward_ratio, Decimal::zero()); + assert_eq!( + after2.cumulative_reward_ratio, + test.mix_rewarding(mix_id_quad).total_unit_reward + ); + + // accumulate some more + for _ in 0..10 { + let dist = test.reward_with_distribution(mix_id_quad, performance); + accumulated_quad += dist.delegates; + test.skip_to_next_epoch_end(); + } + + let before1_new = test.read_delegation(mix_id_quad, delegator1, None); + let before2_new = test.read_delegation(mix_id_quad, delegator2, None); + let before3 = test.read_delegation(mix_id_quad, delegator3, None); + let before4 = test.read_delegation(mix_id_quad, delegator4, None); + + assert_eq!( + before1_new.cumulative_reward_ratio, + after1.cumulative_reward_ratio + ); + assert_eq!( + before2_new.cumulative_reward_ratio, + after2.cumulative_reward_ratio + ); + assert_eq!(before3.cumulative_reward_ratio, Decimal::zero()); + assert_eq!(before4.cumulative_reward_ratio, Decimal::zero()); + + let res1 = + try_withdraw_delegator_reward(test.deps_mut(), sender1, mix_id_quad).unwrap(); + let (_, reward1_new) = test_helpers::get_bank_send_msg(&res1).unwrap(); + let res2 = + try_withdraw_delegator_reward(test.deps_mut(), sender2, mix_id_quad).unwrap(); + let (_, reward2_new) = test_helpers::get_bank_send_msg(&res2).unwrap(); + + // the ratio between first and second delegator is still the same + assert_eq_with_leeway( + reward1_new[0].amount * Uint128::new(4), + reward2_new[0].amount, + Uint128::new(4), + ); + + let res3 = + try_withdraw_delegator_reward(test.deps_mut(), sender3, mix_id_quad).unwrap(); + let (_, reward3) = test_helpers::get_bank_send_msg(&res3).unwrap(); + let res4 = + try_withdraw_delegator_reward(test.deps_mut(), sender4, mix_id_quad).unwrap(); + let (_, reward4) = test_helpers::get_bank_send_msg(&res4).unwrap(); + + // (and so is the ratio between 3rd and 4th) + assert_eq_with_leeway( + reward3[0].amount * Uint128::new(2), + reward4[0].amount, + Uint128::new(2), + ); + + // and finally the total distributed equals to total reward claimed + let total_claimed = reward1[0].amount + + reward2[0].amount + + reward1_new[0].amount + + reward2_new[0].amount + + reward3[0].amount + + reward4[0].amount; + + // we're adding 6 values together so our leeway can be at most 6 + let accumulated_actual = truncate_reward_amount(accumulated_quad); + assert_eq_with_leeway(total_claimed, accumulated_actual, Uint128::new(6)); + } + } + + #[cfg(test)] + mod withdrawing_delegator_reward { + use super::*; + use crate::interval::pending_events; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::{BankMsg, CosmosMsg, Uint128}; + + #[test] + fn can_only_be_done_if_bond_exists() { + let mut test = TestSetup::new(); + + let owner = "mix-owner"; + let mix_id = test.add_dummy_mixnode(owner, Some(Uint128::new(1_000_000_000_000))); + let sender = mock_info("random-guy", &[]); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let res = try_withdraw_operator_reward(test.deps_mut(), sender.clone()); + assert_eq!( + res, + Err(MixnetContractError::NoAssociatedMixNodeBond { + owner: sender.sender + }) ) - .unwrap(); + } + + #[test] + fn tokens_are_only_sent_if_reward_is_nonzero() { + let mut test = TestSetup::new(); + + let owner1 = "mix-owner1"; + let owner2 = "mix-owner2"; + let mix_id1 = test.add_dummy_mixnode(owner1, Some(Uint128::new(1_000_000_000_000))); + test.add_dummy_mixnode(owner2, Some(Uint128::new(1_000_000_000_000))); + + let sender1 = mock_info(owner1, &[]); + let sender2 = mock_info(owner2, &[]); + + // reward mix1, but don't reward mix2 + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id1]); + test.reward_with_distribution(mix_id1, test_helpers::performance(100.0)); + + let res1 = try_withdraw_operator_reward(test.deps_mut(), sender1).unwrap(); + assert!(matches!( + &res1.messages[0].msg, + CosmosMsg::Bank(BankMsg::Send { to_address, amount }) if to_address == owner1 && !amount[0].amount.is_zero() + ),); + + let res2 = try_withdraw_operator_reward(test.deps_mut(), sender2).unwrap(); + assert!(res2.messages.is_empty()); + } + + #[test] + fn can_only_be_done_for_fully_bonded_nodes() { + // note: if node has unbonded or is in the process of unbonding, the expected + // way of getting back the rewards is finish the undelegation + let mut test = TestSetup::new(); + let owner1 = "mix-owner1"; + let owner2 = "mix-owner2"; + let sender1 = mock_info(owner1, &[]); + let sender2 = mock_info(owner2, &[]); + let mix_id_unbonding = + test.add_dummy_mixnode(owner1, Some(Uint128::new(1_000_000_000_000))); + let mix_id_unbonded_leftover = + test.add_dummy_mixnode(owner2, Some(Uint128::new(1_000_000_000_000))); + + // add some delegation to the second node so that it wouldn't be cleared upon unbonding + test.add_immediate_delegation("delegator", 100_000_000u128, mix_id_unbonded_leftover); + + let performance = test_helpers::performance(100.0); + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id_unbonding, mix_id_unbonded_leftover]); + + // go through few rewarding cycles before unbonding nodes (partially or fully) + for _ in 0..10 { + test.reward_with_distribution(mix_id_unbonding, performance); + test.reward_with_distribution(mix_id_unbonded_leftover, performance); + + test.skip_to_next_epoch_end(); + } + + // start unbonding the first node and fully unbond the other + let mut bond = mixnodes_storage::mixnode_bonds() + .load(test.deps().storage, mix_id_unbonding) + .unwrap(); + bond.is_unbonding = true; + mixnodes_storage::mixnode_bonds() + .save(test.deps_mut().storage, mix_id_unbonding, &bond) + .unwrap(); + + let env = test.env(); + pending_events::unbond_mixnode(test.deps_mut(), &env, mix_id_unbonded_leftover) + .unwrap(); + + let res = try_withdraw_operator_reward(test.deps_mut(), sender1); + assert_eq!( + res, + Err(MixnetContractError::MixnodeIsUnbonding { + node_id: mix_id_unbonding + }) + ); + + let res = try_withdraw_operator_reward(test.deps_mut(), sender2); + assert_eq!( + res, + Err(MixnetContractError::NoAssociatedMixNodeBond { + owner: Addr::unchecked(owner2) + }) + ); + } + } + + #[cfg(test)] + mod updating_active_set { + use super::*; + use crate::support::tests::test_helpers::TestSetup; + + #[test] + fn can_only_be_done_by_contract_owner() { + let mut test = TestSetup::new(); + + let rewarding_validator = test.rewarding_validator(); + let owner = test.owner(); + let random = mock_info("random-guy", &[]); + + let env = test.env(); + let res = try_update_active_set_size( + test.deps_mut(), + env.clone(), + rewarding_validator, + 42, + false, + ); + assert_eq!(res, Err(MixnetContractError::Unauthorized)); + + let res = try_update_active_set_size(test.deps_mut(), env.clone(), random, 42, false); + assert_eq!(res, Err(MixnetContractError::Unauthorized)); + + let res = try_update_active_set_size(test.deps_mut(), env, owner, 42, false); + assert!(res.is_ok()) + } + + #[test] + fn new_size_cant_be_bigger_than_rewarded_set() { + let mut test = TestSetup::new(); + let owner = test.owner(); + + let rewarded_set_size = storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap() + .rewarded_set_size; + + let env = test.env(); + let res = try_update_active_set_size( + test.deps_mut(), + env, + owner.clone(), + rewarded_set_size + 1, + false, + ); + assert_eq!(res, Err(MixnetContractError::InvalidActiveSetSize)); + + // if its equal, its fine + // (make sure we start with the fresh state) + let mut test = TestSetup::new(); + let env = test.env(); + let res = try_update_active_set_size( + test.deps_mut(), + env, + owner.clone(), + rewarded_set_size, + false, + ); + assert!(res.is_ok()); + + // as well as if its any value lower than that + let mut test = TestSetup::new(); + let env = test.env(); + let res = try_update_active_set_size( + test.deps_mut(), + env, + owner, + rewarded_set_size - 100, + false, + ); + assert!(res.is_ok()); + } + + #[test] + fn if_interval_is_finished_change_happens_immediately() { + let mut test = TestSetup::new(); + let owner = test.owner(); + + let old = storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap() + .active_set_size; + test.skip_to_current_interval_end(); + let env = test.env(); + let res = try_update_active_set_size(test.deps_mut(), env, owner.clone(), 42, false); + assert!(res.is_ok()); + let new = storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap() + .active_set_size; + assert_ne!(old, new); + assert_eq!(new, 42); + + // sanity check for "normal" case + let mut test = TestSetup::new(); + let env = test.env(); + let res = try_update_active_set_size(test.deps_mut(), env, owner, 42, false); + assert!(res.is_ok()); + let new = storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap() + .active_set_size; + assert_eq!(old, new); + } + + #[test] + fn if_update_is_forced_it_happens_immediately() { + let mut test = TestSetup::new(); + let owner = test.owner(); + + let old = storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap() + .active_set_size; + let env = test.env(); + let res = try_update_active_set_size(test.deps_mut(), env, owner.clone(), 42, true); + assert!(res.is_ok()); + let new = storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap() + .active_set_size; + assert_ne!(old, new); + assert_eq!(new, 42); + } + + #[test] + fn without_forcing_it_change_happens_upon_clearing_epoch_events() { + let mut test = TestSetup::new(); + let owner = test.owner(); + + let old = storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap() + .active_set_size; + let env = test.env(); + let res = try_update_active_set_size(test.deps_mut(), env, owner, 42, false); + assert!(res.is_ok()); + let new = storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap() + .active_set_size; + assert_eq!(old, new); + + // make sure it's actually saved to pending events + let events = test.pending_epoch_events(); + assert!( + matches!(events[0], PendingEpochEventData::UpdateActiveSetSize { new_size } if new_size == 42) + ); + + test.execute_all_pending_events(); + let new = storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap() + .active_set_size; + assert_ne!(old, new); + assert_eq!(new, 42); + } + } + + #[cfg(test)] + mod updating_rewarding_params { + use super::*; + use crate::support::tests::test_helpers::{assert_decimals, TestSetup}; + use cosmwasm_std::Decimal; + + #[test] + fn can_only_be_done_by_contract_owner() { + let mut test = TestSetup::new(); + + let rewarding_validator = test.rewarding_validator(); + let owner = test.owner(); + let random = mock_info("random-guy", &[]); + + let update = IntervalRewardingParamsUpdate { + reward_pool: None, + staking_supply: None, + sybil_resistance_percent: None, + active_set_work_factor: None, + interval_pool_emission: None, + rewarded_set_size: Some(123), + }; + + let env = test.env(); + let res = try_update_rewarding_params( + test.deps_mut(), + env.clone(), + rewarding_validator, + update, + false, + ); + assert_eq!(res, Err(MixnetContractError::Unauthorized)); + + let res = + try_update_rewarding_params(test.deps_mut(), env.clone(), random, update, false); + assert_eq!(res, Err(MixnetContractError::Unauthorized)); + + let res = try_update_rewarding_params(test.deps_mut(), env, owner, update, false); + assert!(res.is_ok()) + } + + #[test] + fn request_must_contain_at_least_single_update() { + let mut test = TestSetup::new(); + let owner = test.owner(); + + let empty_update = IntervalRewardingParamsUpdate { + reward_pool: None, + staking_supply: None, + sybil_resistance_percent: None, + active_set_work_factor: None, + interval_pool_emission: None, + rewarded_set_size: None, + }; + + let env = test.env(); + let res = try_update_rewarding_params(test.deps_mut(), env, owner, empty_update, false); + assert_eq!(res, Err(MixnetContractError::EmptyParamsChangeMsg)); + } + + #[test] + fn if_interval_is_finished_change_happens_immediately() { + let mut test = TestSetup::new(); + let owner = test.owner(); + + let update = IntervalRewardingParamsUpdate { + reward_pool: None, + staking_supply: None, + sybil_resistance_percent: None, + active_set_work_factor: None, + interval_pool_emission: None, + rewarded_set_size: Some(123), + }; + + let old = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); + test.skip_to_current_interval_end(); + + let env = test.env(); + let res = + try_update_rewarding_params(test.deps_mut(), env, owner.clone(), update, false); + assert!(res.is_ok()); + let new = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); + assert_ne!(old, new); + assert_eq!(new.rewarded_set_size, 123); + + // sanity check for "normal" case + let mut test = TestSetup::new(); + let env = test.env(); + let res = try_update_rewarding_params(test.deps_mut(), env, owner, update, false); + assert!(res.is_ok()); + let new = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); + assert_eq!(old, new); + } + + #[test] + fn if_update_is_forced_it_happens_immediately() { + let mut test = TestSetup::new(); + let owner = test.owner(); + + let update = IntervalRewardingParamsUpdate { + reward_pool: None, + staking_supply: None, + sybil_resistance_percent: None, + active_set_work_factor: None, + interval_pool_emission: None, + rewarded_set_size: Some(123), + }; + + let old = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); + let env = test.env(); + let res = + try_update_rewarding_params(test.deps_mut(), env, owner.clone(), update, true); + assert!(res.is_ok()); + let new = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); + assert_ne!(old, new); + assert_eq!(new.rewarded_set_size, 123); + } + + #[test] + fn without_forcing_it_change_happens_upon_clearing_interval_events() { + let mut test = TestSetup::new(); + let owner = test.owner(); + + let update = IntervalRewardingParamsUpdate { + reward_pool: None, + staking_supply: None, + sybil_resistance_percent: None, + active_set_work_factor: None, + interval_pool_emission: None, + rewarded_set_size: Some(123), + }; + + let old = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); + let env = test.env(); + let res = try_update_rewarding_params(test.deps_mut(), env, owner, update, false); + assert!(res.is_ok()); + let new = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); + assert_eq!(old, new); + + // make sure it's actually saved to pending events + let events = test.pending_interval_events(); + assert!( + matches!(events[0],PendingIntervalEventData::UpdateRewardingParams { update } if update.rewarded_set_size == Some(123)) + ); + + test.execute_all_pending_events(); + let new = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); + assert_ne!(old, new); + assert_eq!(new.rewarded_set_size, 123); + } + + #[test] + fn upon_update_fields_are_recomputed_accordingly() { + let mut test = TestSetup::new(); + let owner = test.owner(); + + let old = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); + + let two = Decimal::from_atomics(2u32, 0).unwrap(); + let four = Decimal::from_atomics(4u32, 0).unwrap(); + + // TODO: be more fuzzy about it and try to vary other fields that can cause + // re-computation like pool emission or rewarded set size update + let update = IntervalRewardingParamsUpdate { + reward_pool: Some(old.interval.reward_pool / two), + staking_supply: Some(old.interval.staking_supply * four), + sybil_resistance_percent: None, + active_set_work_factor: None, + interval_pool_emission: None, + rewarded_set_size: None, + }; + + let env = test.env(); + let res = try_update_rewarding_params(test.deps_mut(), env, owner, update, true); + assert!(res.is_ok()); + let new = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); + + // with half the reward pool, our reward budget is also halved + assert_decimals( + old.interval.epoch_reward_budget, + two * new.interval.epoch_reward_budget, + ); + + // and with 4x the staking supply, the saturation point is also increased 4-folds + assert_decimals( + four * old.interval.stake_saturation_point, + new.interval.stake_saturation_point, + ); } } } diff --git a/contracts/mixnet/src/support/helpers.rs b/contracts/mixnet/src/support/helpers.rs index 7776e5522b..27d9202713 100644 --- a/contracts/mixnet/src/support/helpers.rs +++ b/contracts/mixnet/src/support/helpers.rs @@ -1,59 +1,160 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::constants::{DEFAULT_OPERATOR_INTERVAL_COST, INTERVAL_SECONDS}; -use crate::interval::storage::{current_epoch, EPOCHS}; +use crate::gateways::storage as gateways_storage; use crate::mixnodes::storage as mixnodes_storage; -use crate::{constants, gateways::storage as gateways_storage}; +use cosmwasm_std::{Addr, BankMsg, Coin, CosmosMsg, Deps, Response, Storage}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::{IdentityKeyRef, MixNodeBond}; -use crate::error::ContractError; -use cosmwasm_std::{Addr, Deps, Storage}; -use mixnet_contract_common::{reward_params::EpochRewardParams, IdentityKeyRef}; +// helper trait to attach `Msg` to a response if it's provided +pub(crate) trait AttachOptionalMessage { + fn add_optional_message(self, msg: Option>>) -> Self; +} -pub(crate) fn is_authorized(sender: String, storage: &dyn Storage) -> Result<(), ContractError> { +impl AttachOptionalMessage for Response { + fn add_optional_message(self, msg: Option>>) -> Self { + if let Some(msg) = msg { + self.add_message(msg) + } else { + self + } + } +} + +// pub fn debug_with_visibility>(api: &dyn Api, msg: S) { +// 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, +) -> Result { + // check if anything was put as bond + if pledge.is_empty() { + return Err(MixnetContractError::NoBondFound); + } + + if pledge.len() > 1 { + return Err(MixnetContractError::MultipleDenoms); + } + + // check that the denomination is correct + if pledge[0].denom != minimum_pledge.denom { + return Err(MixnetContractError::WrongDenom { + received: pledge[0].denom.clone(), + expected: minimum_pledge.denom, + }); + } + + // check that the pledge contains the minimum amount of tokens + if pledge[0].amount < minimum_pledge.amount { + return Err(MixnetContractError::InsufficientPledge { + received: pledge[0].clone(), + minimum: minimum_pledge, + }); + } + + Ok(pledge.pop().unwrap()) +} + +pub(crate) fn validate_delegation_stake( + mut delegation: Vec, + minimum_delegation: Option, + expected_denom: String, +) -> Result { + // check if anything was put as delegation + if delegation.is_empty() { + return Err(MixnetContractError::EmptyDelegation); + } + + if delegation.len() > 1 { + return Err(MixnetContractError::MultipleDenoms); + } + + // check that the denomination is correct + if delegation[0].denom != expected_denom { + return Err(MixnetContractError::WrongDenom { + received: delegation[0].denom.clone(), + expected: expected_denom, + }); + } + + // if we have a minimum set, check if enough tokens were sent, otherwise just check if its non-zero + if let Some(minimum_delegation) = minimum_delegation { + if delegation[0].amount < minimum_delegation.amount { + return Err(MixnetContractError::InsufficientDelegation { + received: delegation[0].clone(), + minimum: minimum_delegation, + }); + } + } else if delegation[0].amount.is_zero() { + return Err(MixnetContractError::EmptyDelegation); + } + + Ok(delegation.pop().unwrap()) +} + +pub(crate) fn ensure_is_authorized( + sender: Addr, + storage: &dyn Storage, +) -> Result<(), MixnetContractError> { if sender != crate::mixnet_contract_settings::storage::rewarding_validator_address(storage)? { - return Err(ContractError::Unauthorized); + return Err(MixnetContractError::Unauthorized); } Ok(()) } -pub fn epochs_in_interval(storage: &dyn Storage) -> Result { - let epoch = current_epoch(storage)?; - Ok(INTERVAL_SECONDS / epoch.length_secs()) -} - -#[allow(dead_code)] -pub fn current_operator_epoch_cost(storage: &dyn Storage) -> Result { - Ok(DEFAULT_OPERATOR_INTERVAL_COST / epochs_in_interval(storage)?) -} - -pub fn operator_cost_at_epoch(storage: &dyn Storage, epoch_id: u32) -> Result { - let epoch = EPOCHS.load(storage, epoch_id)?; - // This is historical, so we can't use the function defined above - let epochs_in_interval = INTERVAL_SECONDS / epoch.length_secs(); - Ok(DEFAULT_OPERATOR_INTERVAL_COST / epochs_in_interval) -} - -pub(crate) fn epoch_reward_params( +pub(crate) fn ensure_is_owner( + sender: Addr, storage: &dyn Storage, -) -> Result { - let state = crate::mixnet_contract_settings::storage::CONTRACT_STATE - .load(storage) - .map(|settings| settings.params)?; - let reward_pool = crate::rewards::storage::REWARD_POOL.load(storage)?; - let interval_reward_percent = crate::constants::INTERVAL_REWARD_PERCENT; - let epochs_in_interval = epochs_in_interval(storage)?; +) -> Result<(), MixnetContractError> { + if sender + != crate::mixnet_contract_settings::storage::CONTRACT_STATE + .load(storage)? + .owner + { + return Err(MixnetContractError::Unauthorized); + } + Ok(()) +} - let epoch_reward_params = EpochRewardParams::new( - (reward_pool.u128() / 100 / epochs_in_interval as u128) * interval_reward_percent as u128, - state.mixnode_rewarded_set_size as u128, - state.mixnode_active_set_size as u128, - state.staking_supply.u128(), - constants::SYBIL_RESISTANCE_PERCENT, - constants::ACTIVE_SET_WORK_FACTOR, - ); +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(()) +} - Ok(epoch_reward_params) +pub(crate) fn ensure_bonded(bond: &MixNodeBond) -> Result<(), MixnetContractError> { + if bond.is_unbonding { + return Err(MixnetContractError::MixnodeIsUnbonding { node_id: bond.id }); + } + Ok(()) } // check if the target address has already bonded a mixnode or gateway, @@ -61,14 +162,14 @@ pub(crate) fn epoch_reward_params( pub(crate) fn ensure_no_existing_bond( storage: &dyn Storage, sender: &Addr, -) -> Result<(), ContractError> { - if mixnodes_storage::mixnodes() +) -> Result<(), MixnetContractError> { + if mixnodes_storage::mixnode_bonds() .idx .owner .item(storage, sender.clone())? .is_some() { - return Err(ContractError::AlreadyOwnsMixnode); + return Err(MixnetContractError::AlreadyOwnsMixnode); } if gateways_storage::gateways() @@ -77,7 +178,7 @@ pub(crate) fn ensure_no_existing_bond( .item(storage, sender.clone())? .is_some() { - return Err(ContractError::AlreadyOwnsGateway); + return Err(MixnetContractError::AlreadyOwnsGateway); } Ok(()) @@ -88,7 +189,7 @@ pub(crate) fn validate_node_identity_signature( owner: &Addr, signature: String, identity: IdentityKeyRef<'_>, -) -> Result<(), ContractError> { +) -> Result<(), MixnetContractError> { let owner_bytes = owner.as_bytes(); let mut identity_bytes = [0u8; 32]; @@ -96,20 +197,20 @@ pub(crate) fn validate_node_identity_signature( let identity_used_bytes = bs58::decode(identity) .into(&mut identity_bytes) - .map_err(|err| ContractError::MalformedEd25519IdentityKey(err.to_string()))?; + .map_err(|err| MixnetContractError::MalformedEd25519IdentityKey(err.to_string()))?; let signature_used_bytes = bs58::decode(signature) .into(&mut signature_bytes) - .map_err(|err| ContractError::MalformedEd25519Signature(err.to_string()))?; + .map_err(|err| MixnetContractError::MalformedEd25519Signature(err.to_string()))?; if identity_used_bytes != 32 { - return Err(ContractError::MalformedEd25519IdentityKey( - "Too few bytes provided".into(), + return Err(MixnetContractError::MalformedEd25519IdentityKey( + "Too few bytes provided for the public key".into(), )); } if signature_used_bytes != 64 { - return Err(ContractError::MalformedEd25519Signature( - "Too few bytes provided".into(), + return Err(MixnetContractError::MalformedEd25519Signature( + "Too few bytes provided for the signature".into(), )); } @@ -118,7 +219,7 @@ pub(crate) fn validate_node_identity_signature( .ed25519_verify(owner_bytes, &signature_bytes, &identity_bytes) .map_err(cosmwasm_std::StdError::verification_err)?; if !res { - Err(ContractError::InvalidEd25519Signature) + Err(MixnetContractError::InvalidEd25519Signature) } else { Ok(()) } @@ -162,7 +263,7 @@ mod tests { .to_base58_string(); assert_eq!( - Err(ContractError::MalformedEd25519IdentityKey( + Err(MixnetContractError::MalformedEd25519IdentityKey( "buffer provided to decode base58 encoded string into was too small".into() )), validate_node_identity_signature( @@ -174,7 +275,7 @@ mod tests { ); assert_eq!( - Err(ContractError::MalformedEd25519Signature( + Err(MixnetContractError::MalformedEd25519Signature( "buffer provided to decode base58 encoded string into was too small".into() )), validate_node_identity_signature( @@ -186,8 +287,8 @@ mod tests { ); assert_eq!( - Err(ContractError::MalformedEd25519IdentityKey( - "Too few bytes provided".into() + Err(MixnetContractError::MalformedEd25519IdentityKey( + "Too few bytes provided for the public key".into() )), validate_node_identity_signature( deps.as_ref(), @@ -198,8 +299,8 @@ mod tests { ); assert_eq!( - Err(ContractError::MalformedEd25519Signature( - "Too few bytes provided".into() + Err(MixnetContractError::MalformedEd25519Signature( + "Too few bytes provided for the signature".into() )), validate_node_identity_signature( deps.as_ref(), @@ -210,7 +311,7 @@ mod tests { ); assert_eq!( - Err(ContractError::InvalidEd25519Signature), + Err(MixnetContractError::InvalidEd25519Signature), validate_node_identity_signature( deps.as_ref(), &address1, @@ -220,7 +321,7 @@ mod tests { ); assert_eq!( - Err(ContractError::InvalidEd25519Signature), + Err(MixnetContractError::InvalidEd25519Signature), validate_node_identity_signature( deps.as_ref(), &address1, @@ -230,7 +331,7 @@ mod tests { ); assert_eq!( - Err(ContractError::InvalidEd25519Signature), + Err(MixnetContractError::InvalidEd25519Signature), validate_node_identity_signature( deps.as_ref(), &address2, @@ -240,7 +341,7 @@ mod tests { ); assert_eq!( - Err(ContractError::InvalidEd25519Signature), + Err(MixnetContractError::InvalidEd25519Signature), validate_node_identity_signature( deps.as_ref(), &address1, diff --git a/contracts/mixnet/src/support/tests/fixtures.rs b/contracts/mixnet/src/support/tests/fixtures.rs index 813a68e537..5a3d5e38a6 100644 --- a/contracts/mixnet/src/support/tests/fixtures.rs +++ b/contracts/mixnet/src/support/tests/fixtures.rs @@ -1,12 +1,9 @@ -use crate::contract::INITIAL_MIXNODE_PLEDGE; -use crate::mixnodes::storage as mixnodes_storage; -use crate::{mixnodes::storage::StoredMixnodeBond, support::tests}; -use cosmwasm_std::{coin, Addr, Coin}; -use mixnet_contract_common::reward_params::NodeRewardParams; -use mixnet_contract_common::{Gateway, GatewayBond, Layer, MixNode}; +use crate::constants::{INITIAL_GATEWAY_PLEDGE_AMOUNT, INITIAL_MIXNODE_PLEDGE_AMOUNT}; +use cosmwasm_std::{coin, Coin}; +use mixnet_contract_common::mixnode::MixNodeCostParams; +use mixnet_contract_common::{Gateway, MixNode, Percent}; pub const TEST_COIN_DENOM: &str = "unym"; -pub const TEST_REWARDING_VALIDATOR_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; pub fn mix_node_fixture() -> MixNode { MixNode { @@ -17,7 +14,13 @@ pub fn mix_node_fixture() -> MixNode { sphinx_key: "sphinx".to_string(), identity_key: "identity".to_string(), version: "0.10.0".to_string(), - profit_margin_percent: 10, + } +} + +pub fn mix_node_cost_params_fixture() -> MixNodeCostParams { + MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(10).unwrap(), + interval_operating_cost: coin(40_000_000, TEST_COIN_DENOM), } } @@ -33,51 +36,16 @@ pub fn gateway_fixture() -> Gateway { } } -pub fn gateway_bond_fixture(owner: &str) -> GatewayBond { - let gateway = Gateway { - identity_key: format!("id-{}", owner), - ..tests::fixtures::gateway_fixture() - }; - GatewayBond::new( - coin(50, TEST_COIN_DENOM), - Addr::unchecked(owner), - 12_345, - gateway, - None, - ) -} - -pub(crate) fn stored_mixnode_bond_fixture(owner: &str) -> mixnodes_storage::StoredMixnodeBond { - StoredMixnodeBond::new( - coin(50, TEST_COIN_DENOM), - Addr::unchecked(owner), - Layer::One, - 12_345, - MixNode { - identity_key: format!("id-{}", owner), - sphinx_key: format!("sphinx-{}", owner), - ..super::fixtures::mix_node_fixture() - }, - None, - None, - None, - ) -} - pub fn good_mixnode_pledge() -> Vec { vec![Coin { denom: TEST_COIN_DENOM.to_string(), - amount: INITIAL_MIXNODE_PLEDGE, + amount: INITIAL_MIXNODE_PLEDGE_AMOUNT, }] } pub fn good_gateway_pledge() -> Vec { vec![Coin { denom: TEST_COIN_DENOM.to_string(), - amount: INITIAL_MIXNODE_PLEDGE, + amount: INITIAL_GATEWAY_PLEDGE_AMOUNT, }] } - -pub fn node_reward_params_fixture(uptime: u128) -> NodeRewardParams { - NodeRewardParams::new(0, uptime, true) -} diff --git a/contracts/mixnet/src/support/tests/messages.rs b/contracts/mixnet/src/support/tests/messages.rs index 75cb210ba0..f3593b3aca 100644 --- a/contracts/mixnet/src/support/tests/messages.rs +++ b/contracts/mixnet/src/support/tests/messages.rs @@ -1,33 +1,13 @@ -use mixnet_contract_common::{ExecuteMsg, Gateway, IdentityKey, MixNode, SphinxKey}; -use rand::thread_rng; +use mixnet_contract_common::{ExecuteMsg, Gateway, IdentityKey}; +use rand_chacha::rand_core::{CryptoRng, RngCore}; use crate::support::tests; -pub(crate) fn valid_bond_mixnode_msg(sender: &str) -> (ExecuteMsg, (IdentityKey, SphinxKey)) { - let keypair = crypto::asymmetric::identity::KeyPair::new(&mut thread_rng()); - let legit_sphinx_key = crypto::asymmetric::encryption::KeyPair::new(&mut thread_rng()); - let owner_signature = keypair - .private_key() - .sign(sender.as_bytes()) - .to_base58_string(); - - let identity_key = keypair.public_key().to_base58_string(); - let sphinx_key = legit_sphinx_key.public_key().to_base58_string(); - ( - ExecuteMsg::BondMixnode { - mix_node: MixNode { - identity_key: identity_key.clone(), - sphinx_key: sphinx_key.clone(), - ..tests::fixtures::mix_node_fixture() - }, - owner_signature, - }, - (identity_key, sphinx_key), - ) -} - -pub(crate) fn valid_bond_gateway_msg(sender: &str) -> (ExecuteMsg, IdentityKey) { - let keypair = crypto::asymmetric::identity::KeyPair::new(&mut thread_rng()); +pub(crate) fn valid_bond_gateway_msg( + mut rng: impl RngCore + CryptoRng, + sender: &str, +) -> (ExecuteMsg, IdentityKey) { + let keypair = crypto::asymmetric::identity::KeyPair::new(&mut rng); let owner_signature = keypair .private_key() .sign(sender.as_bytes()) diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index 19fe66dd35..8e0fba5ed6 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -1,5 +1,6 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 + #[cfg(test)] pub mod fixtures; #[cfg(test)] @@ -9,60 +10,707 @@ pub mod queries; #[cfg(test)] pub mod test_helpers { + use crate::constants; use crate::contract::instantiate; + use crate::delegations::queries::query_mixnode_delegations_paged; use crate::delegations::storage as delegations_storage; + use crate::delegations::transactions::try_delegate_to_mixnode; use crate::gateways::transactions::try_add_gateway; - use crate::interval; - use crate::interval::storage as interval_storage; + use crate::interval::transactions::{ + perform_pending_epoch_actions, perform_pending_interval_actions, + }; + use crate::interval::{pending_events, storage as interval_storage}; + use crate::mixnet_contract_settings::storage as mixnet_params_storage; + use crate::mixnet_contract_settings::storage::{ + minimum_mixnode_pledge, rewarding_denom, rewarding_validator_address, + }; use crate::mixnodes::storage as mixnodes_storage; - use crate::mixnodes::transactions::try_add_mixnode; + use crate::mixnodes::transactions::{ + try_add_mixnode, try_add_mixnode_on_behalf, try_remove_mixnode, + }; + use crate::rewards::storage as rewards_storage; + use crate::rewards::transactions::try_reward_mixnode; use crate::support::tests; + use crate::support::tests::fixtures::TEST_COIN_DENOM; use cosmwasm_std::testing::mock_dependencies; use cosmwasm_std::testing::mock_env; use cosmwasm_std::testing::mock_info; use cosmwasm_std::testing::MockApi; use cosmwasm_std::testing::MockQuerier; - use cosmwasm_std::Coin; - use cosmwasm_std::DepsMut; - use cosmwasm_std::OwnedDeps; - use cosmwasm_std::{coin, Env, Timestamp}; - use cosmwasm_std::{Addr, StdResult, Storage}; - use cosmwasm_std::{Empty, MemoryStorage}; - use mixnet_contract_common::{Delegation, Gateway, IdentityKeyRef, InstantiateMsg, MixNode}; - use rand::thread_rng; + use cosmwasm_std::{Addr, BankMsg, CosmosMsg, Storage}; + use cosmwasm_std::{Coin, Order}; + use cosmwasm_std::{Decimal, Empty, MemoryStorage}; + use cosmwasm_std::{Deps, OwnedDeps}; + use cosmwasm_std::{DepsMut, MessageInfo}; + use cosmwasm_std::{Env, Response, Timestamp, Uint128}; + use mixnet_contract_common::events::{ + may_find_attribute, MixnetEventType, DELEGATES_REWARD_KEY, OPERATOR_REWARD_KEY, + }; + use mixnet_contract_common::mixnode::{MixNodeRewarding, UnbondedMixnode}; + use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData}; + use mixnet_contract_common::reward_params::{Performance, RewardingParams}; + use mixnet_contract_common::rewarding::simulator::Simulator; + use mixnet_contract_common::rewarding::RewardDistribution; + use mixnet_contract_common::{ + Delegation, Gateway, InitialRewardingParams, InstantiateMsg, Interval, MixNode, NodeId, + Percent, RewardedSetNodeStatus, + }; + use rand_chacha::rand_core::{CryptoRng, RngCore, SeedableRng}; + use rand_chacha::ChaCha20Rng; + use std::time::Duration; - use super::fixtures::TEST_COIN_DENOM; - use super::fixtures::TEST_REWARDING_VALIDATOR_ADDRESS; + pub fn assert_eq_with_leeway(a: Uint128, b: Uint128, leeway: Uint128) { + if a > b { + assert!(a - b <= leeway) + } else { + assert!(b - a <= leeway) + } + } - pub fn add_mixnode(sender: &str, stake: Vec, deps: DepsMut<'_>) -> String { - let keypair = crypto::asymmetric::identity::KeyPair::new(&mut thread_rng()); + pub fn assert_decimals(a: Decimal, b: Decimal) { + let epsilon = Decimal::from_ratio(1u128, 100_000_000u128); + if a > b { + assert!(a - b < epsilon, "{} != {}", a, b) + } else { + assert!(b - a < epsilon, "{} != {}", a, b) + } + } + + pub struct TestSetup { + pub deps: OwnedDeps>, + pub env: Env, + pub rng: ChaCha20Rng, + + pub rewarding_validator: MessageInfo, + pub owner: MessageInfo, + } + + impl TestSetup { + pub fn new() -> Self { + 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) + .unwrap() + .owner; + + TestSetup { + deps, + env: mock_env(), + rng: test_rng(), + rewarding_validator: mock_info(rewarding_validator_address.as_ref(), &[]), + owner: mock_info(owner.as_str(), &[]), + } + } + + pub fn deps(&self) -> Deps<'_> { + self.deps.as_ref() + } + + pub fn deps_mut(&mut self) -> DepsMut<'_> { + self.deps.as_mut() + } + + pub fn env(&self) -> Env { + self.env.clone() + } + + pub fn rewarding_validator(&self) -> MessageInfo { + self.rewarding_validator.clone() + } + + pub fn rewarding_params(&self) -> RewardingParams { + rewards_storage::REWARDING_PARAMS + .load(self.deps().storage) + .unwrap() + } + + pub fn owner(&self) -> MessageInfo { + self.owner.clone() + } + + pub fn vesting_contract(&self) -> Addr { + mixnet_params_storage::CONTRACT_STATE + .load(self.deps().storage) + .unwrap() + .vesting_contract_address + } + + pub fn current_interval(&self) -> Interval { + interval_storage::current_interval(self.deps().storage).unwrap() + } + + pub fn rewarded_set(&self) -> Vec<(NodeId, RewardedSetNodeStatus)> { + interval_storage::REWARDED_SET + .range(self.deps().storage, None, None, Order::Ascending) + .map(|res| res.unwrap()) + .collect::>() + } + + pub fn add_dummy_mixnode(&mut self, owner: &str, stake: Option) -> NodeId { + 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 env = self.env(); + add_mixnode(&mut self.rng, self.deps.as_mut(), env, owner, vec![stake]) + } + + pub fn add_dummy_mixnode_with_proxy( + &mut self, + owner: &str, + stake: Option, + proxy: Addr, + ) -> NodeId { + 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 = crypto::asymmetric::identity::KeyPair::new(&mut self.rng); + let owner_signature = keypair + .private_key() + .sign(owner.as_bytes()) + .to_base58_string(); + + let legit_sphinx_key = crypto::asymmetric::encryption::KeyPair::new(&mut self.rng); + + let info = mock_info(proxy.as_str(), &[stake]); + let key = keypair.public_key().to_base58_string(); + 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 { + identity_key: key, + sphinx_key: legit_sphinx_key.public_key().to_base58_string(), + ..tests::fixtures::mix_node_fixture() + }, + 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 + } + + pub fn start_unbonding_mixnode(&mut self, mix_id: NodeId) { + let bond_details = mixnodes_storage::mixnode_bonds() + .load(self.deps().storage, mix_id) + .unwrap(); + + try_remove_mixnode(self.deps_mut(), mock_info(bond_details.owner.as_str(), &[])) + .unwrap(); + } + + pub fn immediately_unbond_mixnode(&mut self, mix_id: NodeId) { + let env = self.env(); + pending_events::unbond_mixnode(self.deps_mut(), &env, mix_id).unwrap(); + } + + pub fn add_immediate_delegation( + &mut self, + delegator: &str, + amount: impl Into, + target: NodeId, + ) { + let denom = rewarding_denom(self.deps().storage).unwrap(); + let amount = Coin { + denom, + amount: amount.into(), + }; + let env = self.env(); + pending_events::delegate( + self.deps_mut(), + &env, + Addr::unchecked(delegator), + target, + amount, + None, + ) + .unwrap(); + } + + pub fn add_immediate_delegation_with_proxy( + &mut self, + delegator: &str, + amount: impl Into, + target: NodeId, + proxy: Addr, + ) { + let denom = rewarding_denom(self.deps().storage).unwrap(); + let amount = Coin { + denom, + amount: amount.into(), + }; + let env = self.env(); + pending_events::delegate( + self.deps_mut(), + &env, + Addr::unchecked(delegator), + target, + amount, + Some(proxy), + ) + .unwrap(); + } + + pub fn add_delegation( + &mut self, + delegator: &str, + amount: impl Into, + target: NodeId, + ) { + let denom = rewarding_denom(self.deps().storage).unwrap(); + let amount = Coin { + denom, + amount: amount.into(), + }; + delegate(self.deps_mut(), delegator, vec![amount], target) + } + + pub fn remove_immediate_delegation(&mut self, delegator: &str, target: NodeId) { + pending_events::undelegate(self.deps_mut(), Addr::unchecked(delegator), target, None) + .unwrap(); + } + + pub fn skip_to_next_epoch_end(&mut self) { + self.skip_to_next_epoch(); + self.skip_to_current_epoch_end(); + } + + pub fn skip_to_current_epoch_end(&mut self) { + let interval = interval_storage::current_interval(self.deps().storage).unwrap(); + let epoch_end = interval.current_epoch_end_unix_timestamp(); + // skip few blocks just in case + self.env.block.height += 10; + self.env.block.time = Timestamp::from_seconds(epoch_end as u64); + } + + pub fn skip_to_current_interval_end(&mut self) { + let interval = interval_storage::current_interval(self.deps().storage).unwrap(); + let interval_end = interval.current_interval_end_unix_timestamp(); + // skip few blocks just in case + self.env.block.height += 10; + self.env.block.time = Timestamp::from_seconds(interval_end as u64); + } + + pub fn skip_to_next_epoch(&mut self) { + let interval = interval_storage::current_interval(self.deps().storage).unwrap(); + let epoch_end = interval.current_epoch_end_unix_timestamp(); + // skip few blocks just in case + self.env.block.height += 10; + self.env.block.time = Timestamp::from_seconds(epoch_end as u64 + 1); + let advanced = interval.advance_epoch(); + + if interval.current_epoch_id() != interval.epochs_in_interval() { + assert_eq!(interval.current_epoch_id() + 1, advanced.current_epoch_id()) + } + + interval_storage::save_interval(self.deps_mut().storage, &advanced).unwrap() + } + + pub fn update_rewarded_set(&mut self, nodes: Vec) { + let active_set_size = rewards_storage::REWARDING_PARAMS + .load(self.deps().storage) + .unwrap() + .active_set_size; + interval_storage::update_rewarded_set(self.deps_mut().storage, active_set_size, nodes) + .unwrap(); + } + + pub fn instantiate_simulator(&self, node: NodeId) -> Simulator { + simulator_from_state(self.deps(), node) + } + + pub fn execute_all_pending_events(&mut self) { + let env = self.env(); + execute_all_pending_events(self.deps_mut(), env) + } + + pub fn pending_interval_events(&self) -> Vec { + interval_storage::PENDING_INTERVAL_EVENTS + .range(self.deps().storage, None, None, Order::Ascending) + .map(|res| res.unwrap().1) + .collect::>() + } + + pub fn pending_epoch_events(&self) -> Vec { + interval_storage::PENDING_EPOCH_EVENTS + .range(self.deps().storage, None, None, Order::Ascending) + .map(|res| res.unwrap().1) + .collect::>() + } + + pub fn reward_with_distribution( + &mut self, + mix_id: NodeId, + performance: Performance, + ) -> RewardDistribution { + let env = self.env(); + let sender = self.rewarding_validator(); + + let res = + try_reward_mixnode(self.deps_mut(), env, sender, mix_id, performance).unwrap(); + let operator: Decimal = find_attribute( + Some(MixnetEventType::MixnodeRewarding.to_string()), + OPERATOR_REWARD_KEY, + &res, + ) + .parse() + .unwrap(); + let delegates: Decimal = find_attribute( + Some(MixnetEventType::MixnodeRewarding.to_string()), + DELEGATES_REWARD_KEY, + &res, + ) + .parse() + .unwrap(); + + RewardDistribution { + operator, + delegates, + } + } + + pub fn read_delegation( + &mut self, + mix: NodeId, + owner: &str, + proxy: Option<&str>, + ) -> Delegation { + read_delegation( + self.deps().storage, + mix, + &Addr::unchecked(owner), + &proxy.map(Addr::unchecked), + ) + .unwrap() + } + + pub fn mix_rewarding(&self, node: NodeId) -> MixNodeRewarding { + rewards_storage::MIXNODE_REWARDING + .load(self.deps().storage, node) + .unwrap() + } + + pub fn delegation(&self, mix: NodeId, owner: &str, proxy: &Option) -> Delegation { + read_delegation(self.deps().storage, mix, &Addr::unchecked(owner), proxy).unwrap() + } + } + + pub fn simulator_from_state(deps: Deps<'_>, node: NodeId) -> Simulator { + let mix_rewarding = rewards_storage::MIXNODE_REWARDING + .load(deps.storage, node) + .unwrap(); + let delegations = query_mixnode_delegations_paged(deps, node, None, None).unwrap(); + if delegations.delegations.len() as u32 + == constants::DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT + { + // can't be bothered to deal with paging for this test case since it's incredibly unlikely + // we'd ever need it + panic!("too many delegations") + } + let rewarding_params = rewards_storage::REWARDING_PARAMS + .load(deps.storage) + .unwrap(); + let interval = interval_storage::current_interval(deps.storage).unwrap(); + Simulator { + node_rewarding_details: mix_rewarding, + node_delegations: delegations.delegations, + system_rewarding_params: rewarding_params, + interval, + } + } + + pub fn get_bank_send_msg(response: &Response) -> Option<(String, Vec)> { + for msg in &response.messages { + if let CosmosMsg::Bank(BankMsg::Send { to_address, amount }) = &msg.msg { + return Some((to_address.clone(), amount.clone())); + } + } + None + } + + pub fn find_attribute( + event_type: Option, + attribute: &str, + response: &Response, + ) -> String { + for event in &response.events { + if let Some(typ) = &event_type { + if &event.ty != typ { + continue; + } + } + if let Some(attr) = may_find_attribute(event, attribute) { + return attr; + } + } + // this is only used in tests so panic here is fine + panic!("did not find the attribute") + } + + // using floats in tests is fine + // (what it does is converting % value, like 12.34 into `Performance` (`Percent`) + // which internally is represented by decimal `0.1234` + pub fn performance(val: f32) -> Performance { + assert!(val <= 100.0); + assert!(val >= 0.0); + + // hehe, that's such a nasty conversion, but it works for test purposes + let str = (val / 100.0).to_string(); + let dec = str.parse().unwrap(); + Performance::new(dec).unwrap() + } + + // use rng with constant seed for all tests so that they would be deterministic + pub fn test_rng() -> ChaCha20Rng { + let dummy_seed = [42u8; 32]; + rand_chacha::ChaCha20Rng::from_seed(dummy_seed) + } + + pub fn execute_all_pending_events(mut deps: DepsMut<'_>, env: Env) { + perform_pending_epoch_actions(deps.branch(), &env, None).unwrap(); + perform_pending_interval_actions(deps.branch(), &env, None).unwrap(); + } + + pub fn mixnode_with_signature( + mut rng: impl RngCore + CryptoRng, + sender: &str, + ) -> (MixNode, String) { + let keypair = crypto::asymmetric::identity::KeyPair::new(&mut rng); + let legit_sphinx_key = crypto::asymmetric::encryption::KeyPair::new(&mut rng); let owner_signature = keypair .private_key() .sign(sender.as_bytes()) .to_base58_string(); - let legit_sphinx_key = crypto::asymmetric::encryption::KeyPair::new(&mut thread_rng()); + let identity_key = keypair.public_key().to_base58_string(); + let sphinx_key = legit_sphinx_key.public_key().to_base58_string(); - let info = mock_info(sender, &stake); - let key = keypair.public_key().to_base58_string(); - - try_add_mixnode( - deps, - mock_env(), - info, + ( MixNode { - identity_key: key.clone(), - sphinx_key: legit_sphinx_key.public_key().to_base58_string(), + identity_key, + sphinx_key, ..tests::fixtures::mix_node_fixture() }, owner_signature, ) - .unwrap(); - key } - pub fn add_gateway(sender: &str, stake: Vec, deps: DepsMut<'_>) -> String { - let keypair = crypto::asymmetric::identity::KeyPair::new(&mut thread_rng()); + pub fn gateway_with_signature( + mut rng: impl RngCore + CryptoRng, + sender: &str, + ) -> (Gateway, String) { + let keypair = crypto::asymmetric::identity::KeyPair::new(&mut rng); + let legit_sphinx_key = crypto::asymmetric::encryption::KeyPair::new(&mut rng); + let owner_signature = keypair + .private_key() + .sign(sender.as_bytes()) + .to_base58_string(); + + let identity_key = keypair.public_key().to_base58_string(); + let sphinx_key = legit_sphinx_key.public_key().to_base58_string(); + + ( + Gateway { + identity_key, + sphinx_key, + ..tests::fixtures::gateway_fixture() + }, + owner_signature, + ) + } + + pub fn add_dummy_delegations(mut deps: DepsMut<'_>, env: Env, mix_id: NodeId, n: usize) { + for i in 0..n { + pending_events::delegate( + deps.branch(), + &env, + Addr::unchecked(&format!("owner{}", i)), + mix_id, + tests::fixtures::good_mixnode_pledge().pop().unwrap(), + None, + ) + .unwrap(); + } + } + + pub fn add_dummy_mixnodes( + mut rng: impl RngCore + CryptoRng, + mut deps: DepsMut<'_>, + env: Env, + n: usize, + ) { + for i in 0..n { + add_mixnode( + &mut rng, + deps.branch(), + env.clone(), + &format!("owner{}", i), + tests::fixtures::good_mixnode_pledge(), + ); + } + } + + pub fn add_dummy_gateways( + mut rng: impl RngCore + CryptoRng, + mut deps: DepsMut<'_>, + env: Env, + n: usize, + ) { + for i in 0..n { + add_gateway( + &mut rng, + deps.branch(), + env.clone(), + &format!("owner{}", i), + tests::fixtures::good_mixnode_pledge(), + ); + } + } + + pub fn add_dummy_unbonded_mixnodes( + mut rng: impl RngCore + CryptoRng, + mut deps: DepsMut<'_>, + n: usize, + ) { + for i in 0..n { + add_unbonded_mixnode(&mut rng, deps.branch(), None, &format!("owner{}", i)); + } + } + + pub fn add_dummy_unbonded_mixnodes_with_owner( + mut rng: impl RngCore + CryptoRng, + mut deps: DepsMut<'_>, + owner: &str, + n: usize, + ) { + for _ in 0..n { + add_unbonded_mixnode(&mut rng, deps.branch(), None, owner); + } + } + + pub fn add_dummy_unbonded_mixnodes_with_identity( + mut rng: impl RngCore + CryptoRng, + mut deps: DepsMut<'_>, + identity: &str, + n: usize, + ) { + for i in 0..n { + add_unbonded_mixnode( + &mut rng, + deps.branch(), + Some(identity), + &format!("owner{}", i), + ); + } + } + + // same note as with `add_mixnode` + pub fn add_unbonded_mixnode( + mut rng: impl RngCore + CryptoRng, + deps: DepsMut<'_>, + identity_key: Option<&str>, + owner: &str, + ) -> NodeId { + let id = loop { + let candidate = rng.next_u32(); + if !mixnodes_storage::unbonded_mixnodes().has(deps.storage, candidate) { + break candidate; + } + }; + + // we don't care about 'correctness' of the identity key here + mixnodes_storage::unbonded_mixnodes() + .save( + deps.storage, + id, + &UnbondedMixnode { + identity_key: identity_key + .unwrap_or(&*format!("identity{}", id)) + .to_string(), + owner: Addr::unchecked(owner), + proxy: None, + unbonding_height: 12345, + }, + ) + .unwrap(); + + id + } + + // note to whoever wants to refactor this function, you dont want to grab rng here directly + // via `let rng = test_rng()` + // because it's extremely likely you might end up calling `add_mixnode()` multiple times + // in the same test and thus you're going to get mixnodes with the same keys and that's + // not what you want (presumably) + pub fn add_mixnode( + mut rng: impl RngCore + CryptoRng, + deps: DepsMut<'_>, + env: Env, + sender: &str, + stake: Vec, + ) -> NodeId { + let keypair = crypto::asymmetric::identity::KeyPair::new(&mut rng); + let owner_signature = keypair + .private_key() + .sign(sender.as_bytes()) + .to_base58_string(); + + let legit_sphinx_key = crypto::asymmetric::encryption::KeyPair::new(&mut rng); + + let info = mock_info(sender, &stake); + let key = keypair.public_key().to_base58_string(); + let current_id_counter = mixnodes_storage::MIXNODE_ID_COUNTER + .may_load(deps.storage) + .unwrap() + .unwrap_or_default(); + + try_add_mixnode( + deps, + env, + info, + MixNode { + identity_key: key, + sphinx_key: legit_sphinx_key.public_key().to_base58_string(), + ..tests::fixtures::mix_node_fixture() + }, + tests::fixtures::mix_node_cost_params_fixture(), + owner_signature, + ) + .unwrap(); + + // newly added mixnode gets assigned the current counter + 1 + current_id_counter + 1 + } + + // same note as with `add_mixnode` + pub fn add_gateway( + mut rng: impl RngCore + CryptoRng, + deps: DepsMut<'_>, + env: Env, + sender: &str, + stake: Vec, + ) -> String { + let keypair = crypto::asymmetric::identity::KeyPair::new(&mut rng); let owner_signature = keypair .private_key() .sign(sender.as_bytes()) @@ -72,7 +720,7 @@ pub mod test_helpers { let key = keypair.public_key().to_base58_string(); try_add_gateway( deps, - mock_env(), + env, info, Gateway { identity_key: key.clone(), @@ -84,11 +732,30 @@ pub mod test_helpers { key } + fn initial_rewarding_params() -> InitialRewardingParams { + let reward_pool = 250_000_000_000_000u128; + let staking_supply = 100_000_000_000_000u128; + + InitialRewardingParams { + initial_reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens) + initial_staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M + sybil_resistance: Percent::from_percentage_value(30).unwrap(), + active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), + interval_pool_emission: Percent::from_percentage_value(2).unwrap(), + rewarded_set_size: 240, + active_set_size: 100, + } + } + pub fn init_contract() -> OwnedDeps> { let mut deps = mock_dependencies(); let msg = InstantiateMsg { - rewarding_validator_address: TEST_REWARDING_VALIDATOR_ADDRESS.to_string(), - mixnet_denom: TEST_COIN_DENOM.to_string(), + rewarding_validator_address: "rewarder".into(), + vesting_contract_address: "vesting-contract".to_string(), + rewarding_denom: TEST_COIN_DENOM.to_string(), + epochs_in_interval: 720, + epoch_duration: Duration::from_secs(60 * 60), + initial_rewarding_params: initial_rewarding_params(), }; let env = mock_env(); let info = mock_info("creator", &[]); @@ -96,58 +763,22 @@ pub mod test_helpers { deps } - // currently not used outside tests - pub(crate) fn read_mixnode_pledge_amount( - storage: &dyn Storage, - identity: IdentityKeyRef<'_>, - ) -> StdResult { - let node = mixnodes_storage::mixnodes().load(storage, identity)?; - Ok(node.pledge_amount.amount) - } - - pub(crate) fn save_dummy_delegation( - storage: &mut dyn Storage, - mix: impl Into, - owner: impl Into, - block_height: u64, - ) { - let delegation = Delegation { - owner: Addr::unchecked(owner.into()), - node_identity: mix.into(), - amount: coin(12345, TEST_COIN_DENOM), - block_height: block_height, - proxy: None, - }; - - delegations_storage::delegations() - .save(storage, delegation.storage_key(), &delegation) - .unwrap(); + pub fn delegate(deps: DepsMut<'_>, sender: &str, stake: Vec, mix_id: NodeId) { + let info = mock_info(sender, &stake); + try_delegate_to_mixnode(deps, info, mix_id).unwrap(); } pub(crate) fn read_delegation( storage: &dyn Storage, - mix: impl Into, - owner: impl Into>, - block_height: u64, + mix: NodeId, + owner: &Addr, + proxy: &Option, ) -> Option { delegations_storage::delegations() - .may_load(storage, (mix.into(), owner.into(), block_height)) + .may_load( + storage, + Delegation::generate_storage_key(mix, owner, proxy.as_ref()), + ) .unwrap() } - - pub(crate) fn update_env_and_progress_interval(env: &mut Env, storage: &mut dyn Storage) { - // make sure current block time is within the expected next interval - env.block.time = Timestamp::from_seconds( - (interval_storage::current_epoch(storage) - .unwrap() - .next() - .start_unix_timestamp() - + 123) as u64, - ); - - let sender = - crate::mixnet_contract_settings::storage::rewarding_validator_address(storage).unwrap(); - - interval::transactions::try_advance_epoch(env.clone(), storage, sender).unwrap(); - } } diff --git a/contracts/mixnet/src/support/tests/queries.rs b/contracts/mixnet/src/support/tests/queries.rs index e53caf148e..1955e8a85c 100644 --- a/contracts/mixnet/src/support/tests/queries.rs +++ b/contracts/mixnet/src/support/tests/queries.rs @@ -1,31 +1,13 @@ +use crate::contract::query; use cosmwasm_std::{ from_binary, testing::{mock_env, MockApi, MockQuerier, MockStorage}, - Addr, Coin, OwnedDeps, -}; -use mixnet_contract_common::{ - GatewayBond, MixNodeBond, PagedGatewayResponse, PagedMixnodeResponse, QueryMsg, + OwnedDeps, }; +use mixnet_contract_common::{GatewayBond, PagedGatewayResponse, QueryMsg}; -use crate::contract::query; - -use super::fixtures::TEST_COIN_DENOM; - -pub fn get_mix_nodes(deps: &mut OwnedDeps) -> Vec { - let result = query( - deps.as_ref(), - mock_env(), - QueryMsg::GetMixNodes { - start_after: None, - limit: Option::from(2), - }, - ) - .unwrap(); - - let page: PagedMixnodeResponse = from_binary(&result).unwrap(); - page.nodes -} - +// I honestly don't know why we're using this way of querying in tests, but I couldn't be bothered to change it +// since I haven't done anything to gateways pub fn get_gateways(deps: &mut OwnedDeps) -> Vec { let result = query( deps.as_ref(), @@ -38,13 +20,20 @@ pub fn get_gateways(deps: &mut OwnedDeps) -> .unwrap(); let page: PagedGatewayResponse = from_binary(&result).unwrap(); + if page.start_next_after.is_some() { + let next_page = query( + deps.as_ref(), + mock_env(), + QueryMsg::GetGateways { + start_after: page.start_next_after, + limit: None, + }, + ) + .unwrap(); + let next_page: PagedGatewayResponse = from_binary(&next_page).unwrap(); + if !next_page.nodes.is_empty() { + panic!("we didn't manage to get all gateways in a single query") + } + } page.nodes } - -pub fn query_contract_balance( - address: Addr, - deps: OwnedDeps, -) -> Vec { - let querier = deps.as_ref().querier; - vec![querier.query_balance(address, TEST_COIN_DENOM).unwrap()] -} diff --git a/contracts/mixnet/v2-changes.md b/contracts/mixnet/v2-changes.md new file mode 100644 index 0000000000..ecb290703c --- /dev/null +++ b/contracts/mixnet/v2-changes.md @@ -0,0 +1,903 @@ + +# Mixnet contract changes + +This file shall describe (hopefully) all relevant changes made to the contract as the result of changing reward calculation. + +## Overview + +There are two main changes performed to the mixnet contract that have a cascading effect on the rest of the system. They are as follows: + +1. The delegator rewarding is modified so that in order to determine the correct reward, we no longer have to iterate through data from all the epochs to correctly compound the reward. Instead, we assume there's a theoretical "unit" delegation on each mixnode that we keep track of and scale all the actual delegation values accordingly. It's very similar to the idea presented in the [Cosmos' F1 paper](https://drops.dagstuhl.de/opus/volltexte/2020/11974/pdf/OASIcs-Tokenomics-2019-10.pdf). I've explained the entire algorithm on an example in more details on [our gitlab](https://gitlab.nymte.ch/jstuczyn/reward-testing/-/blob/main/README.md). + +2. Mixnodes are no longer stored and indexed by their identity keys. Instead, they get assigned a unique `NodeId` (just an increasing `u64` id). This is to resolve my favourite ~~bug~~ feature (I will explain this in slightly more details in the next sections) that causes rebonded mixnode to retain its delegations. With this change the following would happen: + - new mixnode bonds, gets assigned id `X` + - delegations are being made towards this mixnode + - mixnode decides to unbond + - the same mixnode rebonds, with the same identity key, same owner, same sphinx key, etc. but this time it gets assigned new id `Y` + - as a result the delegations are still pointing to `X` and thus are no longer accumulating any rewards + +While not as major as the above changes, the other notable changes include: +- introduction of `PendingEpochEvent` and `PendingIntervalEvent`. It means that whenever a relevant request is received, it's only going to get executed once the current epoch (or interval) finishes. This might include, for example, mixnode unbonding or changing mixnode cost parameters. +- rewarding parameters, such as the size of the reward pool are only updated at the end of the current **interval**. They should not get modified between epochs. +- node uptime/performance can now be a decimal value as opposed to integer between 0-100 +- node operators can now set their operating costs +- profit margin can now be a decimal value as opposed to integer between 0-100 +- delegation/undelegation is no longer instantaneous. They will happen at the end of corresponding epoch. + +## 'Benefits' to community: +- compounding happens automatically - you no longer have to keep track of it +- node operators can now set their operating costs +- profit margin can be more granular +- operators can update their basic configuration without rebonding +- if mixnode unbonds, it loses all of its delegations (permanently) + +## Instantiation + +The `InstantiateMsg` contains more fields to remove dependency on our "beloved" `NETWORK_DEFAULTS` and constants. Now we have to explicitly specify relevant parameters during instantiation: +```rust +pub struct InstantiateMsg { + pub rewarding_validator_address: String, + pub vesting_contract_address: String, + pub rewarding_denom: String, + pub epochs_in_interval: u32, + pub epoch_duration: Duration, + pub initial_rewarding_params: InitialRewardingParams, +} +``` + +We have to explicitly state, as before, address of the rewarding validator which is authorized to update rewarded sets and distribute rewards to mixnodes, but also vesting contract address (since we need to know if we should call `Track...` methods. Without it we could end up attempt to call a vesting contract method on a non-contract address), interval/epoch related parameters (so that we wouldn't accidentally try to set 10min epochs via migration :eyes:) and initial rewarding parameters that include things such as the size of the initial reward pool or the per interval emission. + +## Mixnodes + +### Overview + +The mixnode part (apart from the delegations themselves) is the one most heavily affected by the changes made. + +- All the associated structs were either modified or completely replaced, for example our top level `MixNodeBond` has been superseded by `MixNodeDetails` that _mostly_ contains what the original `MixNodeBond` had, but also holds additional information regarding rewarding parameters. +- Furthermore, as mentioned before, the indexing works differently now. We use `NodeId` to identify nodes as opposed to `IdentityKey`. +- However, our unique indices are still in place, i.e. it's impossible to bond multiple mixnodes with the same identities. +- "unbonding" is no longer instantaneous, instead it is executed when the current epoch finishes. +- at the time of writing this, "bonding" is still instant, though it might change. +- operators can now set their interval operating costs alongside their profit margins +- profit margin and operating cost changes will only happen at the end of current interval +- operators can now change basic information about their mixnodes without having to unbond, this includes things such as the version or host information +- ... + +### Types/Models + +#### Added + +##### MixNodeDetails + +Top level struct containing all information about particular mixnode, i.e. public keys, host information, cost function, rewarding parameters, etc. + +```rust +pub struct MixNodeDetails { + pub bond_information: MixNodeBond, + + pub rewarding_details: MixNodeRewarding, +} +``` + +##### MixNodeRewarding + +New struct containing information required to determine correct rewards for all delegators and the operator. Keeps track of the currently distributed rewards as well as the value of the "unit delegation". + +It is stored in separate `Map`, as after mixnode finishes the unbonding process, if there are any delegators who haven't undelegated yet, we need to know the below information in order to determine their rewards correctly. +```rust +pub struct MixNodeRewarding { + /// Information provided by the operator that influence the cost function. + #[serde(rename = "cp")] + pub cost_params: MixNodeCostParams, + + /// Total pledge and compounded reward earned by the node operator. + #[serde(rename = "op")] + pub operator: Decimal, + + /// Total delegation and compounded reward earned by all node delegators. + #[serde(rename = "dg")] + pub delegates: Decimal, + + /// Cumulative reward earned by the "unit delegation" since the block 0. + #[serde(rename = "tur")] + pub total_unit_reward: Decimal, + + /// Value of the theoretical "unit delegation" that has delegated to this mixnode at block 0. + #[serde(rename = "ud")] + pub unit_delegation: Decimal, + + /// Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt + /// to reward it multiple times in the same epoch. + #[serde(rename = "le")] + pub last_rewarded_epoch: FullEpochId, + + // technically we don't need that field to determine reward magnitude or anything + // but it saves on extra queries to determine if we're removing the final delegation + // (so that we could zero the field correctly) + #[serde(rename = "uqd")] + pub unique_delegations: u32, +} +``` + +##### MixNodeCostParams + +Contains all cost-function related information of a given mixnode, i.e. currently the profit margin and the operating cost (per interval). It is provided by the node operator at the time of bonding and can only be changed as an interval rolls over. + +```rust +pub struct MixNodeCostParams { + pub profit_margin_percent: Percent, + + /// Operating cost of the associated mixnode per the entire interval. + pub interval_operating_cost: Coin, +} + +``` + +##### UnbondedMixnode + +This struct is used to keep track very basic information about nodes that have already unbonded, as we would only know their `NodeId`. It is especially useful if your delegation is pointing to an unbonded node and you wanted to know the owner of the node that decided to unbond or its identity key. + +```rust +pub struct UnbondedMixnode { + pub identity: IdentityKey, + pub owner: Addr, + pub unbonding_height: u64, +} +``` + +##### MixNodeConfigUpdate + +Encapsulates information sent to the contract whenever operator wants to update basic configuration information about the bonded node. + +```rust +pub struct MixNodeConfigUpdate { + pub host: String, + pub mix_port: u16, + pub verloc_port: u16, + pub http_api_port: u16, + pub version: String, +} +``` + +#### Removed + +##### StoredMixnodeBond + +The initial idea behind `StoredMixnodeBond` was to store the total delegation separately to the operator pledge (and accumulated rewards). The same issue is now resolved with the `MixNodeBond` and `MixNodeRewarding`. + +#### Modified + +##### MixNodeBond + +```rust +// operator information + data assigned by the contract(s) +pub struct MixNodeBond { + // + // ... + // + // `id` field has been added that contains information about the assigned `NodeId` + // +++ pub id: NodeId, + // + // we no longer keep total pledge (alongside delegation) on `MixNodeBond` via the `pledge_amount`, instead we only hold the `original_pledge` which is **NEVER** modified + // --- pub pledge_amount: Coin + // +++ pub original_pledge: Coin, + // + // `block_height` has been renamed to `bonding_height` to be more explicit about the intent + // --- pub block_height: u64 + // +++ pub bonding_height: u64, + // + // `is_unbonding` field has been added to indicate when the mixnode has issue the request to unbond but the epoch hasn't rolled over yet (to prevent delegations on an unbonding node) + // +++ pub is_unbonding: bool, + // + // `accumulated_rewards` field is no longer required to keep track of all rewards for particular node + // --- pub accumulated_rewards: Option +} +``` + +##### MixNode + +```rust +// information provided by the operator +pub struct MixNode { + // + // ... + // + // `profit_margin_percent` has been removed and this information is now provided via `MixNodeCostParams` + // --- pub profit_margin_percent: u8, + // +++ +} +``` + +##### Layer + +```rust +pub enum Layer { + // `Gateway` layer has been removed from the `Layer` enum + //--- Gateway = 0, + //+++ + One = 1, + Two = 2, + Three = 3, +} +``` + +### Transactions + +As before, all transactions have their associated `OnBehalf` equivalent that allows them to be called from the vesting contract. + +#### Added + +##### UpdateMixnodeCostParams + +This one allows you to update the cost parameters of your mixnode, i.e. the profit margin and the interval operating costs. Execution of this transaction will result in the creation of a `PendingIntervalEvent` that will get resolved at the end of the current interval. + +#### Removed + +##### CheckpointMixnodes + +Due to the changes to the rewarding system, we no longer have to be checkpointing mixnodes every epoch in order to keep track of their stake/parameters at those blocks. + +#### Modified + +##### UpdateMixnodeConfig + +Updating mixnode config allowed you to update your profit margin. This functionality has been replaced with `UpdateMixnodeCostParams` and instead `UpdateMixnodeConfig` lets you to instantaneously update basic configuration such as the host information or the node version. + +##### UnbondMixnode + +In general sense `UnbondMixnode` works as before, i.e. it will eventually result in the mixnode getting removed from the directory. However, it's no longer instant. Whenever the transaction is executed, it will instead push a `PendingEpochEvent` that shall get resolved at the end of the current epoch. + +The only immediate effect is that `is_unbonding` field on the `MixNodeBond` is going to be set to `true` and as a result, no new delegations are going to be permitted on this node. + +##### BondMixnode + +The only difference made to the mixnode bonding process is that operators need to provide an additional argument, of type `MixNodeCostParams`, to specify the cost function arguments of the node. + +### Queries + +The most relevant thing here to note is that whenever old queries were using `mix_identity` as one of their arguments, they instead use `mix_id` of type `NodeId` (`u64`). + +#### Added + +##### GetUnbondedMixNodes + +New query allowing to grab the details of all mixnodes (paged) that have unbonded at some point in the past. + +##### GetUnbondedMixNodeInformation + +Same as above, but rather than getting information for all the mixnodes, it does it for the node specified by the provided `mix_id`. + +##### GetStakeSaturation + +Allows to directly obtain stake saturation (i.e. of the full bond (pledge + delegations)) of given mixnode. + +##### GetMixnodeRewardingDetails + +Allows obtaining `MixNodeRewarding` details of a particular node that, among other things, contain total delegation towards this node or the current value of the "unit" delegation. + +#### Removed + +Everything that was related to node snapshotting is removed, this includes the below to queries: + +##### GetMixnodeAtHeight + +No longer needed due to no snapshotting. + +##### GetCheckpointsForMixnode + +No longer needed due to no snapshotting. + +##### GetCurrentOperatorCost + +In the previous version the operator cost was a constant value shared by all operators. Now it is configurable and it can be queried by getting information associated via the particular node, for example with `GetMixnodeRewardingDetails` or `GetMixnodeDetails`. + +#### Modified + +##### GetMixNodes => GetMixNodeBonds, GetMixNodesDetailed + +Since the structure of `MixNodeBond` has changed, `GetMixNodes` is replaced by `GetMixNodeBonds` that returns all `MixNodeBond` (paged) while `GetMixNodesDetailed` returns `MixNodeDetails`, that apart from the bond also contains rewarding details. + +##### GetMixnodeBond + +Similarly to the above query for the bond information of given mixnode has been superseded by `GetMixnodeDetails`. + +##### OwnsMixnode + +`OwnsMixnode` has been renamed to `GetOwnedMixnode` to keep the naming consistent. + +### Storage + +#### Added + +- `MIXNODE_ID_COUNTER` - as mentioned before all mixnodes are indexed by an increasing `NodeId`. This counter keep track of the current value. +- `UNBONDED_MIXNODES` - `Map`storing basic information about the mixnodes that have unbonded. + +#### Removed + +- memoized `TOTAL_DELEGATION` was removed. Similar functionality is achieved via `rewards_storage::MIXNODE_REWARDING`. +- `LAST_PM_UPDATE_TIME` is also removed. We no longer have to keep track of that since the profit margin updates are enforced to be happening at the end of intervals. + +#### Modified + +- As mentioned multiple times before, mixnodes are no longer snapshot and we index them with `NodeId` and thus instead of using `IndexedSnapshotMap<'a, IdentityKeyRef<'a>, StoredMixnodeBond, MixnodeBondIndex<'a>>` we use `IndexedMap<'a, NodeId, MixNodeBond, MixnodeBondIndex<'a>>` +- To preserve uniqueness on identity keys, `MixnodeBondIndex` now also contains `UniqueIndex` on the `identity_key`. + +## Gateways + +### Overview + +Gateways remain mostly unchanged and unaffected by the fallout of other changes. We still keep the gateways indexed by their identity keys. This might change in the future, but for the time being, there's no reason to do anything about it. + +The only relevant change is that `OwnsGateway` query has been renamed to `GetOwnedGateway` to keep the naming consistent. + +## Delegation + +### Overview + +Apart from mixnodes, the delegations are the other part of the system most affected by the introduced changes. Structurally-wise, the differences are relatively minimal, but the logic behind them, especially concerning rewarding and reward estimation (which will be described in more details in the subsequent sections) has changed in a meaningful way. + +### Types/Models + +#### Added + +N/A + +#### Removed + +N/A + +#### Modified + +##### Delegation + +```rust +pub struct Delegation { + // + // ... + // + // `node_identity` of the associated mixnode has been replaced by its assigned `node_id` as mixnode indexing has been modified + // --- pub node_identity: IdentityKey, + // +++ pub node_id: NodeId, + + // Value of the "unit delegation" associated with the mixnode at the time of delegation. It's used purely for calculating rewards + // +++ pub cumulative_reward_ratio: Decimal, + + // `block_height` has been renamed to `height` + // --- pub block_height: u64, + // +++ pub height: u64, +} +``` + +### Transactions + +Apart from the changes to the mixnode indexing (i.e. `identity_key => node_id`) there's been no significant changes to the transactions involving delegations. Of course this excludes anything regarding rewards, but this part is going to have its own dedicated section below. + +### Queries + +The same holds true for queries. If we exclude reward estimation-related queries and changes due to the new indexing, there are hardly any changes. The only notable difference is the introduction of `GetAllDelegations` which allows one to query for all delegations in the system as opposed to being restricted to a single owner or a single mixnode. The responses are still, however, paged. + +### Storage + +#### Added + +N/A + +#### Removed + +- `PENDING_DELEGATION_EVENTS` `Map` has been removed as this concept is being superseded by the `PendingEpochEvent` queue. + +#### Modified + +The storage key structure of delegation has been slightly adjusted compared to the previous version: +- The composite storage key no longer includes the block height as we're now able to immediately work with the potentially changed values, +- For the simplicity sake, the composite subkey created for the purpose of querying by the owner/proxy combination has been changed from being a `Vec` to instead being a base58-encoded String (of the same data). This makes it slightly easier for the clients to use it, especially in paged queries. + +## Interval + +### Overview + +Generally we've been going back and forth with having explicit distinction between epochs and intervals and making this purely implicit. In this iteration of the contract both pieces of data are explicit. `Interval` has an associated id, etc. as well as it holds information about the current epoch, number of epochs in interval, etc. + +The other notable change to how interval behaves is that we expanded the concept of particular events being executed as given epoch (or interval) rolls over. Previously this was only applicable to `PendingDelegations`. + +Also, now advancing epoch happens in the same message as writing the new rewarded set, so it's impossible to perform one without the other. Speaking of updating the rewarded set, I was attempting to be smart and reduce number of storage read by not writing entries that hasn't changed (i.e. if node was `Active` and its updated status is still `Active`, don't do anything). We're about to see if this wasn't a stupid overkill... + +### Types/Models + +#### Added + +##### PendingEpochEvent + +New structure keeping track of events that shall get invoked at the end of the current **epoch** (after rewards have already been distributed). + +```rust +pub enum PendingEpochEvent { + Delegate { + owner: Addr, + mix_id: NodeId, + amount: Coin, + proxy: Option, + }, + Undelegate { + owner: Addr, + mix_id: NodeId, + proxy: Option, + }, + UnbondMixnode { + mix_id: NodeId, + }, + UpdateActiveSetSize { + new_size: u32, + }, +} +``` + +##### PendingIntervalEvent + +New structure keeping track of events that shall get invoked at the end of the current **interval** (after rewards have already been distributed). + +```rust +pub enum PendingIntervalEvent { + ChangeMixCostParams { + mix: NodeId, + new_costs: MixNodeCostParams, + }, + UpdateRewardingParams { + update: IntervalRewardingParamsUpdate, + }, + UpdateIntervalConfig { + epochs_in_interval: u32, + epoch_duration_secs: u64, + }, +} +``` + +#### Removed + +N/A + +#### Modified + +##### Interval + +```rust +pub struct Interval { + // + // ... + // + // we're now explicitly keeping track of the expected number of epochs in the stored interval (as opposed to making it implicit via constants) + // +++ epochs_in_interval: u32, + // + // we're just being explicit about what we're keeping track of + // --- start: OffsetDateTime, + // +++ current_epoch_start: OffsetDateTime, + // + // the same is true for this one + // --- length: Duration, + // +++ epoch_length: Duration, + // + // and we're also explicitly separating ids of interval and epochs. Do note that it's illegal for `current_epoch_id` to be equal or larger to `epochs_in_interval` (in that case it should roll over back to 0) + // --- id: u32, + // +++ id: IntervalId, + // +++ current_epoch_id: EpochId, +} +``` + +### Transactions + +#### Added + +There's a couple of newly added transaction that allow changing rewarding-related parameters, such as the active set size or pool emission, etc. But those changes should preferably only be executed at the end of the current epoch/interval (depending on a particular change requested) so that the current rewarding interval wouldn't be affected in an unexpected way. However, the transactions include the `force_immediately` field to make the change immediate if required. + +##### UpdateActiveSetSize + +Allows updating the active set size. Note that the new size **must** be equal to or smaller than the current rewarded set. If `force_immediately` is not set, the change will be applied at the end of the current epoch. + +##### UpdateRewardingParams + +Allows updating (almost) all the other rewarding-related global parameters: + +```rust +pub struct IntervalRewardingParamsUpdate { + pub reward_pool: Option, + pub staking_supply: Option, + + pub sybil_resistance_percent: Option, + pub active_set_work_factor: Option, + pub interval_pool_emission: Option, + pub rewarded_set_size: Option, +} +``` + +Note that at least a single change must be specified. If `force_immediately` is not set, the change will be applied at the end of the current interval. + +##### UpdateIntervalConfig + +Allows adjusting configuration of the interval, i.e. number of epochs it contains as well as the duration of the epochs themselves. Similarly to the above, if `force_immediately` is not set, the change will be applied at the end of the current interval. + +##### ReconcileEpochEvents + +Serves a very similar purpose to the removed `ReconcileDelegations`. But rather than being limited to just delegation creation/removal, this transaction would attempt to execute all pending epoch and interval events. + +Do note that if current epoch is in **NOT** in progress, nothing is going to happen. Furthermore, interval events will only get executed if apart from the current epoch being over, the interval itself is over. + +Anyone willing to pay the associated gas costs is can call this transaction. It's not limited to the `owner` account. + +#### Removed + +##### InitEpoch + +Since we're going to be creating a brand-new contract, we no longer have to separately initialise the epoch (interval). It's going to be performed implicitly during contract instantiation. + +##### ReconcileDelegations + +As explained before, superseded by `ReconcileEpochEvents`. + +##### CheckpointMixnodes + +As explained multiple times before, due to the changes to the reward calculation algorithm, we no longer have to keep track of the state of all the mixnodes at each epoch. + +##### WriteRewardedSet + +This functionality has been moved into `AdvanceCurrentEpoch` so that it would not be possible to roll over the epoch without explicitly updating the rewarded set. + +##### GetRewardedSetUpdateDetails + +Rewarded set is now always being updated whenever epoch rolls over + +##### GetRewardedSetRefreshBlocks + +Same as above + +##### GetCurrentRewardedSetHeight + +We no longer keep track of rewarded sets at given height thanks to the change to the reward calculation + +#### Modified + +##### AdvanceCurrentEpoch + +The main idea behind this transaction remains unchanged - if the current epoch/interval is over, this call rolls it over to the next one. However, it is now also responsible for additional functionalities: +- updating the rewarded set to the newly provided value, +- emptying the `PendingEpochEvent` and `PendingIntervalEvent` queues if there's anything left in there -> Do note that a separate explicit call in a different transaction is preferred, since there might be a significant amount of events to go through, +- if the interval has rolled over all the pending reward pool changes from `RewardPoolChange` (that will be elaborated in the rewards section) are applied + +### Queries + +#### Added + +##### GetCurrentIntervalDetails + +Allows querying for the information about the current `Interval` alongside data on the current blocktime and whether the current epoch and interval are already over. + +##### GetPendingEpochEvents + +Paged query for obtaining all currently pending `PendingEpochEvents` that shall get cleared at the end of the current epoch. + +##### GetPendingIntervalEvents + +Paged query for obtaining all currently pending `PendingIntervalEvents` that shall get cleared at the end of the current interval. + +#### Removed + +##### GetEpochsInInterval + +This was removed in favour of `GetCurrentIntervalDetails` that returns the same piece of data on top of additional content. + +#### Modified + +##### GetRewardedSet + +Querying for the rewarded set no longer lets you specify the block height. It always grabs the current one. + +### Storage + +#### Added + +Similarly to `MIXNODE_ID_COUNTER`, we keep an increasing id for epoch and interval events. Essentially we want to ensure that we'd execute them in the order they were created and we can't use block height as it's very possible multiple requests might be created in the same block height. Thus we introduce `EPOCH_EVENT_ID_COUNTER` and `INTERVAL_EVENT_ID_COUNTER` for that purpose. + +Furthermore, we keep track of the ID of the most recently executed event (in both categories), so we'd known more easily if there have been any new ones pushed without having to explicitly query for them. For that end we use `LAST_PROCESSED_EPOCH_EVENT` and `LAST_PROCESSED_INTERVAL_EVENT` + +Finally, rather obviously, we have to store the actual events and those are being help in `PENDING_EPOCH_EVENTS` and `PENDING_INTERVAL_EVENTS` `Map`s. + +#### Removed + +- `CURRENT_EPOCH_REWARD_PARAMS` - in a way superseded by `rewards_storage::REWARDING_PARAMS` that holds current rewarding parameters for the entire interval. +- `CURRENT_REWARDED_SET_HEIGHT` - we only hold a single rewarded set at a time +- `EPOCHS` - all epoch related information is included in the `Interval` data. + +#### Modified + +- `CURRENT_EPOCH` has been replaced by a differently named `CURRENT_INTERVAL` +- `REWARDED_SET` - the storage key no longer requires using the block height as there's only ever a single rewarded set at a time + +## Contract settings + +### Overview + +Contract state/settings now explicitly contain information that previously was implicit via the constants or network defaults (such as the `denom`). Also, parameters affecting rewarding, such as the active set size, were moved to more appropriate modules. + +### Types/Models + +#### Added + +N/A + +#### Removed + +N/A + +#### Modified + +##### ContractState + +```rust +pub struct ContractState { + // + // ... + // + // we're explicitly keeping track of what we think is the vesting contract (as specified during instantiation), so that we'd known if we should call `Track...` methods on the proxy address + // +++ pub vesting_contract_address: Addr, + // added information about the expected coin denomination that's used for rewarding + // +++ pub rewarding_denom: String, +} +``` + +##### ContractStateParams + +```rust +pub struct ContractStateParams { + // + // ... + // + // we're tracking all minimum pledges explicitly as `Coin` now + // --- pub minimum_mixnode_pledge: Uint128 + // +++ pub minimum_mixnode_pledge: Coin, + // + // --- pub minimum_gateway_pledge: Uint128, + // +++ pub minimum_gateway_pledge: Coin, + // + // optional functionality to set minimum delegation amount if required + // +++ pub minimum_mixnode_delegation: Option, + // + // Attributes directly affecting rewarding are moved to `rewards_storage` now + // --- pub mixnode_rewarded_set_size: u32, + // --- pub mixnode_active_set_size: u32, + // --- pub staking_supply: Uint128, +} +``` + +### Transactions + +The only transaction, i.e. updating state params, is no longer a unit enum. It was changed from +```rust +pub enum ExecuteMsg { + UpdateContractStateParams(ContractStateParams), +} +``` + +to +```rust +pub enum ExecuteMsg { + UpdateContractStateParams { + updated_parameters: ContractStateParams, + }, +} +``` + +### Queries + +#### Added + +##### GetState + +Introduced new query to get the entire `ContractState` struct, so we'd known about, for example, the rewarding denom or the rewarding validator address. + +#### Removed + +N/A + +#### Modified + +##### StateParams + +Was renamed to `GetStateParams` to keep naming consistent + +### Storage + +No relevant changes were performed to the storage structure of the contract settings. + +## Rewards + +### Overview + +Changing the logic behind the rewards was the main motivation behind this new contract version. The main things in this section, apart from what was already mentioned before, include but is not limited to: +- reward pool (and the staking supply) being only updated at the end of the given interval. However, the accounting is still happening as the rewards are distributed, so we'd known how much the pool should be adjusted by. +- we no longer keep any historical information regarding past epochs/parameters/etc for the purposes of rewarding. Whatever exists in the storage at the time is the thing that's going to be used for the next distribution. +- queries for reward estimation now require constant(ish) amount of gas as opposed to growing linearly with the number of epochs since last claim/compounding. +- + +### Types/Models + +#### Added + +##### MixNodeRewarding + +The most important struct created for the purposes of the changes described. All the data here allows us to correctly determine rewards for all the delegators by scaling the value of `total_unit_reward` based on the ratio the delegation to `unit_delegation` and scaled by the unit delegation reward at the time of delegation of the delegate. + +```rust +pub struct MixNodeRewarding { + /// Information provided by the operator that influence the cost function. + pub cost_params: MixNodeCostParams, + + /// Total pledge and compounded reward earned by the node operator. + pub operator: Decimal, + + /// Total delegation and compounded reward earned by all node delegators. + pub delegates: Decimal, + + /// Cumulative reward earned by the "unit delegation" since the block 0. + pub total_unit_reward: Decimal, + + /// Value of the theoretical "unit delegation" that has delegated to this mixnode at block 0. + pub unit_delegation: Decimal, + + /// Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt + /// to reward it multiple times in the same epoch. + pub last_rewarded_epoch: FullEpochId, + + // technically we don't need that field to determine reward magnitude or anything + // but it saves on extra queries to determine if we're removing the final delegation + // (so that we could zero the field correctly) + pub unique_delegations: u32, +} +``` + +##### RewardPoolChange + +Whenever we distribute rewards, we keep track of how much should get removed from the reward pool and moved into the staking supply when the interval finishes. + +```rust +pub(crate) struct RewardPoolChange { + /// Indicates amount that shall get moved from the reward pool to the staking supply + /// upon the current interval finishing. + pub removed: Decimal, + + // this will be used once coconut credentials are in use; + /// Indicates amount that shall get added to the both reward pool and not touch the staking supply + /// upon the current interval finishing. + #[allow(unused)] + pub added: Decimal, +} +``` + +##### RewardingParams and IntervalRewardParams + +Those are used for keeping track of parameters used for rewarding of all nodes during a particular interval. Unless there's an exceptionally good reason for it, they remain constants within an interval. + +```rust +pub struct RewardingParams { + /// Parameters that should remain unchanged throughout an interval. + pub interval: IntervalRewardParams, + + // while the active set size can change between epochs to accommodate for bandwidth demands, + // the active set size should be unchanged between epochs and should only be adjusted between + // intervals. However, it makes more sense to keep both of those values together as they're + // very strongly related to each other. + pub rewarded_set_size: u32, + pub active_set_size: u32, +} + +pub struct IntervalRewardParams { + /// Current value of the rewarding pool. + /// It is expected to be constant throughout the interval. + pub reward_pool: Decimal, + + /// Current value of the staking supply. + /// It is expected to be constant throughout the interval. + pub staking_supply: Decimal, + + // computed values + /// Current value of the computed reward budget per epoch, per node. + /// It is expected to be constant throughout the interval. + pub epoch_reward_budget: Decimal, + + /// Current value of the stake saturation point. + /// It is expected to be constant throughout the interval. + pub stake_saturation_point: Decimal, + + // constants(-ish) + // default: 30% + /// Current value of the sybil resistance percent (`alpha`). + /// It is not really expected to be changing very often. + /// As a matter of fact, unless there's a very specific reason, it should remain constant. + pub sybil_resistance: Percent, + + // default: 10 + /// Current active set work factor. + /// It is not really expected to be changing very often. + /// As a matter of fact, unless there's a very specific reason, it should remain constant. + pub active_set_work_factor: Decimal, + + // default: 2% + /// Current maximum interval pool emission. + /// Assuming all nodes in the rewarded set are fully saturated and have 100% performance, + /// this % of the reward pool would get distributed in rewards to all operators and its delegators. + /// It is not really expected to be changing very often. + /// As a matter of fact, unless there's a very specific reason, it should remain constant. + pub interval_pool_emission: Percent, +} +``` + +#### Removed + +##### DelegatorRewardParams, StoredNodeRewardResult, EpochRewardParams, NodeRewardResult + +All reward-related results and parameters got consolidated, mostly into `RewardingParams` + +### Transactions + +#### Added + +#### Removed + +##### CompoundOperatorReward, CompoundDelegatorReward, CompoundOperatorRewardOnBehalf, CompoundDelegatorRewardOnBehalf + +All compounding-related transactions have been removed as they're no longer required since the compounding is happening automatically now. + +#### Modified + +##### RewardMixnode + +As with previous changes, we're now rewarding given mixnode by its `NodeId` as opposed to `IdentityKey`. Furthermore, we no longer have to pass entire set of `NodeRewardParams`. Everything is implicit from the contract state, with the single exception of node `Performance`, which is now required. However, the strong typing ensures its always in the correct range. + +##### ClaimOperator/DelegatorReward + +All claim-related operations have been renamed to `Withdraw` for consistency with cosmos-sdk. It essentially zeroes your reward and moves this amount to your account address. + +### Queries + +#### Added + +##### GetPendingMixNodeOperatorReward + +Added variant of obtaining pending operator reward by the id of the bonded mixnode. + +#### Removed + +##### GetRewardPool, GetCirculatingSupply, GetStakingSupply, GetIntervalRewardPercent, GetSybilResistancePercent, GetActiveSetWorkFactor + +All the queries regarding individual rewarding parameters have been consolidated into a single `GetRewardingParams` + +#### Modified + +##### QueryOperatorReward + +Renamed to `GetPendingOperatorReward` for consistency' sake. + +##### QueryDelegatorReward + +Renamed to `GetPendingDelegatorReward` for consistency' sake. + +### Storage + +As with everything in this module, storage was also completely revamped. The changes here mostly follow on the changes to data structs. + +#### Added + +- `REWARDING_PARAMS` - all the rewarding parameters are consolidated in a single `Item` +- `PENDING_REWARD_POOL_CHANGE` - keeping track of the reward pool changes that shall get applied at the end of the interval +- `MIXNODE_REWARDING` - per mixnode, indexed by `NodeId`, parameters required to determine operator and delegates rewards + +#### Removed + +- `REWARD_POOL` - incorporated into `REWARDING_PARAMS` +- `REWARDING_STATUS` - it was already deprecated to begin with since we're no longer explicitly rewarding delegators, +- `DELEGATOR_REWARD_CLAIMED_HEIGHT`, `OPERATOR_REWARD_CLAIMED_HEIGHT` - due to auto-compounding, we don't have to keep track of heights of reward claiming +- `EPOCH_REWARD_PARAMS` - we no longer have to retroactively determine rewards for past epochs and thus we no longer have to keep track of rewarding params for past epochs + +## Final remarks + +As mentioned during multiple chats, I think the migration the rest of our codebase is going to be a huge undertaking mostly because of how many aspects of the system this change is affecting. From the top of my head, we'd need to definitely change our `nymd client` (and as a result `validator-api`, `clients`, etc.) and also the vesting contract. + +With the latter case (and with the current mixnet contract), it's going to be even trickier given that the current contract is already live. We will need to adjust how the values are stored, i.e. mixnodes are now indexed by `NodeId` as opposed to `IdentityKey`. My recommendation would be to create a migration such that it would "cancel" / "return" (you name it) all existing delegations and bonds so that the users would have to make new ones under the new contract. diff --git a/contracts/vesting/Cargo.toml b/contracts/vesting/Cargo.toml index 4ffc353a6a..3c03d92c91 100644 --- a/contracts/vesting/Cargo.toml +++ b/contracts/vesting/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vesting-contract" -version = "1.0.2" +version = "1.1.0" authors = ["Drazen Urch "] edition = "2021" diff --git a/contracts/vesting/src/contract.rs b/contracts/vesting/src/contract.rs index 236ff12b65..923c403733 100644 --- a/contracts/vesting/src/contract.rs +++ b/contracts/vesting/src/contract.rs @@ -1,4 +1,5 @@ use crate::errors::ContractError; +use crate::queued_migrations::migrate_to_v2_mixnet_contract; use crate::storage::{ account_from_address, locked_pledge_cap, update_locked_pledge_cap, BlockTimestampSecs, ADMIN, DELEGATIONS, MIXNET_CONTRACT_ADDRESS, MIX_DENOM, @@ -12,7 +13,8 @@ use cosmwasm_std::{ QueryResponse, Response, StdResult, Timestamp, Uint128, }; use cw_storage_plus::Bound; -use mixnet_contract_common::{Gateway, IdentityKey, MixNode}; +use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; +use mixnet_contract_common::{Gateway, MixNode, NodeId}; use vesting_contract_common::events::{ new_ownership_transfer_event, new_periodic_vesting_account_event, new_staking_address_update_event, new_track_gateway_unbond_event, @@ -45,8 +47,8 @@ pub fn instantiate( } #[entry_point] -pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result { - Ok(Response::default()) +pub fn migrate(deps: DepsMut<'_>, _env: Env, msg: MigrateMsg) -> Result { + migrate_to_v2_mixnet_contract(deps, msg) } #[entry_point] @@ -64,25 +66,23 @@ pub fn execute( try_track_reward(deps, info, amount, &address) } ExecuteMsg::ClaimOperatorReward {} => try_claim_operator_reward(deps, info), - ExecuteMsg::ClaimDelegatorReward { mix_identity } => { - try_claim_delegator_reward(deps, info, mix_identity) + ExecuteMsg::ClaimDelegatorReward { mix_id } => { + try_claim_delegator_reward(deps, info, mix_id) } - ExecuteMsg::CompoundDelegatorReward { mix_identity } => { - try_compound_delegator_reward(mix_identity, info, deps) + 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::CompoundOperatorReward {} => try_compound_operator_reward(info, deps), - ExecuteMsg::UpdateMixnodeConfig { - profit_margin_percent, - } => try_update_mixnode_config(profit_margin_percent, info, deps), ExecuteMsg::UpdateMixnetAddress { address } => { try_update_mixnet_address(address, info, deps) } - ExecuteMsg::DelegateToMixnode { - mix_identity, - amount, - } => try_delegate_to_mixnode(mix_identity, amount, info, env, deps), - ExecuteMsg::UndelegateFromMixnode { mix_identity } => { - try_undelegate_from_mixnode(mix_identity, info, deps) + ExecuteMsg::DelegateToMixnode { mix_id, amount } => { + try_delegate_to_mixnode(mix_id, amount, info, env, deps) + } + ExecuteMsg::UndelegateFromMixnode { mix_id } => { + try_undelegate_from_mixnode(mix_id, info, deps) } ExecuteMsg::CreateAccount { owner_address, @@ -101,14 +101,23 @@ pub fn execute( } ExecuteMsg::TrackUndelegation { owner, - mix_identity, + mix_id, amount, - } => try_track_undelegation(&owner, mix_identity, amount, info, deps), + } => try_track_undelegation(&owner, mix_id, amount, info, deps), ExecuteMsg::BondMixnode { mix_node, + cost_params, owner_signature, amount, - } => try_bond_mixnode(mix_node, owner_signature, amount, info, env, deps), + } => try_bond_mixnode( + mix_node, + cost_params, + owner_signature, + amount, + info, + env, + deps, + ), ExecuteMsg::UnbondMixnode {} => try_unbond_mixnode(info, deps), ExecuteMsg::TrackUnbondMixnode { owner, amount } => { try_track_unbond_mixnode(&owner, amount, info, deps) @@ -148,12 +157,21 @@ pub fn try_update_locked_pledge_cap( /// Update config for a mixnode bonded with vesting account, sends [mixnet_contract_common::ExecuteMsg::UpdateMixnodeConfig] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. pub fn try_update_mixnode_config( - profit_margin_percent: u8, + new_config: MixNodeConfigUpdate, info: MessageInfo, deps: DepsMut, ) -> Result { let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - account.try_update_mixnode_config(profit_margin_percent, deps.storage) + account.try_update_mixnode_config(new_config, deps.storage) +} + +pub fn try_update_mixnode_cost_params( + new_costs: MixNodeCostParams, + info: MessageInfo, + deps: DepsMut, +) -> Result { + let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; + account.try_update_mixnode_cost_params(new_costs, deps.storage) } /// Updates mixnet contract address, for cases when a new mixnet contract is deployed. @@ -283,18 +301,10 @@ pub fn try_track_unbond_gateway( Ok(Response::new().add_event(new_track_gateway_unbond_event())) } -/// Compound operator reward, sends [mixnet_contract_common::ExecuteMsg::CompoundOperatorRewardOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS], adds available rewards to the existing bond -pub fn try_compound_operator_reward( - info: MessageInfo, - deps: DepsMut<'_>, -) -> Result { - let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - account.try_compound_operator_reward(deps.storage) -} - /// Bond a mixnode, sends [mixnet_contract_common::ExecuteMsg::BondMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. pub fn try_bond_mixnode( mix_node: MixNode, + cost_params: MixNodeCostParams, owner_signature: String, amount: Coin, info: MessageInfo, @@ -304,7 +314,14 @@ pub fn try_bond_mixnode( let mix_denom = MIX_DENOM.load(deps.storage)?; let pledge = validate_funds(&[amount], mix_denom)?; let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - account.try_bond_mixnode(mix_node, owner_signature, pledge, &env, deps.storage) + account.try_bond_mixnode( + mix_node, + cost_params, + owner_signature, + pledge, + &env, + deps.storage, + ) } /// Unbond a mixnode, sends [mixnet_contract_common::ExecuteMsg::UnbondMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. @@ -346,7 +363,7 @@ fn try_track_reward( /// Track undelegation, invoked by the mixnet contract after sucessful undelegation, message contains coins returned with any accrued rewards. fn try_track_undelegation( address: &str, - mix_identity: IdentityKey, + mix_id: NodeId, amount: Coin, info: MessageInfo, deps: DepsMut<'_>, @@ -355,13 +372,14 @@ fn try_track_undelegation( return Err(ContractError::NotMixnetContract(info.sender)); } let account = account_from_address(address, deps.storage, deps.api)?; - account.track_undelegation(mix_identity, amount, deps.storage)?; + + account.track_undelegation(mix_id, amount, deps.storage)?; Ok(Response::new().add_event(new_track_undelegation_event())) } /// Delegate to mixnode, sends [mixnet_contract_common::ExecuteMsg::DelegateToMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS].. fn try_delegate_to_mixnode( - mix_identity: IdentityKey, + mix_id: NodeId, amount: Coin, info: MessageInfo, env: Env, @@ -370,17 +388,8 @@ fn try_delegate_to_mixnode( let mix_denom = MIX_DENOM.load(deps.storage)?; let amount = validate_funds(&[amount], mix_denom)?; let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - account.try_delegate_to_mixnode(mix_identity, amount, &env, deps.storage) -} -/// Compounds deleagtor reward, ie adds it to the existing delegations for a node, sends [mixnet_contract_common::ExecuteMsg::CompoundDelegatorRewardOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. -fn try_compound_delegator_reward( - mix_identity: IdentityKey, - info: MessageInfo, - deps: DepsMut<'_>, -) -> Result { - let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - account.try_compound_delegator_reward(mix_identity, deps.storage) + account.try_delegate_to_mixnode(mix_id, amount, &env, deps.storage) } /// Claims operator reward, sends [mixnet_contract_common::ExecuteMsg::ClaimOperatorRewardOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. @@ -396,20 +405,22 @@ fn try_claim_operator_reward( fn try_claim_delegator_reward( deps: DepsMut<'_>, info: MessageInfo, - mix_identity: String, + mix_id: NodeId, ) -> Result { let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - account.try_claim_delegator_reward(mix_identity, deps.storage) + + account.try_claim_delegator_reward(mix_id, deps.storage) } /// Undelegates from a mixnode, sends [mixnet_contract_common::ExecuteMsg::UndelegateFromMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. fn try_undelegate_from_mixnode( - mix_identity: IdentityKey, + mix_id: NodeId, info: MessageInfo, deps: DepsMut<'_>, ) -> Result { let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - account.try_undelegate_from_mixnode(mix_identity, deps.storage) + + account.try_undelegate_from_mixnode(mix_id, deps.storage) } /// Creates a new periodic vesting account, and deposits funds to vest into the contract. @@ -545,10 +556,9 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result { to_binary(&try_get_current_vesting_period(&address, deps, env)?) } - QueryMsg::GetDelegationTimes { - address, - mix_identity, - } => to_binary(&try_get_delegation_times(deps, &address, mix_identity)?), + QueryMsg::GetDelegationTimes { address, mix_id } => { + to_binary(&try_get_delegation_times(deps, &address, mix_id)?) + } QueryMsg::GetAllDelegations { start_after, limit } => { to_binary(&try_get_all_delegations(deps, start_after, limit)?) } @@ -685,27 +695,27 @@ pub fn try_get_delegated_vesting( pub fn try_get_delegation_times( deps: Deps<'_>, vesting_account_address: &str, - mix_identity: String, + mix_id: NodeId, ) -> Result { let owner = deps.api.addr_validate(vesting_account_address)?; let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; let delegation_timestamps = DELEGATIONS - .prefix((account.storage_key(), mix_identity.clone())) + .prefix((account.storage_key(), mix_id)) .keys(deps.storage, None, None, Order::Ascending) .collect::>>()?; Ok(DelegationTimesResponse { owner, account_id: account.storage_key(), - mix_identity, + mix_id, delegation_timestamps, }) } pub fn try_get_all_delegations( deps: Deps<'_>, - start_after: Option<(u32, IdentityKey, BlockTimestampSecs)>, + start_after: Option<(u32, NodeId, BlockTimestampSecs)>, limit: Option, ) -> Result { let limit = limit.unwrap_or(100).min(200) as usize; @@ -715,9 +725,9 @@ pub fn try_get_all_delegations( .range(deps.storage, start, None, Order::Ascending) .map(|kv| { kv.map( - |((account_id, mix_identity, block_timestamp), amount)| VestingDelegation { + |((account_id, mix_id, block_timestamp), amount)| VestingDelegation { account_id, - mix_identity, + mix_id, block_timestamp, amount, }, diff --git a/contracts/vesting/src/errors.rs b/contracts/vesting/src/errors.rs index 867620ff8f..48dd152c24 100644 --- a/contracts/vesting/src/errors.rs +++ b/contracts/vesting/src/errors.rs @@ -1,5 +1,5 @@ use cosmwasm_std::{Addr, StdError, Uint128}; -use mixnet_contract_common::IdentityKey; +use mixnet_contract_common::NodeId; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -31,7 +31,7 @@ pub enum ContractError { #[error("VESTING ({}): Received multiple denoms, expected 1", line!())] MultipleDenoms, #[error("VESTING ({}): No delegations found for account {0}, mix_identity {1}", line!())] - NoSuchDelegation(Addr, IdentityKey), + NoSuchDelegation(Addr, NodeId), #[error("VESTING ({}): Only mixnet contract can perform this operation, got {0}", line!())] NotMixnetContract(Addr), #[error("VESTING ({}): Calculation underflowed", line!())] diff --git a/contracts/vesting/src/queued_migrations.rs b/contracts/vesting/src/queued_migrations.rs index 87d5c392c8..9bb44331a1 100644 --- a/contracts/vesting/src/queued_migrations.rs +++ b/contracts/vesting/src/queued_migrations.rs @@ -1,2 +1,15 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 + +use crate::errors::ContractError; +use crate::storage::MIXNET_CONTRACT_ADDRESS; +use cosmwasm_std::{DepsMut, Response}; +use vesting_contract_common::MigrateMsg; + +pub fn migrate_to_v2_mixnet_contract( + deps: DepsMut<'_>, + msg: MigrateMsg, +) -> Result { + MIXNET_CONTRACT_ADDRESS.save(deps.storage, &msg.v2_mixnet_contract_address)?; + Ok(Response::new()) +} diff --git a/contracts/vesting/src/storage.rs b/contracts/vesting/src/storage.rs index c753e57c37..e42ee5621c 100644 --- a/contracts/vesting/src/storage.rs +++ b/contracts/vesting/src/storage.rs @@ -2,7 +2,7 @@ use crate::vesting::Account; use crate::{contract::INITIAL_LOCKED_PLEDGE_CAP, errors::ContractError}; use cosmwasm_std::{Addr, Api, Storage, Uint128}; use cw_storage_plus::{Item, Map}; -use mixnet_contract_common::IdentityKey; +use mixnet_contract_common::{IdentityKey, NodeId}; use vesting_contract_common::PledgeData; pub(crate) type BlockTimestampSecs = u64; @@ -14,7 +14,9 @@ const BALANCES: Map<'_, u32, Uint128> = Map::new("blc"); const WITHDRAWNS: Map<'_, u32, Uint128> = Map::new("wthd"); const BOND_PLEDGES: Map<'_, u32, PledgeData> = Map::new("bnd"); const GATEWAY_PLEDGES: Map<'_, u32, PledgeData> = Map::new("gtw"); -pub const DELEGATIONS: Map<'_, (u32, IdentityKey, BlockTimestampSecs), Uint128> = Map::new("dlg"); +pub const _OLD_DELEGATIONS: Map<'_, (u32, IdentityKey, BlockTimestampSecs), Uint128> = + Map::new("dlg"); +pub const DELEGATIONS: Map<'_, (u32, NodeId, BlockTimestampSecs), Uint128> = Map::new("dlg_v2"); pub const ADMIN: Item<'_, String> = Item::new("adm"); pub const MIXNET_CONTRACT_ADDRESS: Item<'_, String> = Item::new("mix"); pub const MIX_DENOM: Item<'_, String> = Item::new("den"); @@ -35,7 +37,7 @@ pub fn update_locked_pledge_cap( } pub fn save_delegation( - key: (u32, IdentityKey, BlockTimestampSecs), + key: (u32, NodeId, BlockTimestampSecs), amount: Uint128, storage: &mut dyn Storage, ) -> Result<(), ContractError> { @@ -44,7 +46,7 @@ pub fn save_delegation( } pub fn remove_delegation( - key: (u32, IdentityKey, BlockTimestampSecs), + key: (u32, NodeId, BlockTimestampSecs), storage: &mut dyn Storage, ) -> Result<(), ContractError> { DELEGATIONS.remove(storage, key); diff --git a/contracts/vesting/src/traits/bonding_account.rs b/contracts/vesting/src/traits/bonding_account.rs index 1ce2690df9..36c248e393 100644 --- a/contracts/vesting/src/traits/bonding_account.rs +++ b/contracts/vesting/src/traits/bonding_account.rs @@ -1,18 +1,17 @@ use crate::errors::ContractError; use cosmwasm_std::{Coin, Env, Response, Storage}; -use mixnet_contract_common::{Gateway, MixNode}; +use mixnet_contract_common::{ + mixnode::{MixNodeConfigUpdate, MixNodeCostParams}, + Gateway, MixNode, +}; pub trait MixnodeBondingAccount { - fn try_compound_operator_reward( - &self, - storage: &dyn Storage, - ) -> Result; - fn try_claim_operator_reward(&self, storage: &dyn Storage) -> Result; fn try_bond_mixnode( &self, mix_node: MixNode, + cost_params: MixNodeCostParams, owner_signature: String, pledge: Coin, env: &Env, @@ -29,7 +28,13 @@ pub trait MixnodeBondingAccount { fn try_update_mixnode_config( &self, - profit_margin_percent: u8, + new_config: MixNodeConfigUpdate, + storage: &mut dyn Storage, + ) -> Result; + + fn try_update_mixnode_cost_params( + &self, + new_costs: MixNodeCostParams, storage: &mut dyn Storage, ) -> Result; } diff --git a/contracts/vesting/src/traits/delegating_account.rs b/contracts/vesting/src/traits/delegating_account.rs index 174a9202ea..230f8b3bd9 100644 --- a/contracts/vesting/src/traits/delegating_account.rs +++ b/contracts/vesting/src/traits/delegating_account.rs @@ -1,23 +1,17 @@ use crate::errors::ContractError; use cosmwasm_std::{Coin, Env, Response, Storage, Uint128}; -use mixnet_contract_common::IdentityKey; +use mixnet_contract_common::NodeId; pub trait DelegatingAccount { fn try_claim_delegator_reward( &self, - mix_identity: IdentityKey, - storage: &dyn Storage, - ) -> Result; - - fn try_compound_delegator_reward( - &self, - mix_identity: IdentityKey, + mix_id: NodeId, storage: &dyn Storage, ) -> Result; fn try_delegate_to_mixnode( &self, - mix_identity: IdentityKey, + mix_id: NodeId, amount: Coin, env: &Env, storage: &mut dyn Storage, @@ -25,7 +19,7 @@ pub trait DelegatingAccount { fn try_undelegate_from_mixnode( &self, - mix_identity: IdentityKey, + mix_id: NodeId, storage: &dyn Storage, ) -> Result; @@ -36,7 +30,7 @@ pub trait DelegatingAccount { fn track_delegation( &self, block_height: u64, - mix_identity: IdentityKey, + mix_id: NodeId, // Save some gas by passing it in current_balance: Uint128, delegation: Coin, @@ -46,7 +40,7 @@ pub trait DelegatingAccount { // vesting account performs an undelegation. fn track_undelegation( &self, - mix_identity: IdentityKey, + mix_id: NodeId, amount: Coin, storage: &mut dyn Storage, ) -> Result<(), ContractError>; diff --git a/contracts/vesting/src/vesting/account/delegating_account.rs b/contracts/vesting/src/vesting/account/delegating_account.rs index 80f825084b..7699fb58c1 100644 --- a/contracts/vesting/src/vesting/account/delegating_account.rs +++ b/contracts/vesting/src/vesting/account/delegating_account.rs @@ -2,60 +2,37 @@ use crate::errors::ContractError; use crate::storage::locked_pledge_cap; use crate::storage::save_delegation; use crate::storage::MIXNET_CONTRACT_ADDRESS; -use crate::storage::MIX_DENOM; use crate::traits::DelegatingAccount; use crate::traits::VestingAccount; use cosmwasm_std::{wasm_execute, Coin, Env, Response, Storage, Uint128}; use mixnet_contract_common::ExecuteMsg as MixnetExecuteMsg; -use mixnet_contract_common::IdentityKey; +use mixnet_contract_common::NodeId; use vesting_contract_common::events::{ new_vesting_delegation_event, new_vesting_undelegation_event, }; -use vesting_contract_common::one_ucoin; use super::Account; impl DelegatingAccount for Account { fn try_claim_delegator_reward( &self, - mix_identity: IdentityKey, + mix_id: NodeId, storage: &dyn Storage, ) -> Result { - let msg = MixnetExecuteMsg::ClaimDelegatorRewardOnBehalf { + let msg = MixnetExecuteMsg::WithdrawDelegatorRewardOnBehalf { owner: self.owner_address().to_string(), - mix_identity, + mix_id, }; - let compound_delegator_reward_msg = wasm_execute( - MIXNET_CONTRACT_ADDRESS.load(storage)?, - &msg, - vec![one_ucoin(MIX_DENOM.load(storage)?)], - )?; - - Ok(Response::new().add_message(compound_delegator_reward_msg)) - } - - fn try_compound_delegator_reward( - &self, - mix_identity: IdentityKey, - storage: &dyn Storage, - ) -> Result { - let msg = MixnetExecuteMsg::CompoundDelegatorRewardOnBehalf { - owner: self.owner_address().to_string(), - mix_identity, - }; - let compound_delegator_reward_msg = wasm_execute( - MIXNET_CONTRACT_ADDRESS.load(storage)?, - &msg, - vec![one_ucoin(MIX_DENOM.load(storage)?)], - )?; + let compound_delegator_reward_msg = + wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; Ok(Response::new().add_message(compound_delegator_reward_msg)) } fn try_delegate_to_mixnode( &self, - mix_identity: IdentityKey, + mix_id: NodeId, coin: Coin, env: &Env, storage: &mut dyn Storage, @@ -79,7 +56,7 @@ impl DelegatingAccount for Account { } let msg = MixnetExecuteMsg::DelegateToMixnodeOnBehalf { - mix_identity: mix_identity.clone(), + mix_id, delegate: self.owner_address().into_string(), }; let delegate_to_mixnode = wasm_execute( @@ -89,7 +66,7 @@ impl DelegatingAccount for Account { )?; self.track_delegation( env.block.time.seconds(), - mix_identity, + mix_id, current_balance, coin, storage, @@ -102,25 +79,22 @@ impl DelegatingAccount for Account { fn try_undelegate_from_mixnode( &self, - mix_identity: IdentityKey, + mix_id: NodeId, storage: &dyn Storage, ) -> Result { - if !self.any_delegation_for_mix(&mix_identity, storage) { + if !self.any_delegation_for_mix(mix_id, storage) { return Err(ContractError::NoSuchDelegation( self.owner_address(), - mix_identity, + mix_id, )); } let msg = MixnetExecuteMsg::UndelegateFromMixnodeOnBehalf { - mix_identity, + mix_id, delegate: self.owner_address().into_string(), }; - let undelegate_from_mixnode = wasm_execute( - MIXNET_CONTRACT_ADDRESS.load(storage)?, - &msg, - vec![one_ucoin(MIX_DENOM.load(storage)?)], - )?; + let undelegate_from_mixnode = + wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; Ok(Response::new() .add_message(undelegate_from_mixnode) @@ -130,13 +104,13 @@ impl DelegatingAccount for Account { fn track_delegation( &self, block_timestamp_secs: u64, - mix_identity: IdentityKey, + mix_id: NodeId, current_balance: Uint128, delegation: Coin, storage: &mut dyn Storage, ) -> Result<(), ContractError> { save_delegation( - (self.storage_key(), mix_identity, block_timestamp_secs), + (self.storage_key(), mix_id, block_timestamp_secs), delegation.amount, storage, )?; @@ -147,11 +121,11 @@ impl DelegatingAccount for Account { fn track_undelegation( &self, - mix_identity: IdentityKey, + mix_id: NodeId, amount: Coin, storage: &mut dyn Storage, ) -> Result<(), ContractError> { - self.remove_delegations_for_mix(&mix_identity, storage)?; + self.remove_delegations_for_mix(mix_id, storage)?; let new_balance = Uint128::new(self.load_balance(storage)?.u128() + amount.amount.u128()); self.save_balance(new_balance, storage)?; Ok(()) diff --git a/contracts/vesting/src/vesting/account/gateway_bonding_account.rs b/contracts/vesting/src/vesting/account/gateway_bonding_account.rs index 33af1d6506..e9979b71a6 100644 --- a/contracts/vesting/src/vesting/account/gateway_bonding_account.rs +++ b/contracts/vesting/src/vesting/account/gateway_bonding_account.rs @@ -2,7 +2,6 @@ use super::PledgeData; use crate::errors::ContractError; use crate::storage::locked_pledge_cap; use crate::storage::MIXNET_CONTRACT_ADDRESS; -use crate::storage::MIX_DENOM; use crate::traits::GatewayBondingAccount; use crate::traits::VestingAccount; use cosmwasm_std::{wasm_execute, Coin, Env, Response, Storage, Uint128}; @@ -10,7 +9,6 @@ use mixnet_contract_common::{ExecuteMsg as MixnetExecuteMsg, Gateway}; use vesting_contract_common::events::{ new_vesting_gateway_bonding_event, new_vesting_gateway_unbonding_event, }; -use vesting_contract_common::one_ucoin; use super::Account; @@ -74,11 +72,7 @@ impl GatewayBondingAccount for Account { }; if let Some(_bond) = self.load_gateway_pledge(storage)? { - let unbond_msg = wasm_execute( - MIXNET_CONTRACT_ADDRESS.load(storage)?, - &msg, - vec![one_ucoin(MIX_DENOM.load(storage)?)], - )?; + let unbond_msg = wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; Ok(Response::new() .add_message(unbond_msg) diff --git a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs index 6312647a9a..7fd182e8c9 100644 --- a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs +++ b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs @@ -1,77 +1,38 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::Account; use crate::errors::ContractError; use crate::storage::locked_pledge_cap; use crate::storage::MIXNET_CONTRACT_ADDRESS; -use crate::storage::MIX_DENOM; use crate::traits::MixnodeBondingAccount; use crate::traits::VestingAccount; use cosmwasm_std::{wasm_execute, Coin, Env, Response, Storage, Uint128}; +use mixnet_contract_common::mixnode::MixNodeConfigUpdate; +use mixnet_contract_common::mixnode::MixNodeCostParams; use mixnet_contract_common::{ExecuteMsg as MixnetExecuteMsg, MixNode}; use vesting_contract_common::events::{ new_vesting_mixnode_bonding_event, new_vesting_mixnode_unbonding_event, - new_vesting_update_mixnode_config_event, + new_vesting_update_mixnode_config_event, new_vesting_update_mixnode_cost_params_event, }; - -use vesting_contract_common::one_ucoin; use vesting_contract_common::PledgeData; -use super::Account; - impl MixnodeBondingAccount for Account { fn try_claim_operator_reward(&self, storage: &dyn Storage) -> Result { - let msg = MixnetExecuteMsg::ClaimOperatorRewardOnBehalf { + let msg = MixnetExecuteMsg::WithdrawOperatorRewardOnBehalf { owner: self.owner_address().into_string(), }; - let compound_operator_reward_msg = wasm_execute( - MIXNET_CONTRACT_ADDRESS.load(storage)?, - &msg, - vec![one_ucoin(MIX_DENOM.load(storage)?)], - )?; + let compound_operator_reward_msg = + wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; Ok(Response::new().add_message(compound_operator_reward_msg)) } - fn try_compound_operator_reward( - &self, - storage: &dyn Storage, - ) -> Result { - let msg = MixnetExecuteMsg::CompoundOperatorRewardOnBehalf { - owner: self.owner_address().into_string(), - }; - - let compound_operator_reward_msg = wasm_execute( - MIXNET_CONTRACT_ADDRESS.load(storage)?, - &msg, - vec![one_ucoin(MIX_DENOM.load(storage)?)], - )?; - - Ok(Response::new().add_message(compound_operator_reward_msg)) - } - - fn try_update_mixnode_config( - &self, - profit_margin_percent: u8, - storage: &mut dyn Storage, - ) -> Result { - let msg = MixnetExecuteMsg::UpdateMixnodeConfigOnBehalf { - profit_margin_percent, - owner: self.owner_address().into_string(), - }; - - let update_mixnode_config_msg = wasm_execute( - MIXNET_CONTRACT_ADDRESS.load(storage)?, - &msg, - vec![one_ucoin(MIX_DENOM.load(storage)?)], - )?; - - Ok(Response::new() - .add_message(update_mixnode_config_msg) - .add_event(new_vesting_update_mixnode_config_event())) - } - fn try_bond_mixnode( &self, mix_node: MixNode, + cost_params: MixNodeCostParams, owner_signature: String, pledge: Coin, env: &Env, @@ -105,6 +66,7 @@ impl MixnodeBondingAccount for Account { let msg = MixnetExecuteMsg::BondMixnodeOnBehalf { mix_node, + cost_params, owner: self.owner_address().into_string(), owner_signature, }; @@ -128,11 +90,7 @@ impl MixnodeBondingAccount for Account { }; if self.load_mixnode_pledge(storage)?.is_some() { - let unbond_msg = wasm_execute( - MIXNET_CONTRACT_ADDRESS.load(storage)?, - &msg, - vec![one_ucoin(MIX_DENOM.load(storage)?)], - )?; + let unbond_msg = wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; Ok(Response::new() .add_message(unbond_msg) @@ -155,4 +113,40 @@ impl MixnodeBondingAccount for Account { self.remove_mixnode_pledge(storage)?; Ok(()) } + + fn try_update_mixnode_config( + &self, + new_config: MixNodeConfigUpdate, + storage: &mut dyn Storage, + ) -> Result { + let msg = MixnetExecuteMsg::UpdateMixnodeConfigOnBehalf { + new_config, + owner: self.owner_address().into_string(), + }; + + let update_mixnode_config_msg = + wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; + + Ok(Response::new() + .add_message(update_mixnode_config_msg) + .add_event(new_vesting_update_mixnode_config_event())) + } + + fn try_update_mixnode_cost_params( + &self, + new_costs: MixNodeCostParams, + storage: &mut dyn Storage, + ) -> Result { + let msg = MixnetExecuteMsg::UpdateMixnodeCostParamsOnBehalf { + new_costs, + owner: self.owner_address().into_string(), + }; + + let update_mixnode_costs_msg = + wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; + + Ok(Response::new() + .add_message(update_mixnode_costs_msg) + .add_event(new_vesting_update_mixnode_cost_params_event())) + } } diff --git a/contracts/vesting/src/vesting/account/mod.rs b/contracts/vesting/src/vesting/account/mod.rs index 232bc32664..171dfaf616 100644 --- a/contracts/vesting/src/vesting/account/mod.rs +++ b/contracts/vesting/src/vesting/account/mod.rs @@ -7,7 +7,7 @@ use crate::storage::{ }; use cosmwasm_std::{Addr, Coin, Order, Storage, Timestamp, Uint128}; use cw_storage_plus::Bound; -use mixnet_contract_common::IdentityKey; +use mixnet_contract_common::NodeId; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use vesting_contract_common::{Period, PledgeData}; @@ -198,9 +198,9 @@ impl Account { remove_gateway_pledge(self.storage_key(), storage) } - pub fn any_delegation_for_mix(&self, mix: &str, storage: &dyn Storage) -> bool { + pub fn any_delegation_for_mix(&self, mix_id: NodeId, storage: &dyn Storage) -> bool { DELEGATIONS - .prefix((self.storage_key(), mix.to_string())) + .prefix((self.storage_key(), mix_id)) .range(storage, None, None, Order::Ascending) .next() .is_some() @@ -208,7 +208,7 @@ impl Account { pub fn remove_delegations_for_mix( &self, - mix: &str, + mix_id: NodeId, storage: &mut dyn Storage, ) -> Result<(), ContractError> { let limit = 50; @@ -219,7 +219,7 @@ impl Account { loop { block_heights.extend( DELEGATIONS - .prefix((self.storage_key(), mix.to_string())) + .prefix((self.storage_key(), mix_id)) .keys(storage, start_after, None, Order::Ascending) .take(limit) .filter_map(|key| key.ok()), @@ -238,18 +238,18 @@ impl Account { } for block_height in block_heights { - remove_delegation((self.storage_key(), mix.to_string(), block_height), storage)?; + remove_delegation((self.storage_key(), mix_id, block_height), storage)?; } Ok(()) } pub fn total_delegations_for_mix( &self, - mix: IdentityKey, + mix_id: NodeId, storage: &dyn Storage, ) -> Result { Ok(DELEGATIONS - .prefix((self.storage_key(), mix)) + .prefix((self.storage_key(), mix_id)) .range(storage, None, None, Order::Ascending) .filter_map(|x| x.ok()) .fold(Uint128::zero(), |acc, (_key, val)| acc + val)) diff --git a/contracts/vesting/src/vesting/mod.rs b/contracts/vesting/src/vesting/mod.rs index 45d8b9bfa2..3e46c46496 100644 --- a/contracts/vesting/src/vesting/mod.rs +++ b/contracts/vesting/src/vesting/mod.rs @@ -47,7 +47,8 @@ mod tests { use crate::vesting::{populate_vesting_periods, Account}; use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::{coins, Addr, Coin, Timestamp, Uint128}; - use mixnet_contract_common::{Gateway, MixNode}; + use mixnet_contract_common::mixnode::MixNodeCostParams; + use mixnet_contract_common::{Gateway, MixNode, Percent}; use vesting_contract_common::messages::{ExecuteMsg, VestingSpecification}; use vesting_contract_common::Period; @@ -272,19 +273,15 @@ mod tests { let withdrawn = account.load_withdrawn(&deps.storage).unwrap(); assert_eq!(withdrawn, Uint128::zero()); - let mix_identity = "alice".to_string(); + let mix_id = 1; let delegation = Coin { amount: Uint128::new(90_000_000_000), denom: TEST_COIN_DENOM.to_string(), }; - let ok = account.try_delegate_to_mixnode( - mix_identity.clone(), - delegation.clone(), - &env, - &mut deps.storage, - ); + let ok = + account.try_delegate_to_mixnode(mix_id, delegation.clone(), &env, &mut deps.storage); assert!(ok.is_ok()); let vested_coins = account.get_vested_coins(None, &env, &deps.storage).unwrap(); @@ -312,11 +309,11 @@ mod tests { .unwrap(); assert_eq!(spendable.amount, Uint128::new(160_000_000_000)); - let ok = account.try_undelegate_from_mixnode(mix_identity.clone(), &mut deps.storage); + let ok = account.try_undelegate_from_mixnode(mix_id, &mut deps.storage); assert!(ok.is_ok()); account - .track_undelegation(mix_identity.clone(), delegation.clone(), &mut deps.storage) + .track_undelegation(mix_id, delegation.clone(), &mut deps.storage) .unwrap(); let delegated_free = account @@ -362,12 +359,8 @@ mod tests { .unwrap(); assert_eq!(spendable.amount, Uint128::zero()); - let ok = account.try_delegate_to_mixnode( - mix_identity.clone(), - delegation.clone(), - &env, - &mut deps.storage, - ); + let ok = + account.try_delegate_to_mixnode(mix_id, delegation.clone(), &env, &mut deps.storage); assert!(ok.is_ok()); let vested_coins = account.get_vested_coins(None, &env, &deps.storage).unwrap(); @@ -405,7 +398,7 @@ mod tests { // Try delegating too much let err = account.try_delegate_to_mixnode( - "alice".to_string(), + 1, Coin { amount: Uint128::new(1_000_000_000_001), denom: TEST_COIN_DENOM.to_string(), @@ -416,7 +409,7 @@ mod tests { assert!(err.is_err()); let ok = account.try_delegate_to_mixnode( - "alice".to_string(), + 1, Coin { amount: Uint128::new(90_000_000_000), denom: TEST_COIN_DENOM.to_string(), @@ -428,7 +421,7 @@ mod tests { // Fails due to delegation locked delegation cap let ok = account.try_delegate_to_mixnode( - "alice".to_string(), + 1, Coin { amount: Uint128::new(20_000_000_000), denom: TEST_COIN_DENOM.to_string(), @@ -443,7 +436,7 @@ mod tests { // Try delegating too much again let err = account.try_delegate_to_mixnode( - "alice".to_string(), + 1, Coin { amount: Uint128::new(500_000_000_001), denom: TEST_COIN_DENOM.to_string(), @@ -453,9 +446,7 @@ mod tests { ); assert!(err.is_err()); - let total_delegations = account - .total_delegations_for_mix("alice".to_string(), &deps.storage) - .unwrap(); + let total_delegations = account.total_delegations_for_mix(1, &deps.storage).unwrap(); assert_eq!(Uint128::new(90_000_000_000), total_delegations); // Current period -> block_time: None @@ -534,11 +525,19 @@ mod tests { sphinx_key: "sphinx".to_string(), identity_key: "identity".to_string(), version: "0.10.0".to_string(), - profit_margin_percent: 10, + }; + + let cost_params = MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(10).unwrap(), + interval_operating_cost: Coin { + denom: "NYM".to_string(), + amount: Uint128::new(40), + }, }; // Try delegating too much let err = account.try_bond_mixnode( mix_node.clone(), + cost_params.clone(), "alice".to_string(), Coin { amount: Uint128::new(1_000_000_000_001), @@ -551,6 +550,7 @@ mod tests { let ok = account.try_bond_mixnode( mix_node.clone(), + cost_params.clone(), "alice".to_string(), Coin { amount: Uint128::new(90_000_000_000), @@ -567,6 +567,7 @@ mod tests { // Try delegating too much again let err = account.try_bond_mixnode( mix_node, + cost_params, "alice".to_string(), Coin { amount: Uint128::new(10_000_000_001), @@ -818,7 +819,7 @@ mod tests { // time for some delegations - let mix_identity = "alice".to_string(); + let mix_id = 42; let delegation = Coin { amount: Uint128::new(90_000_000_000), @@ -830,7 +831,7 @@ mod tests { env.block.height = account_creation_blockheight; env.block.time = Timestamp::from_seconds(account_creation_timestamp); let ok = vesting_account.try_delegate_to_mixnode( - mix_identity.clone(), + mix_id, delegation.clone(), &env, &mut deps.storage, @@ -885,7 +886,7 @@ mod tests { denom: TEST_COIN_DENOM.to_string(), }; let ok = vesting_account.try_delegate_to_mixnode( - mix_identity.clone(), + mix_id, delegation.clone(), &env, &mut deps.storage, diff --git a/explorer-api/src/cache/mod.rs b/explorer-api/src/cache/mod.rs index 7096220486..74448f4417 100644 --- a/explorer-api/src/cache/mod.rs +++ b/explorer-api/src/cache/mod.rs @@ -1,15 +1,20 @@ +use std::borrow::Borrow; use std::collections::HashMap; +use std::hash::Hash; use std::time::{Duration, SystemTime}; const DEFAULT_CACHE_VALIDITY: Duration = Duration::from_secs(60); #[derive(Clone)] -pub(crate) struct Cache { - inner: HashMap>, +pub(crate) struct Cache { + inner: HashMap>, cache_validity_duration: Duration, } -impl Cache { +impl Cache +where + K: Eq + Hash, +{ pub(crate) fn new() -> Self { Cache { inner: HashMap::new(), @@ -29,23 +34,27 @@ impl Cache { self.inner.len() } - pub(crate) fn get_all(&self) -> Vec { + pub(crate) fn get_all(&self) -> Vec { self.inner .values() .map(|cache_item| cache_item.value.clone()) .collect() } - pub(crate) fn get(&self, key: &str) -> Option { + pub(crate) fn get(&self, key: &Q) -> Option + where + K: Borrow, + Q: Hash + Eq, + { self.inner .get(key) .filter(|cache_item| cache_item.valid_until >= SystemTime::now()) .map(|cache_item| cache_item.value.clone()) } - pub(crate) fn set(&mut self, key: &str, value: T) { + pub(crate) fn set(&mut self, key: K, value: V) { self.inner.insert( - key.to_string(), + key, CacheItem { valid_until: SystemTime::now() + self.cache_validity_duration, value, @@ -54,12 +63,20 @@ impl Cache { } #[allow(unused)] - pub(crate) fn remove(&mut self, key: &str) -> Option { + pub(crate) fn remove(&mut self, key: &Q) -> Option + where + K: Borrow, + Q: Hash + Eq, + { self.inner.remove(key).map(|item| item.value) } #[allow(unused)] - pub(crate) fn remove_if_expired(&mut self, key: &str) -> Option { + pub(crate) fn remove_if_expired(&mut self, key: &Q) -> Option + where + K: Borrow, + Q: Hash + Eq, + { if self.inner.get(key)?.has_expired() { self.remove(key) } else { diff --git a/explorer-api/src/country_statistics/geolocate.rs b/explorer-api/src/country_statistics/geolocate.rs index fd84a8c380..10329c212a 100644 --- a/explorer-api/src/country_statistics/geolocate.rs +++ b/explorer-api/src/country_statistics/geolocate.rs @@ -58,7 +58,7 @@ impl GeoLocateTask { .state .inner .mixnodes - .is_location_valid(&cache_item.mix_node().identity_key) + .is_location_valid(cache_item.mix_id()) .await { // when the cached location is valid, don't locate and continue to next mix node @@ -87,7 +87,7 @@ impl GeoLocateTask { self.state .inner .mixnodes - .set_location(&cache_item.mix_node().identity_key, Some(location)) + .set_location(cache_item.mix_id(), Some(location)) .await; // one node has been located, so return out of the loop @@ -106,7 +106,7 @@ impl GeoLocateTask { self.state .inner .mixnodes - .set_location(&cache_item.mix_node().identity_key, None) + .set_location(cache_item.mix_id(), None) .await; }, LocateError::RateLimited(e) => warn!( diff --git a/explorer-api/src/gateways/models.rs b/explorer-api/src/gateways/models.rs index 690aa7cb7e..a051d88e6b 100644 --- a/explorer-api/src/gateways/models.rs +++ b/explorer-api/src/gateways/models.rs @@ -1,17 +1,14 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::sync::Arc; - +use crate::cache::Cache; +use mixnet_contract_common::{GatewayBond, IdentityKey}; use serde::Serialize; +use std::sync::Arc; use tokio::sync::RwLock; -use mixnet_contract_common::GatewayBond; - -use crate::cache::Cache; - pub(crate) struct GatewayCache { - pub(crate) gateways: Cache, + pub(crate) gateways: Cache, } #[derive(Clone, Debug, Serialize, JsonSchema)] @@ -49,7 +46,7 @@ impl ThreadsafeGatewayCache { for gateway in gateways { guard .gateways - .set(gateway.clone().gateway.identity_key.as_str(), gateway) + .set(gateway.gateway.identity_key.clone(), gateway) } } } diff --git a/explorer-api/src/helpers.rs b/explorer-api/src/helpers.rs new file mode 100644 index 0000000000..6b3d019833 --- /dev/null +++ b/explorer-api/src/helpers.rs @@ -0,0 +1,10 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use mixnet_contract_common::{Decimal, Fraction}; + +pub(crate) fn best_effort_small_dec_to_f64(dec: Decimal) -> f64 { + let num = dec.numerator().u128() as f64; + let den = dec.denominator().u128() as f64; + num / den +} diff --git a/explorer-api/src/main.rs b/explorer-api/src/main.rs index 7e6779794b..4e144b56a9 100644 --- a/explorer-api/src/main.rs +++ b/explorer-api/src/main.rs @@ -13,6 +13,7 @@ mod client; pub(crate) mod commands; mod country_statistics; mod gateways; +mod helpers; mod http; mod mix_node; pub(crate) mod mix_nodes; diff --git a/explorer-api/src/mix_node/delegations.rs b/explorer-api/src/mix_node/delegations.rs index adbc61e428..57053fde3a 100644 --- a/explorer-api/src/mix_node/delegations.rs +++ b/explorer-api/src/mix_node/delegations.rs @@ -1,25 +1,23 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use itertools::Itertools; - -use crate::client::ThreadsafeValidatorClient; -use mixnet_contract_common::Delegation; - use super::models::SummedDelegations; +use crate::client::ThreadsafeValidatorClient; +use itertools::Itertools; +use mixnet_contract_common::{Delegation, NodeId}; pub(crate) async fn get_single_mixnode_delegations( client: &ThreadsafeValidatorClient, - pubkey: &str, + mix_id: NodeId, ) -> Vec { match client .0 - .get_all_nymd_single_mixnode_delegations(pubkey.to_string()) + .get_all_nymd_single_mixnode_delegations(mix_id) .await { Ok(result) => result, Err(e) => { - error!("Could not get delegations for mix node {}: {:?}", pubkey, e); + error!("Could not get delegations for mix node {}: {:?}", mix_id, e); vec![] } } @@ -27,9 +25,9 @@ pub(crate) async fn get_single_mixnode_delegations( pub(crate) async fn get_single_mixnode_delegations_summed( client: &ThreadsafeValidatorClient, - pubkey: &str, + mix_id: NodeId, ) -> Vec { - let delegations_by_owner = get_single_mixnode_delegations(client, pubkey) + let delegations_by_owner = get_single_mixnode_delegations(client, mix_id) .await .into_iter() .into_group_map_by(|delegation| delegation.owner.clone()); diff --git a/explorer-api/src/mix_node/econ_stats.rs b/explorer-api/src/mix_node/econ_stats.rs index 3c526097d4..377d542983 100644 --- a/explorer-api/src/mix_node/econ_stats.rs +++ b/explorer-api/src/mix_node/econ_stats.rs @@ -2,40 +2,56 @@ // SPDX-License-Identifier: Apache-2.0 use crate::client::ThreadsafeValidatorClient; +use crate::helpers::best_effort_small_dec_to_f64; use crate::mix_node::models::EconomicDynamicsStats; +use mixnet_contract_common::rewarding::helpers::truncate_decimal; +use mixnet_contract_common::NodeId; pub(crate) async fn retrieve_mixnode_econ_stats( client: &ThreadsafeValidatorClient, - identity: &str, + mix_id: NodeId, ) -> Option { let stake_saturation = client .0 .validator_api - .get_mixnode_stake_saturation(identity) + .get_mixnode_stake_saturation(mix_id) .await .ok()?; let inclusion_probability = client .0 .validator_api - .get_mixnode_inclusion_probability(identity) + .get_mixnode_inclusion_probability(mix_id) .await .ok()?; let reward_estimation = client .0 .validator_api - .get_mixnode_reward_estimation(identity) + .get_mixnode_reward_estimation(mix_id) + .await + .ok()?; + + let uptime_response = client + .0 + .validator_api + .get_mixnode_avg_uptime(mix_id) .await .ok()?; Some(EconomicDynamicsStats { - stake_saturation: stake_saturation.saturation, + stake_saturation: best_effort_small_dec_to_f64(stake_saturation.saturation) as f32, active_set_inclusion_probability: inclusion_probability.in_active, reserve_set_inclusion_probability: inclusion_probability.in_reserve, - estimated_total_node_reward: reward_estimation.estimated_total_node_reward, - estimated_operator_reward: reward_estimation.estimated_operator_reward, - estimated_delegators_reward: reward_estimation.estimated_delegators_reward, - current_interval_uptime: reward_estimation.reward_params.node.uptime().u128() as u8, + // drop precision for compatibility sake + estimated_total_node_reward: truncate_decimal( + reward_estimation.estimation.total_node_reward, + ) + .u128() as u64, + estimated_operator_reward: truncate_decimal(reward_estimation.estimation.operator).u128() + as u64, + estimated_delegators_reward: truncate_decimal(reward_estimation.estimation.delegates).u128() + as u64, + current_interval_uptime: uptime_response.avg_uptime, }) } diff --git a/explorer-api/src/mix_node/http.rs b/explorer-api/src/mix_node/http.rs index aa037c5651..95905ff5d3 100644 --- a/explorer-api/src/mix_node/http.rs +++ b/explorer-api/src/mix_node/http.rs @@ -1,16 +1,6 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use reqwest::Error as ReqwestError; -use rocket::response::status::NotFound; -use rocket::serde::json::Json; -use rocket::{Route, State}; -use rocket_okapi::okapi::openapi3::OpenApi; -use rocket_okapi::openapi_get_routes_spec; -use rocket_okapi::settings::OpenApiSettings; - -use mixnet_contract_common::Delegation; - use crate::mix_node::delegations::{ get_single_mixnode_delegations, get_single_mixnode_delegations_summed, }; @@ -19,6 +9,14 @@ use crate::mix_node::models::{ EconomicDynamicsStats, NodeDescription, NodeStats, PrettyDetailedMixNodeBond, SummedDelegations, }; use crate::state::ExplorerApiStateContext; +use mixnet_contract_common::{Delegation, NodeId}; +use reqwest::Error as ReqwestError; +use rocket::response::status::NotFound; +use rocket::serde::json::Json; +use rocket::{Route, State}; +use rocket_okapi::okapi::openapi3::OpenApi; +use rocket_okapi::openapi_get_routes_spec; +use rocket_okapi::settings::OpenApiSettings; pub fn mix_node_make_default_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { openapi_get_routes_spec![ @@ -31,163 +29,6 @@ pub fn mix_node_make_default_routes(settings: &OpenApiSettings) -> (Vec, ] } -#[openapi(tag = "mix_nodes")] -#[get("/")] -pub(crate) async fn get_by_id( - pubkey: &str, - state: &State, -) -> Result, NotFound> { - match state - .inner - .mixnodes - .get_detailed_mixnode_by_id(pubkey) - .await - { - Some(mixnode) => Ok(Json(mixnode)), - None => Err(NotFound("Mixnode not found".to_string())), - } -} - -#[openapi(tag = "mix_node")] -#[get("//delegations")] -pub(crate) async fn get_delegations( - pubkey: &str, - state: &State, -) -> Json> { - Json(get_single_mixnode_delegations(&state.inner.validator_client, pubkey).await) -} - -#[openapi(tag = "mix_node")] -#[get("//delegations/summed")] -pub(crate) async fn get_delegations_summed( - pubkey: &str, - state: &State, -) -> Json> { - Json(get_single_mixnode_delegations_summed(&state.inner.validator_client, pubkey).await) -} - -#[openapi(tag = "mix_node")] -#[get("//description")] -pub(crate) async fn get_description( - pubkey: &str, - state: &State, -) -> Option> { - match state.inner.mixnode.clone().get_description(pubkey).await { - Some(cache_value) => { - trace!("Returning cached value for {}", pubkey); - Some(Json(cache_value)) - } - None => { - trace!("No valid cache value for {}", pubkey); - match state.inner.get_mix_node(pubkey).await { - Some(bond) => { - match get_mix_node_description( - &bond.mix_node().host, - bond.mix_node().http_api_port, - ) - .await - { - Ok(response) => { - // cache the response and return as the HTTP response - state - .inner - .mixnode - .set_description(pubkey, response.clone()) - .await; - Some(Json(response)) - } - Err(e) => { - error!( - "Unable to get description for {} on {}:{} -> {}", - pubkey, - bond.mix_node().host, - bond.mix_node().http_api_port, - e - ); - Option::None - } - } - } - None => Option::None, - } - } - } -} - -#[openapi(tag = "mix_node")] -#[get("//stats")] -pub(crate) async fn get_stats( - pubkey: &str, - state: &State, -) -> Option> { - match state.inner.mixnode.get_node_stats(pubkey).await { - Some(cache_value) => { - trace!("Returning cached value for {}", pubkey); - Some(Json(cache_value)) - } - None => { - trace!("No valid cache value for {}", pubkey); - match state.inner.get_mix_node(pubkey).await { - Some(bond) => { - match get_mix_node_stats(&bond.mix_node().host, bond.mix_node().http_api_port) - .await - { - Ok(response) => { - // cache the response and return as the HTTP response - state - .inner - .mixnode - .set_node_stats(pubkey, response.clone()) - .await; - Some(Json(response)) - } - Err(e) => { - error!( - "Unable to get description for {} on {}:{} -> {}", - pubkey, - bond.mix_node().host, - bond.mix_node().http_api_port, - e - ); - Option::None - } - } - } - None => Option::None, - } - } - } -} - -#[openapi(tag = "mix_node")] -#[get("//economic-dynamics-stats")] -pub(crate) async fn get_economic_dynamics_stats( - pubkey: &str, - state: &State, -) -> Option> { - match state.inner.mixnode.get_econ_stats(pubkey).await { - Some(cache_value) => { - trace!("Returning cached value for {}", pubkey); - Some(Json(cache_value)) - } - None => { - trace!("No valid cache value for {}", pubkey); - - // get fresh value from the validator API - let econ_stats = - retrieve_mixnode_econ_stats(&state.inner.validator_client, pubkey).await?; - - // update cache - state - .inner - .mixnode - .set_econ_stats(pubkey, econ_stats.clone()) - .await; - Some(Json(econ_stats)) - } - } -} - async fn get_mix_node_description(host: &str, port: u16) -> Result { reqwest::get(format!("http://{}:{}/description", host, port)) .await? @@ -201,3 +42,155 @@ async fn get_mix_node_stats(host: &str, port: u16) -> Result() .await } + +#[openapi(tag = "mix_nodes")] +#[get("/")] +pub(crate) async fn get_by_id( + mix_id: NodeId, + state: &State, +) -> Result, NotFound> { + match state.inner.mixnodes.get_detailed_mixnode(mix_id).await { + Some(mixnode) => Ok(Json(mixnode)), + None => Err(NotFound("Mixnode not found".to_string())), + } +} + +#[openapi(tag = "mix_node")] +#[get("//delegations")] +pub(crate) async fn get_delegations( + mix_id: NodeId, + state: &State, +) -> Json> { + Json(get_single_mixnode_delegations(&state.inner.validator_client, mix_id).await) +} + +#[openapi(tag = "mix_node")] +#[get("//delegations/summed")] +pub(crate) async fn get_delegations_summed( + mix_id: NodeId, + state: &State, +) -> Json> { + Json(get_single_mixnode_delegations_summed(&state.inner.validator_client, mix_id).await) +} + +#[openapi(tag = "mix_node")] +#[get("//description")] +pub(crate) async fn get_description( + mix_id: NodeId, + state: &State, +) -> Option> { + match state.inner.mixnode.clone().get_description(mix_id).await { + Some(cache_value) => { + trace!("Returning cached value for {}", mix_id); + Some(Json(cache_value)) + } + None => { + trace!("No valid cache value for {}", mix_id); + match state.inner.get_mix_node(mix_id).await { + Some(bond) => { + match get_mix_node_description( + &bond.mix_node().host, + bond.mix_node().http_api_port, + ) + .await + { + Ok(response) => { + // cache the response and return as the HTTP response + state + .inner + .mixnode + .set_description(mix_id, response.clone()) + .await; + Some(Json(response)) + } + Err(e) => { + error!( + "Unable to get description for {} on {}:{} -> {}", + mix_id, + bond.mix_node().host, + bond.mix_node().http_api_port, + e + ); + Option::None + } + } + } + None => Option::None, + } + } + } +} + +#[openapi(tag = "mix_node")] +#[get("//stats")] +pub(crate) async fn get_stats( + mix_id: NodeId, + state: &State, +) -> Option> { + match state.inner.mixnode.get_node_stats(mix_id).await { + Some(cache_value) => { + trace!("Returning cached value for {}", mix_id); + Some(Json(cache_value)) + } + None => { + trace!("No valid cache value for {}", mix_id); + match state.inner.get_mix_node(mix_id).await { + Some(bond) => { + match get_mix_node_stats(&bond.mix_node().host, bond.mix_node().http_api_port) + .await + { + Ok(response) => { + // cache the response and return as the HTTP response + state + .inner + .mixnode + .set_node_stats(mix_id, response.clone()) + .await; + Some(Json(response)) + } + Err(e) => { + error!( + "Unable to get description for {} on {}:{} -> {}", + mix_id, + bond.mix_node().host, + bond.mix_node().http_api_port, + e + ); + Option::None + } + } + } + None => Option::None, + } + } + } +} + +#[openapi(tag = "mix_node")] +#[get("//economic-dynamics-stats")] +pub(crate) async fn get_economic_dynamics_stats( + mix_id: NodeId, + state: &State, +) -> Option> { + match state.inner.mixnode.get_econ_stats(mix_id).await { + Some(cache_value) => { + trace!("Returning cached value for {}", mix_id); + Some(Json(cache_value)) + } + None => { + trace!("No valid cache value for {}", mix_id); + + // get fresh value from the validator API + let econ_stats = + retrieve_mixnode_econ_stats(&state.inner.validator_client, mix_id).await?; + + // update cache + state + .inner + .mixnode + .set_econ_stats(mix_id, econ_stats.clone()) + .await; + Some(Json(econ_stats)) + } + } +} diff --git a/explorer-api/src/mix_node/models.rs b/explorer-api/src/mix_node/models.rs index 83789b85c5..9e24aa69b8 100644 --- a/explorer-api/src/mix_node/models.rs +++ b/explorer-api/src/mix_node/models.rs @@ -3,8 +3,8 @@ use crate::cache::Cache; use crate::mix_nodes::location::Location; -use mixnet_contract_common::{Addr, Coin, Layer, MixNode}; -use mixnet_contract_common::{Delegation, IdentityKey}; +use mixnet_contract_common::Delegation; +use mixnet_contract_common::{Addr, Coin, Layer, MixNode, NodeId}; use serde::Deserialize; use serde::Serialize; use std::sync::Arc; @@ -22,6 +22,8 @@ pub(crate) enum MixnodeStatus { #[derive(Clone, Debug, Serialize, JsonSchema)] pub(crate) struct PrettyDetailedMixNodeBond { + // I leave this to @MS to refactor this type as a lot of things here are redundant thanks to + // the existence of `MixNodeDetails` pub location: Option, pub status: MixnodeStatus, pub pledge_amount: Coin, @@ -38,14 +40,14 @@ pub(crate) struct PrettyDetailedMixNodeBond { #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] pub struct SummedDelegations { pub owner: Addr, - pub node_identity: IdentityKey, + pub mix_id: NodeId, pub amount: Coin, } impl SummedDelegations { pub fn from(delegations: &[Delegation]) -> Option { let owner = get_common_owner(delegations)?; - let node_identity = get_common_node_identity(delegations)?; + let mix_id = get_common_mix_id(delegations)?; let denom = get_common_denom(delegations)?; let sum = delegations @@ -57,16 +59,16 @@ impl SummedDelegations { Some(SummedDelegations { owner, - node_identity, + mix_id, amount, }) } } pub(crate) struct MixNodeCache { - pub(crate) descriptions: Cache, - pub(crate) node_stats: Cache, - pub(crate) econ_stats: Cache, + pub(crate) descriptions: Cache, + pub(crate) node_stats: Cache, + pub(crate) econ_stats: Cache, } #[derive(Clone)] @@ -85,44 +87,32 @@ impl ThreadsafeMixNodeCache { } } - pub(crate) async fn get_description(&self, identity_key: &str) -> Option { - self.inner.read().await.descriptions.get(identity_key) + pub(crate) async fn get_description(&self, mix_id: NodeId) -> Option { + self.inner.read().await.descriptions.get(&mix_id) } - pub(crate) async fn get_node_stats(&self, identity_key: &str) -> Option { - self.inner.read().await.node_stats.get(identity_key) + pub(crate) async fn get_node_stats(&self, mix_id: NodeId) -> Option { + self.inner.read().await.node_stats.get(&mix_id) } - pub(crate) async fn get_econ_stats(&self, identity_key: &str) -> Option { - self.inner.read().await.econ_stats.get(identity_key) + pub(crate) async fn get_econ_stats(&self, mix_id: NodeId) -> Option { + self.inner.read().await.econ_stats.get(&mix_id) } - pub(crate) async fn set_description(&self, identity_key: &str, description: NodeDescription) { + pub(crate) async fn set_description(&self, mix_id: NodeId, description: NodeDescription) { self.inner .write() .await .descriptions - .set(identity_key, description); + .set(mix_id, description); } - pub(crate) async fn set_node_stats(&self, identity_key: &str, node_stats: NodeStats) { - self.inner - .write() - .await - .node_stats - .set(identity_key, node_stats); + pub(crate) async fn set_node_stats(&self, mix_id: NodeId, node_stats: NodeStats) { + self.inner.write().await.node_stats.set(mix_id, node_stats); } - pub(crate) async fn set_econ_stats( - &self, - identity_key: &str, - econ_stats: EconomicDynamicsStats, - ) { - self.inner - .write() - .await - .econ_stats - .set(identity_key, econ_stats); + pub(crate) async fn set_econ_stats(&self, mix_id: NodeId, econ_stats: EconomicDynamicsStats) { + self.inner.write().await.econ_stats.set(mix_id, econ_stats); } } @@ -171,10 +161,10 @@ pub(crate) struct EconomicDynamicsStats { } fn get_common_owner(delegations: &[Delegation]) -> Option { - let owner = delegations.iter().next()?.owner(); + let owner = delegations.iter().next()?.owner.clone(); if delegations .iter() - .any(|delegation| delegation.owner() != owner) + .any(|delegation| delegation.owner != owner) { log::warn!("Unexpected different owners when summing delegations"); return None; @@ -182,16 +172,16 @@ fn get_common_owner(delegations: &[Delegation]) -> Option { Some(owner) } -fn get_common_node_identity(delegations: &[Delegation]) -> Option { - let node_identity = delegations.iter().next()?.node_identity(); +fn get_common_mix_id(delegations: &[Delegation]) -> Option { + let mix_id = delegations.iter().next()?.node_id; if delegations .iter() - .any(|delegation| delegation.node_identity() != node_identity) + .any(|delegation| delegation.node_id != mix_id) { log::warn!("Unexpected different node identities when summing delegations"); return None; } - Some(node_identity) + Some(mix_id) } fn get_common_denom(delegations: &[Delegation]) -> Option { diff --git a/explorer-api/src/mix_nodes/location.rs b/explorer-api/src/mix_nodes/location.rs index 80f24f6635..5591a2b6a7 100644 --- a/explorer-api/src/mix_nodes/location.rs +++ b/explorer-api/src/mix_nodes/location.rs @@ -2,11 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 use crate::mix_nodes::utils::map_2_letter_to_3_letter_country_code; +use mixnet_contract_common::NodeId; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::time::{Duration, SystemTime}; -pub(crate) type LocationCache = HashMap; +pub(crate) type LocationCache = HashMap; #[allow(dead_code)] #[derive(Debug, Deserialize)] diff --git a/explorer-api/src/mix_nodes/models.rs b/explorer-api/src/mix_nodes/models.rs index c07ef6ac17..e0b40a21b5 100644 --- a/explorer-api/src/mix_nodes/models.rs +++ b/explorer-api/src/mix_nodes/models.rs @@ -5,9 +5,12 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; use std::time::{Duration, SystemTime}; +use mixnet_contract_common::rewarding::helpers::truncate_reward; +use mixnet_contract_common::NodeId; use serde::Serialize; -use tokio::sync::RwLock; +use tokio::sync::{RwLock, RwLockReadGuard}; +use crate::helpers::best_effort_small_dec_to_f64; use validator_client::models::MixNodeBondAnnotated; use crate::mix_node::models::{MixnodeStatus, PrettyDetailedMixNodeBond}; @@ -30,9 +33,9 @@ pub(crate) struct MixNodeSummary { #[derive(Clone, Debug)] pub(crate) struct MixNodesResult { pub(crate) valid_until: SystemTime, - pub(crate) all_mixnodes: HashMap, - active_mixnodes: HashSet, - rewarded_mixnodes: HashSet, + pub(crate) all_mixnodes: HashMap, + active_mixnodes: HashSet, + rewarded_mixnodes: HashSet, } impl MixNodesResult { @@ -45,10 +48,10 @@ impl MixNodesResult { } } - fn determine_node_status(&self, public_key: &str) -> MixnodeStatus { - if self.active_mixnodes.contains(public_key) { + fn determine_node_status(&self, mix_id: NodeId) -> MixnodeStatus { + if self.active_mixnodes.contains(&mix_id) { MixnodeStatus::Active - } else if self.rewarded_mixnodes.contains(public_key) { + } else if self.rewarded_mixnodes.contains(&mix_id) { MixnodeStatus::Standby } else { MixnodeStatus::Inactive @@ -59,15 +62,15 @@ impl MixNodesResult { self.valid_until >= SystemTime::now() } - fn get_mixnode(&self, pubkey: &str) -> Option { + fn get_mixnode(&self, mix_id: NodeId) -> Option { if self.is_valid() { - self.all_mixnodes.get(pubkey).cloned() + self.all_mixnodes.get(&mix_id).cloned() } else { None } } - fn get_mixnodes(&self) -> Option> { + fn get_mixnodes(&self) -> Option> { if self.is_valid() { Some(self.all_mixnodes.clone()) } else { @@ -97,11 +100,11 @@ impl ThreadsafeMixNodesCache { } } - pub(crate) async fn is_location_valid(&self, identity_key: &str) -> bool { + pub(crate) async fn is_location_valid(&self, mix_id: NodeId) -> bool { self.locations .read() .await - .get(identity_key) + .get(&mix_id) .map_or(false, |cache_item| { cache_item.valid_until > SystemTime::now() }) @@ -111,48 +114,71 @@ impl ThreadsafeMixNodesCache { self.locations.read().await.clone() } - pub(crate) async fn set_location(&self, identity_key: &str, location: Option) { + pub(crate) async fn set_location(&self, mix_id: NodeId, location: Option) { // cache the location for this mix node so that it can be used when the mix node list is refreshed - self.locations.write().await.insert( - identity_key.to_string(), - LocationCacheItem::new_from_location(location), - ); + self.locations + .write() + .await + .insert(mix_id, LocationCacheItem::new_from_location(location)); } - pub(crate) async fn get_mixnode(&self, pubkey: &str) -> Option { - self.mixnodes.read().await.get_mixnode(pubkey) + pub(crate) async fn get_mixnode(&self, mix_id: NodeId) -> Option { + self.mixnodes.read().await.get_mixnode(mix_id) } - pub(crate) async fn get_mixnodes(&self) -> Option> { + pub(crate) async fn get_mixnode_by_identity( + &self, + pubkey: &str, + ) -> Option { + let all_nodes = self.get_mixnodes().await?; + for (_, node) in all_nodes { + if node.mix_node().identity_key == pubkey { + return Some(node); + } + } + None + } + + pub(crate) async fn get_mixnodes(&self) -> Option> { self.mixnodes.read().await.get_mixnodes() } - pub(crate) async fn get_detailed_mixnode_by_id( + fn create_detailed_mixnode( &self, - identity_key: &str, + mix_id: NodeId, + mixnodes_guard: &RwLockReadGuard<'_, MixNodesResult>, + location: Option<&LocationCacheItem>, + node: &MixNodeBondAnnotated, + ) -> PrettyDetailedMixNodeBond { + let denom = &node.mixnode_details.original_pledge().denom; + let rewarding_info = &node.mixnode_details.rewarding_details; + + PrettyDetailedMixNodeBond { + location: location.and_then(|l| l.location.clone()), + status: mixnodes_guard.determine_node_status(mix_id), + pledge_amount: truncate_reward(rewarding_info.operator, denom), + total_delegation: truncate_reward(rewarding_info.delegates, denom), + owner: node.mixnode_details.bond_information.owner.clone(), + layer: node.mixnode_details.bond_information.layer, + mix_node: node.mixnode_details.bond_information.mix_node.clone(), + avg_uptime: node.performance.round_to_integer(), + stake_saturation: best_effort_small_dec_to_f64(node.stake_saturation) as f32, + estimated_operator_apy: best_effort_small_dec_to_f64(node.estimated_operator_apy), + estimated_delegators_apy: best_effort_small_dec_to_f64(node.estimated_delegators_apy), + } + } + + pub(crate) async fn get_detailed_mixnode( + &self, + mix_id: NodeId, ) -> Option { let mixnodes_guard = self.mixnodes.read().await; let location_guard = self.locations.read().await; - let bond = mixnodes_guard.get_mixnode(identity_key); - let location = location_guard.get(identity_key); + let bond = mixnodes_guard.get_mixnode(mix_id); + let location = location_guard.get(&mix_id); - match bond { - Some(bond) => Some(PrettyDetailedMixNodeBond { - location: location.and_then(|l| l.location.clone()), - status: mixnodes_guard.determine_node_status(&bond.mix_node().identity_key), - pledge_amount: bond.mixnode_bond.pledge_amount, - total_delegation: bond.mixnode_bond.total_delegation, - owner: bond.mixnode_bond.owner, - layer: bond.mixnode_bond.layer, - mix_node: bond.mixnode_bond.mix_node, - avg_uptime: bond.uptime, - stake_saturation: bond.stake_saturation, - estimated_operator_apy: bond.estimated_operator_apy, - estimated_delegators_apy: bond.estimated_delegators_apy, - }), - None => None, - } + bond.map(|bond| self.create_detailed_mixnode(mix_id, &mixnodes_guard, location, &bond)) } pub(crate) async fn get_detailed_mixnodes(&self) -> Vec { @@ -163,21 +189,8 @@ impl ThreadsafeMixNodesCache { .all_mixnodes .values() .map(|bond| { - let location = location_guard.get(&bond.mix_node().identity_key); - let copy = bond.mixnode_bond.clone(); - PrettyDetailedMixNodeBond { - location: location.and_then(|l| l.location.clone()), - status: mixnodes_guard.determine_node_status(&bond.mix_node().identity_key), - pledge_amount: copy.pledge_amount, - total_delegation: copy.total_delegation, - owner: copy.owner, - layer: copy.layer, - mix_node: copy.mix_node, - avg_uptime: bond.uptime, - stake_saturation: bond.stake_saturation, - estimated_operator_apy: bond.estimated_operator_apy, - estimated_delegators_apy: bond.estimated_delegators_apy, - } + let location = location_guard.get(&bond.mix_id()); + self.create_detailed_mixnode(bond.mix_id(), &mixnodes_guard, location, bond) }) .collect() } @@ -185,13 +198,13 @@ impl ThreadsafeMixNodesCache { pub(crate) async fn update_cache( &self, all_bonds: Vec, - rewarded_nodes: HashSet, - active_nodes: HashSet, + rewarded_nodes: HashSet, + active_nodes: HashSet, ) { let mut guard = self.mixnodes.write().await; guard.all_mixnodes = all_bonds .into_iter() - .map(|bond| (bond.mix_node().identity_key.to_string(), bond)) + .map(|bond| (bond.mix_id(), bond)) .collect(); guard.rewarded_mixnodes = rewarded_nodes; guard.active_mixnodes = active_nodes; diff --git a/explorer-api/src/ping/http.rs b/explorer-api/src/ping/http.rs index 615bce29cb..8a23c4f082 100644 --- a/explorer-api/src/ping/http.rs +++ b/explorer-api/src/ping/http.rs @@ -1,14 +1,15 @@ -use std::collections::HashMap; -use std::net::{SocketAddr, ToSocketAddrs}; -use std::time::Duration; +// Copyright 2021-2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 +use mixnet_contract_common::MixNode; use rocket::serde::json::Json; use rocket::{Route, State}; use rocket_okapi::okapi::openapi3::OpenApi; use rocket_okapi::openapi_get_routes_spec; use rocket_okapi::settings::OpenApiSettings; - -use mixnet_contract_common::MixNodeBond; +use std::collections::HashMap; +use std::net::{SocketAddr, ToSocketAddrs}; +use std::time::Duration; use crate::ping::models::PingResponse; use crate::state::ExplorerApiStateContext; @@ -19,6 +20,8 @@ pub fn ping_make_default_routes(settings: &OpenApiSettings) -> (Vec, Open openapi_get_routes_spec![settings: index] } +// TODO: I'm not deprecating this one explicitly since we don't have +// a decision on whether nodes should be accessed (as in using URL) by id or identity key #[openapi(tag = "ping")] #[get("/")] pub(crate) async fn index( @@ -36,13 +39,13 @@ pub(crate) async fn index( None => { trace!("No cache value for {}", pubkey); - match state.inner.get_mix_node(pubkey).await { - Some(bond) => { + match state.inner.get_mix_node_by_pubkey(pubkey).await { + Some(node) => { // set status to pending, so that any HTTP requests are pending state.inner.ping.set_pending(pubkey).await; // do the check - let ports = Some(port_check(&bond.mixnode_bond).await); + let ports = Some(port_check(node.mix_node()).await); trace!("Tested mix node {}: {:?}", pubkey, ports); let response = PingResponse { ports, @@ -62,23 +65,23 @@ pub(crate) async fn index( } } -async fn port_check(bond: &MixNodeBond) -> HashMap { +async fn port_check(mix_node: &MixNode) -> HashMap { let mut ports: HashMap = HashMap::new(); let ports_to_test = vec![ - bond.mix_node.http_api_port, - bond.mix_node.mix_port, - bond.mix_node.verloc_port, + mix_node.http_api_port, + mix_node.mix_port, + mix_node.verloc_port, ]; trace!( "Testing mix node {} on ports {:?}...", - bond.mix_node.identity_key, + mix_node.identity_key, ports_to_test ); for port in ports_to_test { - ports.insert(port, do_port_check(&bond.mix_node.host, port).await); + ports.insert(port, do_port_check(&mix_node.host, port).await); } ports diff --git a/explorer-api/src/state.rs b/explorer-api/src/state.rs index 695354799b..e8f89dbf3f 100644 --- a/explorer-api/src/state.rs +++ b/explorer-api/src/state.rs @@ -3,6 +3,7 @@ use std::path::Path; use chrono::{DateTime, Utc}; use log::info; +use mixnet_contract_common::{IdentityKeyRef, NodeId}; use serde::{Deserialize, Serialize}; use crate::client::ThreadsafeValidatorClient; @@ -35,8 +36,15 @@ pub struct ExplorerApiState { } impl ExplorerApiState { - pub(crate) async fn get_mix_node(&self, pubkey: &str) -> Option { - self.mixnodes.get_mixnode(pubkey).await + pub(crate) async fn get_mix_node(&self, mix_id: NodeId) -> Option { + self.mixnodes.get_mixnode(mix_id).await + } + + pub(crate) async fn get_mix_node_by_pubkey( + &self, + pubkey: IdentityKeyRef<'_>, + ) -> Option { + self.mixnodes.get_mixnode_by_identity(pubkey).await } } diff --git a/explorer-api/src/tasks.rs b/explorer-api/src/tasks.rs index bac19b7ee9..08d43fb805 100644 --- a/explorer-api/src/tasks.rs +++ b/explorer-api/src/tasks.rs @@ -97,13 +97,13 @@ impl ExplorerApiTasks { .retrieve_rewarded_mixnodes() .await .into_iter() - .map(|bond| bond.mix_node().identity_key.clone()) + .map(|bond| bond.mix_id()) .collect(); let active_nodes = self .retrieve_active_mixnodes() .await .into_iter() - .map(|bond| bond.mix_node().identity_key.clone()) + .map(|bond| bond.mix_id()) .collect(); self.state .inner diff --git a/explorer-api/src/validators/models.rs b/explorer-api/src/validators/models.rs index 4f17cbe14a..cc68b58f40 100644 --- a/explorer-api/src/validators/models.rs +++ b/explorer-api/src/validators/models.rs @@ -19,7 +19,7 @@ pub(crate) struct PrettyValidatorInfo { } pub(crate) struct ValidatorCache { - pub(crate) validators: Cache, + pub(crate) validators: Cache, pub(crate) summary: ValidatorSummary, } @@ -61,7 +61,7 @@ impl ThreadsafeValidatorCache { for validator in validator_response.validators { let address = validator.address.to_string(); guard.validators.set( - address.clone().as_str(), + address.clone(), PrettyValidatorInfo { address, pub_key: validator.pub_key.to_hex(), diff --git a/explorer/src/api/constants.ts b/explorer/src/api/constants.ts index 109586c0fd..442fd68ad1 100644 --- a/explorer/src/api/constants.ts +++ b/explorer/src/api/constants.ts @@ -1,7 +1,7 @@ // master APIs export const API_BASE_URL = process.env.EXPLORER_API_URL; export const VALIDATOR_API_BASE_URL = process.env.VALIDATOR_API_URL; -export const VALIDATOR_URL = process.env.VALIDATOR_URL; +export const { VALIDATOR_URL } = process.env; export const BIG_DIPPER = process.env.BIG_DIPPER_URL; // specific API routes diff --git a/explorer/src/components/Filters/Filters.tsx b/explorer/src/components/Filters/Filters.tsx index 65ea7112dd..4bf165d52e 100644 --- a/explorer/src/components/Filters/Filters.tsx +++ b/explorer/src/components/Filters/Filters.tsx @@ -47,6 +47,7 @@ const FilterItem = ({ scale={scale} min={min} max={max} + valueLabelFormat={(val: number) => (val === 100 && id === 'stakeSaturation' ? '>100' : val)} /> ); @@ -141,11 +142,10 @@ export const Filters = () => { diff --git a/explorer/src/components/Filters/filterSchema.ts b/explorer/src/components/Filters/filterSchema.ts index b5e7fbdf89..7c097c9918 100644 --- a/explorer/src/components/Filters/filterSchema.ts +++ b/explorer/src/components/Filters/filterSchema.ts @@ -26,32 +26,19 @@ export const generateFilterSchema = (upperSaturationValue?: number) => ({ label: 'Stake saturation (%)', id: EnumFilterKey.stakeSaturation, value: [0, upperSaturationValue || 100], - marks: [ - { label: '0', value: 0 }, - - { - label: '10', - value: 10, - }, - - { - label: '50', - value: 50, - }, - { label: '90', value: 90 }, - - { - label: upperSaturationValue ? `${upperSaturationValue}` : '100', - value: upperSaturationValue || 100, - }, - ], - max: upperSaturationValue, + isSmooth: true, + marks: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, upperSaturationValue || 100].map((value) => ({ + value: value < 100 ? value : 100, + label: value < 100 ? value : '>100', + })), + max: 100, tooltipInfo: "Select nodes with <100% saturation. Any additional stake above 100% saturation won't get rewards", }, routingScore: { label: 'Routing score (%)', id: EnumFilterKey.routingScore, value: [0, 100], + isSmooth: true, marks: [ { label: '0', value: 0 }, { label: '10', value: 10 }, diff --git a/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx b/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx index be9c4c94f0..f55207b87d 100644 --- a/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx +++ b/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx @@ -32,17 +32,10 @@ const row: EconomicsInfoRowWithIndex = { }, }; -const rowVeryHighProbabilitySelection: EconomicsInfoRowWithIndex = { +const rowGoodProbabilitySelection: EconomicsInfoRowWithIndex = { ...row, selectionChance: { - value: 'Very High', - }, -}; - -const rowModerateProbabilitySelection: EconomicsInfoRowWithIndex = { - ...row, - selectionChance: { - value: 'Moderate', + value: 'Good', }, }; @@ -53,13 +46,6 @@ const rowLowProbabilitySelection: EconomicsInfoRowWithIndex = { }, }; -const rowVeryLowProbabilitySelection: EconomicsInfoRowWithIndex = { - ...row, - selectionChance: { - value: 'Very Low', - }, -}; - const emptyRow: EconomicsInfoRowWithIndex = { id: 1, selectionChance: { @@ -93,13 +79,6 @@ Empty.args = { tableName: 'storybook', }; -export const selectionChanceVeryHigh = Template.bind({}); -selectionChanceVeryHigh.args = { - rows: [rowVeryHighProbabilitySelection], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; - export const selectionChanceHigh = Template.bind({}); selectionChanceHigh.args = { rows: [row], @@ -107,9 +86,9 @@ selectionChanceHigh.args = { tableName: 'storybook', }; -export const selectionChanceModerate = Template.bind({}); -selectionChanceModerate.args = { - rows: [rowModerateProbabilitySelection], +export const selectionChanceGood = Template.bind({}); +selectionChanceGood.args = { + rows: [rowGoodProbabilitySelection], columnsData: EconomicsInfoColumns, tableName: 'storybook', }; @@ -120,10 +99,3 @@ selectionChanceLow.args = { columnsData: EconomicsInfoColumns, tableName: 'storybook', }; - -export const selectionChanceVeryLow = Template.bind({}); -selectionChanceVeryLow.args = { - rows: [rowVeryLowProbabilitySelection], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; diff --git a/explorer/src/components/MixNodes/Economics/Rows.ts b/explorer/src/components/MixNodes/Economics/Rows.ts index 0a620318d7..e4c674f551 100644 --- a/explorer/src/components/MixNodes/Economics/Rows.ts +++ b/explorer/src/components/MixNodes/Economics/Rows.ts @@ -7,13 +7,9 @@ const selectionChance = (economicDynamicsStats: ApiState { if (field === 'selectionChance') { switch (fieldValue) { case 'High': - case 'Very High': return theme.palette.nym.networkExplorer.selectionChance.overModerate; - case 'Moderate': + case 'Good': return theme.palette.nym.networkExplorer.selectionChance.moderate; case 'Low': - case 'Very Low': return theme.palette.nym.networkExplorer.selectionChance.underModerate; default: return theme.palette.nym.wallet.fee; diff --git a/explorer/src/components/MixNodes/StatusDropdown.tsx b/explorer/src/components/MixNodes/StatusDropdown.tsx index 1dba2c2f95..18263edf6e 100644 --- a/explorer/src/components/MixNodes/StatusDropdown.tsx +++ b/explorer/src/components/MixNodes/StatusDropdown.tsx @@ -46,7 +46,7 @@ export const MixNodeStatusDropdown: React.FC = ({ st } }} sx={{ - width: isMobile ? 'auto' : 200, + width: isMobile ? '50%' : 200, ...sx, }} > diff --git a/explorer/src/components/MixNodes/index.ts b/explorer/src/components/MixNodes/index.ts index 55a6b7c080..112e55163a 100644 --- a/explorer/src/components/MixNodes/index.ts +++ b/explorer/src/components/MixNodes/index.ts @@ -9,6 +9,7 @@ export type MixnodeRowType = { identity_key: string; bond: number; self_percentage: string; + pledge_amount: number; host: string; layer: string; profit_percentage: string; @@ -35,6 +36,7 @@ export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem): bond: totalBond || 0, location: item?.location?.country_name || '', self_percentage: selfPercentage, + pledge_amount: pledge, host: item?.mix_node?.host || '', layer: item?.layer || '', profit_percentage: `${profitPercentage}%`, diff --git a/explorer/src/components/TableToolbar.tsx b/explorer/src/components/TableToolbar.tsx index c6a64dd6c5..7452b4707a 100644 --- a/explorer/src/components/TableToolbar.tsx +++ b/explorer/src/components/TableToolbar.tsx @@ -30,39 +30,40 @@ export const TableToolbar: React.FC = ({ width: '100%', marginBottom: 2, display: 'flex', - flexDirection: isMobile ? 'column-reverse' : 'row', + flexDirection: isMobile ? 'column' : 'row', justifyContent: 'space-between', }} > - - {childrenBefore} - - - + + + {childrenBefore} + + = ({ placeholder="search" onChange={(event) => onChangeSearch(event.target.value)} /> + + {withFilters && } {childrenAfter} diff --git a/explorer/src/pages/Mixnodes/index.tsx b/explorer/src/pages/Mixnodes/index.tsx index 33ebb84993..3cf1d05e7b 100644 --- a/explorer/src/pages/Mixnodes/index.tsx +++ b/explorer/src/pages/Mixnodes/index.tsx @@ -188,7 +188,7 @@ export const PageMixnodes: React.FC = () => { /> ), headerClassName: 'MuiDataGrid-header-override', - width: 190, + width: 185, headerAlign: 'left', renderCell: (params: GridRenderCellParams) => ( { ), }, { - field: 'self_percentage', - headerName: 'Self %', - width: 110, + field: 'pledge_amount', + headerName: 'Bond', + width: 200, headerClassName: 'MuiDataGrid-header-override', - renderHeader: () => , + renderHeader: () => , type: 'number', headerAlign: 'left', renderCell: (params: GridRenderCellParams) => ( @@ -217,7 +217,7 @@ export const PageMixnodes: React.FC = () => { component={RRDLink} to={`/network-components/mixnode/${params.row.identity_key}`} > - {params.value}% + {currencyToString(params.value)} ), }, @@ -231,7 +231,7 @@ export const PageMixnodes: React.FC = () => { /> ), headerClassName: 'MuiDataGrid-header-override', - width: 165, + width: 160, headerAlign: 'left', renderCell: (params: GridRenderCellParams) => ( { /> ), headerClassName: 'MuiDataGrid-header-override', - width: 160, + width: 165, headerAlign: 'left', renderCell: (params: GridRenderCellParams) => ( */ -.MuiDataGrid-sortIcon, .MuiDataGrid-menuIcon { +.MuiDataGrid-sortIcon, .MuiDataGrid-menuIcon, .MuiDataGrid-iconButtonContainer{ display: none !important; } diff --git a/explorer/src/typeDefs/explorer-api.ts b/explorer/src/typeDefs/explorer-api.ts index 3463f55d98..241d656edf 100644 --- a/explorer/src/typeDefs/explorer-api.ts +++ b/explorer/src/typeDefs/explorer-api.ts @@ -215,8 +215,8 @@ export type UptimeStoryResponse = { export type MixNodeEconomicDynamicsStatsResponse = { stake_saturation: number; - active_set_inclusion_probability: 'VeryHigh' | 'High' | 'Moderate' | 'Low' | 'VeryLow'; - reserve_set_inclusion_probability: 'VeryHigh' | 'High' | 'Moderate' | 'Low' | 'VeryLow'; + active_set_inclusion_probability: 'High' | 'Good' | 'Low'; + reserve_set_inclusion_probability: 'High' | 'Good' | 'Low'; estimated_total_node_reward: number; estimated_operator_reward: number; estimated_delegators_reward: number; diff --git a/gateway/src/commands/upgrade.rs b/gateway/src/commands/upgrade.rs index cc62c5219e..7d8e5481a6 100644 --- a/gateway/src/commands/upgrade.rs +++ b/gateway/src/commands/upgrade.rs @@ -101,13 +101,13 @@ fn minor_0_12_upgrade( Version::new(0, 12, 0) }; - print_start_upgrade(&config_version, &to_version); + print_start_upgrade(config_version, &to_version); let upgraded_config = config.with_custom_version(to_version.to_string().as_ref()); upgraded_config.save_to_file(None).unwrap_or_else(|err| { eprintln!("failed to overwrite config file! - {:?}", err); - print_failed_upgrade(&config_version, &to_version); + print_failed_upgrade(config_version, &to_version); process::exit(1); }); diff --git a/gateway/src/node/client_handling/websocket/connection_handler/eth_events.rs b/gateway/src/node/client_handling/websocket/connection_handler/eth_events.rs index 917533e8ba..e60159c564 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/eth_events.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/eth_events.rs @@ -8,6 +8,7 @@ use credentials::token::bandwidth::TokenCredential; use crypto::asymmetric::identity::{PublicKey, Signature, SIGNATURE_LENGTH}; use network_defaults::{ETH_EVENT_NAME, ETH_MIN_BLOCK_DEPTH}; use std::str::FromStr; +use validator_client::nymd::traits::MixnetQueryClient; use validator_client::nymd::{AccountId, NymdClient, SigningNymdClient}; use web3::contract::tokens::Detokenize; use web3::contract::{Contract, Error}; diff --git a/mixnode/src/commands/upgrade.rs b/mixnode/src/commands/upgrade.rs index 85352e77c3..d104d47988 100644 --- a/mixnode/src/commands/upgrade.rs +++ b/mixnode/src/commands/upgrade.rs @@ -101,13 +101,13 @@ fn minor_0_12_upgrade( Version::new(0, 12, 0) }; - print_start_upgrade(&config_version, &to_version); + print_start_upgrade(config_version, &to_version); let upgraded_config = config.with_custom_version(to_version.to_string().as_ref()); upgraded_config.save_to_file(None).unwrap_or_else(|err| { eprintln!("failed to overwrite config file! - {:?}", err); - print_failed_upgrade(&config_version, &to_version); + print_failed_upgrade(config_version, &to_version); process::exit(1); }); diff --git a/mixnode/src/node/mod.rs b/mixnode/src/node/mod.rs index c27fd93037..10f70b147d 100644 --- a/mixnode/src/node/mod.rs +++ b/mixnode/src/node/mod.rs @@ -265,6 +265,8 @@ impl MixNode { // TODO: ask DH whether this function still makes sense in ^0.10 async fn check_if_same_ip_node_exists(&mut self) -> Option { + // TODO: if anything, this should be getting data directly from the contract + // as opposed to the validator API let validator_client = self.random_api_client(); let existing_nodes = match validator_client.get_cached_mixnodes().await { Ok(nodes) => nodes, @@ -281,8 +283,8 @@ impl MixNode { existing_nodes .iter() - .find(|node| node.mix_node.host == our_host) - .map(|node| node.mix_node.identity_key.clone()) + .find(|node| node.bond_information.mix_node.host == our_host) + .map(|node| node.bond_information.mix_node.identity_key.clone()) } async fn wait_for_interrupt(&self, mut shutdown: ShutdownNotifier) { diff --git a/nym-connect/Cargo.lock b/nym-connect/Cargo.lock index 91156a95cf..7b443309c5 100644 --- a/nym-connect/Cargo.lock +++ b/nym-connect/Cargo.lock @@ -57,28 +57,37 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" dependencies = [ "memchr", ] [[package]] name = "alloc-no-stdlib" -version = "2.0.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ef4730490ad1c4eae5c4325b2a95f521d023e5c885853ff7aca0a6a1631db3" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" [[package]] name = "alloc-stdlib" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -90,9 +99,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.58" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" +checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" [[package]] name = "arrayref" @@ -114,9 +123,9 @@ checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" [[package]] name = "async-trait" -version = "0.1.56" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" +checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" dependencies = [ "proc-macro2", "quote", @@ -158,20 +167,18 @@ dependencies = [ [[package]] name = "attohttpc" -version = "0.19.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262c3f7f5d61249d8c00e5546e2685cd15ebeeb1bc0f3cc5449350a1cb07319e" +checksum = "1fcf00bc6d5abb29b5f97e3c61a90b6d3caa12f3faf897d4a3e3607c050a35a7" dependencies = [ "flate2", "http", "log", "native-tls", - "openssl", "serde", "serde_json", "serde_urlencoded", "url", - "wildmatch", ] [[package]] @@ -200,12 +207,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "az" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f771a5d1f5503f7f4279a30f3643d3421ba149848b89ecaaec0ea2acf04a5ac4" - [[package]] name = "base16ct" version = "0.1.1" @@ -220,9 +221,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "base64ct" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bdca834647821e0b13d9539a8634eb62d3501b6b6c2cec1722786ee6671b851" +checksum = "ea2b2456fd614d856680dcd9fcc660a51a820fa09daef2e49772b56a193c8474" [[package]] name = "bip32" @@ -235,7 +236,7 @@ dependencies = [ "k256", "once_cell", "pbkdf2", - "rand_core 0.6.3", + "rand_core 0.6.4", "ripemd160", "sha2 0.9.9", "subtle 2.4.1", @@ -302,7 +303,7 @@ dependencies = [ "cc", "cfg-if", "constant_time_eq", - "digest 0.10.3", + "digest 0.10.5", ] [[package]] @@ -311,44 +312,23 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" -[[package]] -name = "block-buffer" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" -dependencies = [ - "block-padding 0.1.5", - "byte-tools", - "byteorder", - "generic-array 0.12.4", -] - [[package]] name = "block-buffer" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "block-padding 0.2.1", - "generic-array 0.14.5", + "block-padding", + "generic-array 0.14.6", ] [[package]] name = "block-buffer" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" dependencies = [ - "generic-array 0.14.5", -] - -[[package]] -name = "block-padding" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" -dependencies = [ - "byte-tools", + "generic-array 0.14.6", ] [[package]] @@ -367,7 +347,7 @@ dependencies = [ "ff 0.10.1", "group 0.10.0", "pairing", - "rand_core 0.6.3", + "rand_core 0.6.4", "subtle 2.4.1", ] @@ -412,9 +392,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.10.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" [[package]] name = "byte-slice-cast" @@ -430,9 +410,9 @@ checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" [[package]] name = "bytemuck" -version = "1.10.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c53dfa917ec274df8ed3c572698f381a24eef2efba9492d797301b72b6db408a" +checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da" [[package]] name = "byteorder" @@ -442,9 +422,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" [[package]] name = "cairo-rs" @@ -472,9 +452,9 @@ dependencies = [ [[package]] name = "cargo_toml" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5809dd3e6444651fd1cdd3dbec71eca438c439a0fcc8081674a14da0afe50185" +checksum = "a4419e9adae9fd7e231b60d50467481bf8181ddeef6ed54683b23ae925c74c9c" dependencies = [ "serde", "serde_derive", @@ -542,14 +522,16 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.19" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" dependencies = [ - "libc", + "iana-time-zone", + "js-sys", "num-integer", "num-traits", "time 0.1.44", + "wasm-bindgen", "winapi", ] @@ -559,7 +541,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" dependencies = [ - "generic-array 0.14.5", + "generic-array 0.14.6", ] [[package]] @@ -574,9 +556,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.8" +version = "3.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190814073e85d238f31ff738fcb0bf6910cedeb73376c87cd69291028966fd83" +checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750" dependencies = [ "atty", "bitflags", @@ -591,9 +573,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.2.7" +version = "3.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759bf187376e1afa7b85b959e6a664a3e7a95203415dba952ad19139e798f902" +checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" dependencies = [ "heck 0.4.0", "proc-macro-error", @@ -718,9 +700,9 @@ dependencies = [ [[package]] name = "combine" -version = "4.6.4" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a604e93b79d1808327a6fca85a6f2d69de66461e7620f5a4cbf5fb4d1d7c948" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" dependencies = [ "bytes", "memchr", @@ -756,6 +738,7 @@ name = "contracts-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "serde", ] [[package]] @@ -828,7 +811,7 @@ dependencies = [ "k256", "prost", "prost-types", - "rand_core 0.6.3", + "rand_core 0.6.4", "serde", "serde_json", "subtle-encoding", @@ -846,15 +829,15 @@ dependencies = [ "digest 0.9.0", "ed25519-zebra", "k256", - "rand_core 0.6.3", + "rand_core 0.6.4", "thiserror", ] [[package]] name = "cosmwasm-derive" -version = "1.0.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b36e527620a2a3e00e46b6e731ab6c9b68d11069c986f7d7be8eba79ef081a4" +checksum = "0faf3a02389f78d6173b7e680751205015d5406f8abbaa9aa36fd216adc9f10d" dependencies = [ "syn", ] @@ -878,9 +861,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" dependencies = [ "libc", ] @@ -935,9 +918,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" dependencies = [ "cfg-if", "crossbeam-utils", @@ -945,9 +928,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07db9d94cbd326813772c968ccd25999e5f8ae22f4f8d1b11effa37ef6ce281d" +checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" dependencies = [ "autocfg 1.1.0", "cfg-if", @@ -959,9 +942,9 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f25d8400f4a7a5778f0e4e52384a48cbd9b5c495d110786187fc750075277a2" +checksum = "1cd42583b04998a5363558e5f9291ee5a5ff6b49944332103f251e7479a82aa7" dependencies = [ "cfg-if", "crossbeam-utils", @@ -969,9 +952,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83" +checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" dependencies = [ "cfg-if", "once_cell", @@ -992,9 +975,9 @@ dependencies = [ "bs58", "cipher 0.4.3", "ctr 0.9.1", - "digest 0.10.3", + "digest 0.10.5", "ed25519-dalek", - "generic-array 0.14.5", + "generic-array 0.14.6", "hkdf 0.12.3", "hmac 0.12.1", "nymsphinx-types", @@ -1010,19 +993,19 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" dependencies = [ - "generic-array 0.14.5", - "rand_core 0.6.3", + "generic-array 0.14.6", + "rand_core 0.6.4", "subtle 2.4.1", "zeroize", ] [[package]] name = "crypto-common" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5999502d32b9c48d492abe66392408144895020ec4709e549e840799f3bb74c0" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array 0.14.5", + "generic-array 0.14.6", "typenum", ] @@ -1042,7 +1025,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" dependencies = [ - "generic-array 0.14.5", + "generic-array 0.14.6", "subtle 2.4.1", ] @@ -1084,9 +1067,9 @@ dependencies = [ [[package]] name = "ctor" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c" +checksum = "cdffe87e1d521a10f9696f833fe502293ea446d7f256c06128293a4119bdf4cb" dependencies = [ "quote", "syn", @@ -1221,15 +1204,6 @@ dependencies = [ "byteorder", ] -[[package]] -name = "deflate" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f" -dependencies = [ - "adler32", -] - [[package]] name = "der" version = "0.5.1" @@ -1267,16 +1241,16 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.5", + "generic-array 0.14.6", ] [[package]] name = "digest" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" dependencies = [ - "block-buffer 0.10.2", + "block-buffer 0.10.3", "crypto-common", "subtle 2.4.1", ] @@ -1357,9 +1331,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "140206b78fb2bc3edbcfc9b5ccbd0b30699cfe8d348b8b31b330e47df5291a5a" +checksum = "4f94fa09c2aeea5b8839e414b7b841bf429fd25b9c522116ac97ee87856d88b2" [[package]] name = "ecdsa" @@ -1404,7 +1378,7 @@ checksum = "403ef3e961ab98f0ba902771d29f842058578bb1ce7e3c59dad5a6a93e784c69" dependencies = [ "curve25519-dalek", "hex", - "rand_core 0.6.3", + "rand_core 0.6.4", "serde", "sha2 0.9.9", "thiserror", @@ -1413,9 +1387,9 @@ dependencies = [ [[package]] name = "either" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "elliptic-curve" @@ -1427,27 +1401,14 @@ dependencies = [ "crypto-bigint", "der", "ff 0.11.1", - "generic-array 0.14.5", + "generic-array 0.14.6", "group 0.11.0", - "rand_core 0.6.3", + "rand_core 0.6.4", "sec1", "subtle 2.4.1", "zeroize", ] -[[package]] -name = "embed-resource" -version = "1.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc24ff8d764818e9ab17963b0593c535f077a513f565e75e4352d758bc4d8c0" -dependencies = [ - "cc", - "rustc_version 0.4.0", - "toml", - "vswhom", - "winreg", -] - [[package]] name = "embed_plist" version = "1.2.2" @@ -1541,9 +1502,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "2.5.2" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "execute" @@ -1563,17 +1524,11 @@ dependencies = [ "once_cell", ] -[[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" - [[package]] name = "fastrand" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" dependencies = [ "instant", ] @@ -1584,7 +1539,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0f40b2dcd8bc322217a5f6559ae5f9e9d1de202a2ecee2e9eafcbece7562a4f" dependencies = [ - "rand_core 0.6.3", + "rand_core 0.6.4", "subtle 2.4.1", ] @@ -1594,7 +1549,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "131655483be284720a17d74ff97592b8e76576dc25563148601df2d7c9080924" dependencies = [ - "rand_core 0.6.3", + "rand_core 0.6.4", "subtle 2.4.1", ] @@ -1629,19 +1584,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "fixed" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93db1ca4ef0496cc54f43660fcb423bc16ced939b8fdd9749e7abcd5674f1803" -dependencies = [ - "az", - "bytemuck", - "half", - "serde", - "typenum", -] - [[package]] name = "fixed-hash" version = "0.7.0" @@ -1676,14 +1618,14 @@ dependencies = [ [[package]] name = "flume" -version = "0.10.13" +version = "0.10.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ceeb589a3157cac0ab8cc585feb749bd2cea5cb55a6ee802ad72d9fd38303da" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" dependencies = [ "futures-core", "futures-sink", "pin-project", - "spin 0.9.3", + "spin 0.9.4", ] [[package]] @@ -1724,11 +1666,10 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" dependencies = [ - "matches", "percent-encoding", ] @@ -1772,9 +1713,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c" dependencies = [ "futures-channel", "futures-core", @@ -1787,9 +1728,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050" dependencies = [ "futures-core", "futures-sink", @@ -1797,15 +1738,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" [[package]] name = "futures-executor" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab" dependencies = [ "futures-core", "futures-task", @@ -1825,30 +1766,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" - -[[package]] -name = "futures-lite" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] +checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" [[package]] name = "futures-macro" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" dependencies = [ "proc-macro2", "quote", @@ -1857,15 +1783,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" +checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" [[package]] name = "futures-task" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" +checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" [[package]] name = "futures-timer" @@ -1875,9 +1801,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" dependencies = [ "futures-channel", "futures-core", @@ -2023,15 +1949,15 @@ dependencies = [ [[package]] name = "generator" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1d9279ca822891c1a4dae06d185612cf8fc6acfe5dff37781b41297811b12ee" +checksum = "cc184cace1cea8335047a471cc1da80f18acf8a76f3bab2028d499e328948ec7" dependencies = [ "cc", "libc", "log", "rustversion", - "winapi", + "windows 0.32.0", ] [[package]] @@ -2045,9 +1971,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" dependencies = [ "typenum", "version_check", @@ -2123,9 +2049,9 @@ dependencies = [ [[package]] name = "git2" -version = "0.14.4" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0155506aab710a86160ddb504a480d2964d7ab5b9e62419be69e0032bc5931c" +checksum = "2994bee4a3a6a51eb90c218523be382fd7ea09b16380b9312e9dbe955ff7c7d1" dependencies = [ "bitflags", "libc", @@ -2217,7 +2143,7 @@ checksum = "1c363a5301b8f153d80747126a04b3c82073b9fe3130571a9d170cacdeaf7912" dependencies = [ "byteorder", "ff 0.10.1", - "rand_core 0.6.3", + "rand_core 0.6.4", "subtle 2.4.1", ] @@ -2228,7 +2154,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89" dependencies = [ "ff 0.11.1", - "rand_core 0.6.3", + "rand_core 0.6.4", "subtle 2.4.1", ] @@ -2289,9 +2215,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" +checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be" dependencies = [ "bytes", "fnv", @@ -2302,16 +2228,10 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util 0.7.3", + "tokio-util 0.7.4", "tracing", ] -[[package]] -name = "half" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" - [[package]] name = "handlebars" version = "3.5.5" @@ -2337,9 +2257,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashlink" @@ -2352,9 +2272,9 @@ dependencies = [ [[package]] name = "headers" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" +checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" dependencies = [ "base64", "bitflags", @@ -2363,7 +2283,7 @@ dependencies = [ "http", "httpdate", "mime", - "sha-1 0.10.0", + "sha1", ] [[package]] @@ -2449,7 +2369,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.3", + "digest 0.10.5", ] [[package]] @@ -2474,7 +2394,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", - "itoa 1.0.2", + "itoa 1.0.3", ] [[package]] @@ -2496,9 +2416,9 @@ checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" [[package]] name = "httparse" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" @@ -2546,7 +2466,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.2", + "itoa 1.0.3", "pin-project-lite", "socket2", "tokio", @@ -2605,6 +2525,20 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "237a0714f28b1ee39ccec0770ccb544eb02c9ef2c82bb096230eefcffa6468b0" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "js-sys", + "once_cell", + "wasm-bindgen", + "winapi", +] + [[package]] name = "ico" version = "0.1.0" @@ -2652,14 +2586,13 @@ dependencies = [ [[package]] name = "image" -version = "0.24.2" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28edd9d7bc256be2502e325ac0628bde30b7001b9b52e0abe31a1a9dc2701212" +checksum = "7e30ca2ecf7666107ff827a8e481de6a132a9b687ed3bb20bb1c144a36c00964" dependencies = [ "bytemuck", "byteorder", "color_quant", - "num-iter", "num-rational", "num-traits", ] @@ -2715,7 +2648,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg 1.1.0", - "hashbrown 0.12.1", + "hashbrown 0.12.3", "serde", ] @@ -2743,7 +2676,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ - "generic-array 0.14.5", + "generic-array 0.14.6", ] [[package]] @@ -2775,9 +2708,9 @@ checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" [[package]] name = "itertools" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +checksum = "d8bf247779e67a9082a4790b45e71ac7cfd1321331a5c856a74a9faebdab78d0" dependencies = [ "either", ] @@ -2790,9 +2723,9 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" [[package]] name = "javascriptcore-rs" @@ -2817,20 +2750,6 @@ dependencies = [ "system-deps 5.0.0", ] -[[package]] -name = "jni" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24967112a1e4301ca5342ea339763613a37592b8a6ce6cf2e4494537c7a42faf" -dependencies = [ - "cesu8", - "combine", - "jni-sys", - "log", - "thiserror", - "walkdir", -] - [[package]] name = "jni" version = "0.19.0" @@ -2965,15 +2884,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.126" +version = "0.2.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" [[package]] name = "libgit2-sys" -version = "0.13.4+1.4.2" +version = "0.14.0+1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0fa6563431ede25f5cc7f6d803c6afbc1c5d3ad3d4925d12c882bf2b526f5d1" +checksum = "47a00859c70c8a4f7218e6d1cc32875c4b55f6799445b842b0d8ed5e4c3d959b" dependencies = [ "cc", "libc", @@ -2993,9 +2912,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db" +checksum = "292a948cd991e376cf75541fe5b97a1081d713c618b4f1b9500f8844e49eb565" [[package]] name = "libsqlite3-sys" @@ -3043,9 +2962,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +checksum = "9f80bf5aacaf25cbfc8210d1cfb718f2bf3b11c4c54e5afe36c236853a8ec390" dependencies = [ "autocfg 1.1.0", "scopeguard", @@ -3090,12 +3009,6 @@ dependencies = [ "libc", ] -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - [[package]] name = "markup5ever" version = "0.10.1" @@ -3166,9 +3079,9 @@ checksum = "933dca44d65cdd53b355d0b73d380a2ff5da71f87f036053188bf1eab6a19881" [[package]] name = "miniz_oxide" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" dependencies = [ "adler", ] @@ -3189,16 +3102,16 @@ dependencies = [ name = "mixnet-contract-common" version = "0.1.0" dependencies = [ - "az", + "bs58", "contracts-common", "cosmwasm-std", - "fixed", "log", "schemars", "serde", + "serde_json", "serde_repr", "thiserror", - "time 0.3.11", + "time 0.3.14", ] [[package]] @@ -3300,7 +3213,7 @@ version = "0.1.0" dependencies = [ "tokio", "tokio-stream", - "tokio-util 0.7.3", + "tokio-util 0.7.4", ] [[package]] @@ -3578,7 +3491,7 @@ dependencies = [ "bytes", "nymsphinx-params", "nymsphinx-types", - "tokio-util 0.7.3", + "tokio-util 0.7.4", ] [[package]] @@ -3637,9 +3550,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.13.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" +checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" [[package]] name = "opaque-debug" @@ -3655,9 +3568,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "open" -version = "3.0.1" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "360bcc8316bf6363aa3954c3ccc4de8add167b087e0259190a043c9514f910fe" +checksum = "b4a3100141f1733ea40b53381b0ae3117330735ef22309a190ac57b9576ea716" dependencies = [ "pathdiff", "windows-sys", @@ -3665,9 +3578,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.40" +version = "0.10.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e" +checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0" dependencies = [ "bitflags", "cfg-if", @@ -3697,9 +3610,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.74" +version = "0.9.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1" +checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f" dependencies = [ "autocfg 1.1.0", "cc", @@ -3717,9 +3630,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.1.0" +version = "6.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" +checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" [[package]] name = "pairing" @@ -3781,12 +3694,6 @@ dependencies = [ "syn", ] -[[package]] -name = "parking" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" - [[package]] name = "parking_lot" version = "0.11.2" @@ -3837,9 +3744,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" +checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1" [[package]] name = "pathdiff" @@ -3903,24 +3810,25 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pest" -version = "2.1.3" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +checksum = "cb779fcf4bb850fbbb0edc96ff6cf34fd90c4b1a112ce042653280d9a7364048" dependencies = [ + "thiserror", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.1.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" +checksum = "502b62a6d0245378b04ffe0a7fb4f4419a4815fce813bd8a0ec89a56e07d67b1" dependencies = [ "pest", "pest_generator", @@ -3928,9 +3836,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.1.3" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" +checksum = "451e629bf49b750254da26132f1a5a9d11fd8a95a3df51d15c4abd1ba154cb6c" dependencies = [ "pest", "pest_meta", @@ -3941,13 +3849,13 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.1.3" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" +checksum = "bcec162c71c45e269dfc3fc2916eaeb97feab22993a21bcce4721d08cd7801a6" dependencies = [ - "maplit", + "once_cell", "pest", - "sha-1 0.8.2", + "sha1", ] [[package]] @@ -4050,18 +3958,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78203e83c48cffbe01e4a2d35d566ca4de445d79a85372fc64e378bfc812a260" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "710faf75e1b33345361201d36d04e98ac1ed8909151a017ed384700836104c74" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ "proc-macro2", "quote", @@ -4107,7 +4015,7 @@ dependencies = [ "indexmap", "line-wrap", "serde", - "time 0.3.11", + "time 0.3.14", "xml-rs", ] @@ -4118,20 +4026,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0b0cabbbd20c2d7f06dbf015e06aad59b6ca3d9ed14848783e98af9aaf19925" dependencies = [ "bitflags", - "deflate 0.7.20", + "deflate", "inflate", "num-iter", ] [[package]] name = "png" -version = "0.17.5" +version = "0.17.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc38c0ad57efb786dd57b9864e5b18bae478c00c824dc55a38bbc9da95dde3ba" +checksum = "8f0e7f4c94ec26ff209cee506314212639d6c91b80afb82984819fafce9df01c" dependencies = [ "bitflags", "crc32fast", - "deflate 1.0.0", + "flate2", "miniz_oxide", ] @@ -4172,10 +4080,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.1.3" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" +checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" dependencies = [ + "once_cell", "thiserror", "toml", ] @@ -4212,9 +4121,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro2" -version = "1.0.40" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" dependencies = [ "unicode-ident", ] @@ -4263,7 +4172,7 @@ dependencies = [ "socks5-requests", "task", "tokio", - "tokio-util 0.7.3", + "tokio-util 0.7.4", ] [[package]] @@ -4280,9 +4189,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] @@ -4334,7 +4243,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -4364,7 +4273,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -4393,9 +4302,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom 0.2.7", ] @@ -4492,9 +4401,9 @@ dependencies = [ [[package]] name = "raw-window-handle" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b800beb9b6e7d2df1fe337c9e3d04e3af22a124460fb4c30fcc22c9117cefb41" +checksum = "ed7e3d950b66e19e0c372f3fa3fbbcf85b1746b571f74e0c2af6042a5c93420a" dependencies = [ "cty", ] @@ -4510,9 +4419,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] @@ -4613,13 +4522,12 @@ dependencies = [ [[package]] name = "rfd" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f121348fd3b9035ed11be1f028e8944263c30641f8c5deacf57a4320782fb402" +checksum = "0149778bd99b6959285b0933288206090c50e2327f47a9c463bfdbf45c8823ea" dependencies = [ "block", "dispatch", - "embed-resource", "glib-sys", "gobject-sys", "gtk-sys", @@ -4633,7 +4541,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows", + "windows 0.37.0", ] [[package]] @@ -4693,7 +4601,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.12", + "semver 1.0.14", ] [[package]] @@ -4723,15 +4631,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0a5f7c728f5d284929a1cccb5bc19884422bfe6ef4d6c409da2c41838983fcf" +checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" [[package]] name = "ryu" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" [[package]] name = "safemem" @@ -4812,7 +4720,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08da66b8b0965a5555b6bd6639e68ccba85e1e2506f5fbb089e93f8a04e1a2d1" dependencies = [ "der", - "generic-array 0.14.5", + "generic-array 0.14.6", "pkcs8", "subtle 2.4.1", "zeroize", @@ -4838,9 +4746,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" dependencies = [ "bitflags", "core-foundation", @@ -4890,9 +4798,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1" +checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" dependencies = [ "serde", ] @@ -4908,9 +4816,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.138" +version = "1.0.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1578c6245786b9d168c5447eeacfb96856573ca56c9d68fdcf394be134882a47" +checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" dependencies = [ "serde_derive", ] @@ -4926,18 +4834,18 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.6" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212e73464ebcde48d723aa02eb270ba62eff38a9b732df31f33f1b4e145f3a54" +checksum = "cfc50e8183eeeb6178dcb167ae34a8051d63535023ae38b5d8d12beae193d37b" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.138" +version = "1.0.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "023e9b1467aef8a10fb88f25611870ada9800ef7e22afce356bb0d2387b6f27c" +checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" dependencies = [ "proc-macro2", "quote", @@ -4957,20 +4865,20 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.82" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" dependencies = [ - "itoa 1.0.2", + "itoa 1.0.3", "ryu", "serde", ] [[package]] name = "serde_repr" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2ad84e47328a31223de7fed7a4f5087f2d6ddfe586cf3ca25b7a165bc0a5aed" +checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" dependencies = [ "proc-macro2", "quote", @@ -4984,7 +4892,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.2", + "itoa 1.0.3", "ryu", "serde", ] @@ -5043,18 +4951,6 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "sha-1" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" -dependencies = [ - "block-buffer 0.7.3", - "digest 0.8.1", - "fake-simd", - "opaque-debug 0.2.3", -] - [[package]] name = "sha-1" version = "0.9.8" @@ -5069,14 +4965,14 @@ dependencies = [ ] [[package]] -name = "sha-1" -version = "0.10.0" +name = "sha1" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.3", + "digest 0.10.5", ] [[package]] @@ -5094,13 +4990,13 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.2" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.3", + "digest 0.10.5", ] [[package]] @@ -5140,7 +5036,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02658e48d89f2bec991f9a78e69cfa4c316f8d6a6c4ec12fae1aeb263d486788" dependencies = [ "digest 0.9.0", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -5151,9 +5047,12 @@ checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" [[package]] name = "slab" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg 1.1.0", +] [[package]] name = "sled" @@ -5209,9 +5108,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.4.4" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" dependencies = [ "libc", "winapi", @@ -5237,7 +5136,7 @@ dependencies = [ "httparse", "log", "rand 0.8.5", - "sha-1 0.9.8", + "sha-1", ] [[package]] @@ -5299,9 +5198,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spin" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c530c2b0d0bf8b69304b39fe2001993e267461948b890cd037d8ad4293fa1a0d" +checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09" dependencies = [ "lock_api", ] @@ -5361,7 +5260,7 @@ dependencies = [ "hashlink", "hex", "indexmap", - "itoa 1.0.2", + "itoa 1.0.3", "libc", "libsqlite3-sys", "log", @@ -5370,7 +5269,7 @@ dependencies = [ "paste", "percent-encoding", "rustls", - "sha2 0.10.2", + "sha2 0.10.6", "smallvec 1.9.0", "sqlformat", "sqlx-rt", @@ -5394,7 +5293,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "sha2 0.10.2", + "sha2 0.10.6", "sqlx-core", "sqlx-rt", "syn", @@ -5507,9 +5406,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.98" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e" dependencies = [ "proc-macro2", "quote", @@ -5556,9 +5455,9 @@ dependencies = [ [[package]] name = "tao" -version = "0.12.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a71c32c2fa7bba46b01becf9cf470f6a781573af7e376c5e317a313ecce27545" +checksum = "43336f5d1793543ba96e2a1e75f3a5c7dcd592743be06a0ab3a190f4fcb4b934" dependencies = [ "bitflags", "cairo-rs", @@ -5579,7 +5478,7 @@ dependencies = [ "gtk", "image", "instant", - "jni 0.19.0", + "jni", "lazy_static", "libappindicator", "libc", @@ -5589,15 +5488,15 @@ dependencies = [ "ndk-sys", "objc", "once_cell", - "parking_lot 0.11.2", + "parking_lot 0.12.1", "paste", - "png 0.17.5", + "png 0.17.6", "raw-window-handle", "scopeguard", "serde", "unicode-segmentation", - "uuid 0.8.2", - "windows", + "uuid 1.1.2", + "windows 0.39.0", "windows-implement", "x11-dl", ] @@ -5629,9 +5528,9 @@ dependencies = [ [[package]] name = "tauri" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61fc211e0bd2c04c0aecd202d2cd72dd797a89da02989a39e1b9691462386d6" +checksum = "efbf22abd61d95ca9b2becd77f9db4c093892f73e8a07d21d8b0b2bf71a7bcea" dependencies = [ "anyhow", "attohttpc", @@ -5639,9 +5538,9 @@ dependencies = [ "cocoa", "dirs-next", "embed_plist", + "encoding_rs", "flate2", - "futures", - "futures-lite", + "futures-util", "glib", "glob", "gtk", @@ -5657,7 +5556,7 @@ dependencies = [ "raw-window-handle", "regex", "rfd", - "semver 1.0.12", + "semver 1.0.14", "serde", "serde_json", "serde_repr", @@ -5670,27 +5569,27 @@ dependencies = [ "tauri-utils", "tempfile", "thiserror", - "time 0.3.11", + "time 0.3.14", "tokio", "url", "uuid 1.1.2", "webkit2gtk", "webview2-com", - "windows", + "windows 0.39.0", "zip", ] [[package]] name = "tauri-build" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f2b32e551ec810ba4ab2ad735de5e3576e54bf0322ab0f4b7ce41244bc65ecf" +checksum = "0991fb306849897439dbd4a72e4cbed2413e2eb26cb4b3ba220b94edba8b4b88" dependencies = [ "anyhow", "cargo_toml", "heck 0.4.0", "json-patch", - "semver 1.0.12", + "semver 1.0.14", "serde_json", "tauri-utils", "winres", @@ -5698,35 +5597,35 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f1f7928dd040fc03c94207adfad506c0cf5b152982fd1dc0a621f7fd777e22" +checksum = "356fa253e40ae4d6ff02075011f2f2bb4066f5c9d8c1e16ca6912d7b75903ba6" dependencies = [ "base64", "brotli", "ico", "json-patch", "plist", - "png 0.17.5", + "png 0.17.6", "proc-macro2", "quote", "regex", - "semver 1.0.12", + "semver 1.0.14", "serde", "serde_json", - "sha2 0.10.2", + "sha2 0.10.6", "tauri-utils", "thiserror", - "time 0.3.11", + "time 0.3.14", "uuid 1.1.2", "walkdir", ] [[package]] name = "tauri-macros" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e50b9f52871c088857360319a37472d59f4644f1ed004489599d62831a1b6996" +checksum = "d6051fd6940ddb22af452340d03c66a3e2f5d72e0788d4081d91e31528ccdc4d" dependencies = [ "heck 0.4.0", "proc-macro2", @@ -5738,14 +5637,15 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "0.10.2" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4cff3b4d9469727fa2107c4b3d2eda110df1ba45103fb420178e536362fae4" +checksum = "d49439a5ea47f474572b854972f42eda2e02a470be5ca9609cc83bb66945abe2" dependencies = [ "gtk", "http", "http-range", "infer", + "rand 0.8.5", "raw-window-handle", "serde", "serde_json", @@ -5753,14 +5653,14 @@ dependencies = [ "thiserror", "uuid 1.1.2", "webview2-com", - "windows", + "windows 0.39.0", ] [[package]] name = "tauri-runtime-wry" -version = "0.10.2" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fa8c4edaf01d8b556e7172c844b1b4dd3399adcd1a606bd520fc3e65f698546" +checksum = "28dce920995fd49907aa9bea7249ed1771454f11f7611924c920a1f75fb614d4" dependencies = [ "cocoa", "gtk", @@ -5772,15 +5672,15 @@ dependencies = [ "uuid 1.1.2", "webkit2gtk", "webview2-com", - "windows", + "windows 0.39.0", "wry", ] [[package]] name = "tauri-utils" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12ff4b68d9faeb57c9c727bf58c9c9768d2b67d8e84e62ce6146e7859a2e9c6b" +checksum = "1e8fdae6f29cef959809a3c3afef510c5b715a446a597ab8b791497585363f39" dependencies = [ "brotli", "ctor", @@ -5793,14 +5693,14 @@ dependencies = [ "phf 0.10.1", "proc-macro2", "quote", - "semver 1.0.12", + "semver 1.0.14", "serde", "serde_json", "serde_with", "thiserror", "url", "walkdir", - "windows", + "windows 0.39.0", ] [[package]] @@ -5844,7 +5744,7 @@ dependencies = [ "subtle 2.4.1", "subtle-encoding", "tendermint-proto", - "time 0.3.11", + "time 0.3.14", "zeroize", ] @@ -5877,7 +5777,7 @@ dependencies = [ "serde", "serde_bytes", "subtle-encoding", - "time 0.3.11", + "time 0.3.14", ] [[package]] @@ -5905,7 +5805,7 @@ dependencies = [ "tendermint-config", "tendermint-proto", "thiserror", - "time 0.3.11", + "time 0.3.14", "tokio", "tracing", "url", @@ -5935,9 +5835,9 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" [[package]] name = "thin-slice" @@ -5947,18 +5847,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.34" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c1b05ca9d106ba7d2e31a9dab4a64e7be2cce415321966ea3132c49a656e252" +checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.34" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8f2591983642de85c921015f3f070c665a197ed69e417af436115e3a1407487" +checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783" dependencies = [ "proc-macro2", "quote", @@ -5987,11 +5887,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.11" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" +checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" dependencies = [ - "itoa 1.0.2", + "itoa 1.0.3", "libc", "num_threads", "time-macros", @@ -6014,10 +5914,11 @@ dependencies = [ [[package]] name = "tokio" -version = "1.19.2" +version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439" +checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95" dependencies = [ + "autocfg 1.1.0", "bytes", "libc", "memchr", @@ -6066,14 +5967,14 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9" +checksum = "f6edf2d6bc038a43d31353570e27270603f4648d18f5ed10c0e179abe43255af" dependencies = [ "futures-core", "pin-project-lite", "tokio", - "tokio-util 0.7.3", + "tokio-util 0.7.4", ] [[package]] @@ -6106,9 +6007,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" +checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" dependencies = [ "bytes", "futures-core", @@ -6150,9 +6051,9 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" +checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" dependencies = [ "cfg-if", "pin-project-lite", @@ -6173,9 +6074,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7" +checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" dependencies = [ "once_cell", "valuable", @@ -6194,9 +6095,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a713421342a5a666b7577783721d3117f1b69a393df803ee17bb73b1e122a59" +checksum = "60db860322da191b40952ad9affe65ea23e7dd6a5c442c2c42865810c6ab8e6b" dependencies = [ "ansi_term", "matchers", @@ -6262,7 +6163,7 @@ dependencies = [ "input_buffer", "log", "rand 0.8.5", - "sha-1 0.9.8", + "sha-1", "thiserror", "url", "utf-8", @@ -6276,9 +6177,9 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "ucd-trie" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89570599c4fe5585de2b388aab47e99f7fa4e9238a1399f707a02e356058141c" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" [[package]] name = "uint" @@ -6300,9 +6201,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" +checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" [[package]] name = "unicode-normalization" @@ -6315,15 +6216,15 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" +checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" [[package]] name = "unicode-xid" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "unicode_categories" @@ -6339,13 +6240,12 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.2.2" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "22fe195a4f217c25b25cb5058ced57059824a678474874038dc88d211bf508d3" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", "serde", ] @@ -6367,9 +6267,6 @@ name = "uuid" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" -dependencies = [ - "getrandom 0.2.7", -] [[package]] name = "uuid" @@ -6387,6 +6284,7 @@ dependencies = [ "bs58", "coconut-interface", "cosmrs", + "cosmwasm-std", "getset", "mixnet-contract-common", "schemars", @@ -6484,7 +6382,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vesting-contract" -version = "1.0.2" +version = "1.1.0" dependencies = [ "cosmwasm-std", "cw-storage-plus", @@ -6505,26 +6403,6 @@ dependencies = [ "serde", ] -[[package]] -name = "vswhom" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" -dependencies = [ - "libc", - "vswhom-sys", -] - -[[package]] -name = "vswhom-sys" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22025f6d8eb903ebf920ea6933b70b1e495be37e2cb4099e62c80454aaf57c39" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "vte" version = "0.10.1" @@ -6546,12 +6424,6 @@ dependencies = [ "quote", ] -[[package]] -name = "waker-fn" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" - [[package]] name = "walkdir" version = "2.3.2" @@ -6793,13 +6665,13 @@ dependencies = [ [[package]] name = "webview2-com" -version = "0.16.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a489a9420acabb3c2ed0434b6f71f6b56b9485ec32665a28dec1ee186d716e0f" +checksum = "b4a769c9f1a64a8734bde70caafac2b96cada12cd4aefa49196b3a386b8b4178" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows", + "windows 0.39.0", "windows-implement", ] @@ -6816,24 +6688,19 @@ dependencies = [ [[package]] name = "webview2-com-sys" -version = "0.16.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0258c53ee9adc0a4f8ba1c8c317588f7a58c7048a55b621d469ba75ab3709ca1" +checksum = "aac48ef20ddf657755fdcda8dfed2a7b4fc7e4581acce6fe9b88c3d64f29dee7" dependencies = [ "regex", "serde", "serde_json", "thiserror", - "windows", + "windows 0.39.0", "windows-bindgen", + "windows-metadata", ] -[[package]] -name = "wildmatch" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee583bdc5ff1cf9db20e9db5bb3ff4c3089a8f6b8b31aff265c9aba85812db86" - [[package]] name = "winapi" version = "0.3.9" @@ -6865,13 +6732,25 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbedf6db9096bc2364adce0ae0aa636dcd89f3c3f2cd67947062aaf0ca2a10ec" +dependencies = [ + "windows_aarch64_msvc 0.32.0", + "windows_i686_gnu 0.32.0", + "windows_i686_msvc 0.32.0", + "windows_x86_64_gnu 0.32.0", + "windows_x86_64_msvc 0.32.0", +] + [[package]] name = "windows" version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647" dependencies = [ - "windows-implement", "windows_aarch64_msvc 0.37.0", "windows_i686_gnu 0.37.0", "windows_i686_msvc 0.37.0", @@ -6880,10 +6759,24 @@ dependencies = [ ] [[package]] -name = "windows-bindgen" -version = "0.37.0" +name = "windows" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bed7be31ade0af08fec9b5343e9edcc005d22b1f11859b8a59b24797f5858e8" +checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a" +dependencies = [ + "windows-implement", + "windows_aarch64_msvc 0.39.0", + "windows_i686_gnu 0.39.0", + "windows_i686_msvc 0.39.0", + "windows_x86_64_gnu 0.39.0", + "windows_x86_64_msvc 0.39.0", +] + +[[package]] +name = "windows-bindgen" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68003dbd0e38abc0fb85b939240f4bce37c43a5981d3df37ccbaaa981b47cb41" dependencies = [ "windows-metadata", "windows-tokens", @@ -6891,9 +6784,9 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.37.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a1062e555f7d9d66fd1130ed4f7c6ec41a47529ee0850cd0e926d95b26bb14" +checksum = "ba01f98f509cb5dc05f4e5fc95e535f78260f15fea8fe1a8abdd08f774f1cee7" dependencies = [ "syn", "windows-tokens", @@ -6901,9 +6794,9 @@ dependencies = [ [[package]] name = "windows-metadata" -version = "0.37.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f33f2b90a6664e369c41ab5ff262d06f048fc9685d9bf8a0e99a47750bb0463" +checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278" [[package]] name = "windows-sys" @@ -6920,9 +6813,15 @@ dependencies = [ [[package]] name = "windows-tokens" -version = "0.37.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3263d25f1170419995b78ff10c06b949e8a986c35c208dc24333c64753a87169" +checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" [[package]] name = "windows_aarch64_msvc" @@ -6936,6 +6835,18 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a" +[[package]] +name = "windows_aarch64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2" + +[[package]] +name = "windows_i686_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" + [[package]] name = "windows_i686_gnu" version = "0.36.1" @@ -6948,6 +6859,18 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1" +[[package]] +name = "windows_i686_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b" + +[[package]] +name = "windows_i686_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" + [[package]] name = "windows_i686_msvc" version = "0.36.1" @@ -6960,6 +6883,18 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c" +[[package]] +name = "windows_i686_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" + [[package]] name = "windows_x86_64_gnu" version = "0.36.1" @@ -6972,6 +6907,18 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d" +[[package]] +name = "windows_x86_64_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" + [[package]] name = "windows_x86_64_msvc" version = "0.36.1" @@ -6984,6 +6931,12 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809" + [[package]] name = "winreg" version = "0.10.1" @@ -7004,19 +6957,22 @@ dependencies = [ [[package]] name = "wry" -version = "0.19.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce19dddbd3ce01dc8f14eb6d4c8f914123bf8379aaa838f6da4f981ff7104a3f" +checksum = "ff5c1352b4266fdf92c63479d2f58ab4cd29dc4e78fbc1b62011ed1227926945" dependencies = [ + "base64", "block", "cocoa", "core-graphics", + "crossbeam-channel", "gdk", "gio", "glib", "gtk", + "html5ever", "http", - "jni 0.18.0", + "kuchiki", "libc", "log", "objc", @@ -7024,13 +6980,14 @@ dependencies = [ "once_cell", "serde", "serde_json", + "sha2 0.10.6", "tao", "thiserror", "url", "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows", + "windows 0.39.0", "windows-implement", ] @@ -7042,9 +6999,9 @@ checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" [[package]] name = "x11" -version = "2.19.1" +version = "2.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd0565fa8bfba8c5efe02725b14dff114c866724eff2cfd44d76cea74bcd87a" +checksum = "f7ae97874a928d821b061fce3d1fc52f08071dd53c89a6102bc06efcac3b2908" dependencies = [ "libc", "pkg-config", @@ -7052,9 +7009,9 @@ dependencies = [ [[package]] name = "x11-dl" -version = "2.19.1" +version = "2.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea26926b4ce81a6f5d9d0f3a0bc401e5a37c6ae14a1bfaa8ff6099ca80038c59" +checksum = "0c83627bc137605acc00bb399c7b908ef460b621fc37c953db2b09f88c449ea6" dependencies = [ "lazy_static", "libc", diff --git a/nym-connect/package.json b/nym-connect/package.json index ae4d2ec7e7..3eb24030d1 100644 --- a/nym-connect/package.json +++ b/nym-connect/package.json @@ -30,7 +30,8 @@ "@mui/material": "^5.2.2", "@mui/styles": "^5.2.2", "@nymproject/react": "^1.0.0", - "@tauri-apps/api": "^1.0.2", + "@tauri-apps/api": "^1.1.0", + "@tauri-apps/tauri-forage": "^1.0.0-beta.2", "clsx": "^1.1.1", "luxon": "^2.3.0", "pretty-bytes": "^6.0.0", @@ -40,7 +41,6 @@ "react-hook-form": "^7.14.2", "react-router-dom": "^5.2.0", "semver": "^6.3.0", - "@tauri-apps/tauri-forage": "^1.0.0-beta.2", "yup": "^0.32.9" }, "devDependencies": { @@ -53,7 +53,7 @@ "@pmmmwh/react-refresh-webpack-plugin": "^0.5.4", "@storybook/react": "^6.5.8", "@svgr/webpack": "^6.1.1", - "@tauri-apps/cli": "^1.0.2", + "@tauri-apps/cli": "^1.1.0", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.0.0", "@types/jest": "^27.0.1", diff --git a/nym-connect/src-tauri/Cargo.toml b/nym-connect/src-tauri/Cargo.toml index 83f9b43382..00d79ec9eb 100644 --- a/nym-connect/src-tauri/Cargo.toml +++ b/nym-connect/src-tauri/Cargo.toml @@ -13,10 +13,10 @@ rust-version = "1.58" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [build-dependencies] -tauri-build = { version = "^1.0.2", features = [] } +tauri-build = { version = "^1.1.1", features = [] } -tauri-codegen = "^1.0.2" -tauri-macros = "^1.0.2" +tauri-codegen = "^1.1.1" +tauri-macros = "^1.1.1" [dependencies] @@ -32,7 +32,7 @@ reqwest = { version = "0.11", features = ["json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tap = "1.0.1" -tauri = { version = "^1.0.2", features = ["clipboard-write-text", "shell-open", "system-tray", "updater"] } +tauri = { version = "^1.1.1", features = ["clipboard-write-text", "shell-open", "system-tray", "updater"] } tendermint-rpc = "0.23.0" thiserror = "1.0" tokio = { version = "1.19.1", features = ["sync", "time"] } diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 04fab7d95c..629811ab68 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -195,12 +195,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "az" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f771a5d1f5503f7f4279a30f3643d3421ba149848b89ecaaec0ea2acf04a5ac4" - [[package]] name = "base16ct" version = "0.1.1" @@ -671,6 +665,7 @@ name = "contracts-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "serde", ] [[package]] @@ -1356,19 +1351,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "fixed" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d38004fb072c44da88a322343c7c784bd1ff6f2e3f5b332050de60c5eea3e0e" -dependencies = [ - "az", - "bytemuck", - "half", - "serde", - "typenum", -] - [[package]] name = "flate2" version = "1.0.22" @@ -1909,12 +1891,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "half" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" - [[package]] name = "handlebars" version = "3.5.5" @@ -2565,16 +2541,17 @@ dependencies = [ name = "mixnet-contract-common" version = "0.1.0" dependencies = [ - "az", + "bs58", "contracts-common", "cosmwasm-std", - "fixed", "log", "schemars", "serde", + "serde_json", "serde_repr", "thiserror", "time 0.3.7", + "ts-rs", ] [[package]] @@ -5256,6 +5233,7 @@ dependencies = [ "bs58", "coconut-interface", "cosmrs", + "cosmwasm-std", "getset", "mixnet-contract-common", "schemars", @@ -5329,7 +5307,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vesting-contract" -version = "1.0.2" +version = "1.1.0" dependencies = [ "cosmwasm-std", "cw-storage-plus", diff --git a/nym-wallet/nym-wallet-recovery-cli/src/main.rs b/nym-wallet/nym-wallet-recovery-cli/src/main.rs index 068745fcdd..e507581def 100644 --- a/nym-wallet/nym-wallet-recovery-cli/src/main.rs +++ b/nym-wallet/nym-wallet-recovery-cli/src/main.rs @@ -170,7 +170,7 @@ fn get_login_entry(login: &Value) -> Result<(&str, &str, &str)> { } fn base64_decode(ciphertext: &str, iv: &str, salt: &str) -> Result<(Vec, Vec, Vec)> { - let ciphertext = base64::decode(&ciphertext) + let ciphertext = base64::decode(ciphertext) .map_err(|err| anyhow!("Unable to base64 decode ciphertext: {}", err))?; let iv = base64::decode(iv).map_err(|err| anyhow!("Unable to base64 decode iv: {}", err))?; let salt = diff --git a/nym-wallet/nym-wallet-types/Cargo.toml b/nym-wallet/nym-wallet-types/Cargo.toml index 2a6fb8836b..56d7f8ae72 100644 --- a/nym-wallet/nym-wallet-types/Cargo.toml +++ b/nym-wallet/nym-wallet-types/Cargo.toml @@ -26,7 +26,7 @@ nym-types = { path = "../../common/types" } [features] default = [] -generate-ts = [] +generate-ts = ["mixnet-contract-common/generate-ts", "nym-types/generate-ts"] diff --git a/nym-wallet/nym-wallet-types/src/admin.rs b/nym-wallet/nym-wallet-types/src/admin.rs index 9e2c30aefd..183ecb7a8b 100644 --- a/nym-wallet/nym-wallet-types/src/admin.rs +++ b/nym-wallet/nym-wallet-types/src/admin.rs @@ -1,10 +1,10 @@ -use std::convert::TryFrom; - -use cosmwasm_std::Uint128; -use serde::{Deserialize, Serialize}; +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 use mixnet_contract_common::ContractStateParams; +use nym_types::currency::{DecCoin, RegisteredCoins}; use nym_types::error::TypesError; +use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( @@ -13,35 +13,44 @@ use nym_types::error::TypesError; )] #[derive(Serialize, Deserialize, Debug)] pub struct TauriContractStateParams { - minimum_mixnode_pledge: String, - minimum_gateway_pledge: String, - mixnode_rewarded_set_size: u32, - mixnode_active_set_size: u32, - staking_supply: String, + minimum_mixnode_pledge: DecCoin, + minimum_gateway_pledge: DecCoin, + minimum_mixnode_delegation: Option, } -impl From for TauriContractStateParams { - fn from(p: ContractStateParams) -> TauriContractStateParams { - TauriContractStateParams { - minimum_mixnode_pledge: p.minimum_mixnode_pledge.to_string(), - minimum_gateway_pledge: p.minimum_gateway_pledge.to_string(), - mixnode_rewarded_set_size: p.mixnode_rewarded_set_size, - mixnode_active_set_size: p.mixnode_active_set_size, - staking_supply: p.staking_supply.to_string(), - } +impl TauriContractStateParams { + pub fn from_mixnet_contract_contract_state_params( + state_params: ContractStateParams, + reg: &RegisteredCoins, + ) -> Result { + Ok(TauriContractStateParams { + minimum_mixnode_pledge: reg + .attempt_convert_to_display_dec_coin(state_params.minimum_mixnode_pledge.into())?, + minimum_gateway_pledge: reg + .attempt_convert_to_display_dec_coin(state_params.minimum_gateway_pledge.into())?, + minimum_mixnode_delegation: state_params + .minimum_mixnode_delegation + .map(|min_del| reg.attempt_convert_to_display_dec_coin(min_del.into())) + .transpose()?, + }) } -} -impl TryFrom for ContractStateParams { - type Error = TypesError; - - fn try_from(p: TauriContractStateParams) -> Result { + pub fn try_convert_to_mixnet_contract_params( + self, + reg: &RegisteredCoins, + ) -> Result { Ok(ContractStateParams { - minimum_mixnode_pledge: Uint128::try_from(p.minimum_mixnode_pledge.as_str())?, - minimum_gateway_pledge: Uint128::try_from(p.minimum_gateway_pledge.as_str())?, - mixnode_rewarded_set_size: p.mixnode_rewarded_set_size, - mixnode_active_set_size: p.mixnode_active_set_size, - staking_supply: Uint128::try_from(p.staking_supply.as_str())?, + minimum_mixnode_delegation: self + .minimum_mixnode_delegation + .map(|min_del| reg.attempt_convert_to_base_coin(min_del)) + .transpose()? + .map(Into::into), + minimum_mixnode_pledge: reg + .attempt_convert_to_base_coin(self.minimum_mixnode_pledge)? + .into(), + minimum_gateway_pledge: reg + .attempt_convert_to_base_coin(self.minimum_gateway_pledge)? + .into(), }) } } diff --git a/nym-wallet/nym-wallet-types/src/epoch.rs b/nym-wallet/nym-wallet-types/src/epoch.rs deleted file mode 100644 index 8e4968df07..0000000000 --- a/nym-wallet/nym-wallet-types/src/epoch.rs +++ /dev/null @@ -1,26 +0,0 @@ -use mixnet_contract_common::Interval; -use serde::{Deserialize, Serialize}; - -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts(export_to = "nym-wallet/src/types/rust/Epoch.ts") -)] -#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Serialize)] -pub struct Epoch { - id: u32, - start: i64, - end: i64, - duration_seconds: u64, -} - -impl From for Epoch { - fn from(interval: Interval) -> Self { - Self { - id: interval.id(), - start: interval.start_unix_timestamp(), - end: interval.end_unix_timestamp(), - duration_seconds: interval.length().as_secs(), - } - } -} diff --git a/nym-wallet/nym-wallet-types/src/interval.rs b/nym-wallet/nym-wallet-types/src/interval.rs new file mode 100644 index 0000000000..c4937b4922 --- /dev/null +++ b/nym-wallet/nym-wallet-types/src/interval.rs @@ -0,0 +1,35 @@ +// Copyright 2021-2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use mixnet_contract_common::{EpochId, Interval as ContractInterval, IntervalId}; +use serde::{Deserialize, Serialize}; + +// TODO: ask @MS why we can't just use ContractInterval directly +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "nym-wallet/src/types/rust/Interval.ts") +)] +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Serialize)] +pub struct Interval { + id: IntervalId, + epochs_in_interval: u32, + + current_epoch_start_unix: i64, + current_epoch_id: EpochId, + epoch_length_seconds: u64, + total_elapsed_epochs: EpochId, +} + +impl From for Interval { + fn from(contract_interval: ContractInterval) -> Self { + Interval { + id: contract_interval.current_interval_id(), + epochs_in_interval: contract_interval.epochs_in_interval(), + current_epoch_start_unix: contract_interval.current_epoch_start_unix_timestamp(), + current_epoch_id: contract_interval.current_epoch_id(), + epoch_length_seconds: contract_interval.epoch_length_secs(), + total_elapsed_epochs: contract_interval.total_elapsed_epochs(), + } + } +} diff --git a/nym-wallet/nym-wallet-types/src/lib.rs b/nym-wallet/nym-wallet-types/src/lib.rs index d6e6600a65..91a50e5691 100644 --- a/nym-wallet/nym-wallet-types/src/lib.rs +++ b/nym-wallet/nym-wallet-types/src/lib.rs @@ -1,5 +1,5 @@ pub mod admin; pub mod app; -pub mod epoch; +pub mod interval; pub mod network; pub mod network_config; diff --git a/nym-wallet/nym-wallet-types/src/network.rs b/nym-wallet/nym-wallet-types/src/network.rs index 19bf580b47..8848b41da8 100644 --- a/nym-wallet/nym-wallet-types/src/network.rs +++ b/nym-wallet/nym-wallet-types/src/network.rs @@ -3,6 +3,7 @@ use config::defaults::all::Network as ConfigNetwork; use config::defaults::{mainnet, qa, sandbox, DenomDetails}; +use nym_types::currency::DecCoin; use serde::{Deserialize, Serialize}; use std::fmt; use strum::EnumIter; @@ -48,6 +49,10 @@ impl Network { Network::MAINNET => mainnet::MIX_DENOM.display, } } + + pub fn default_zero_mix_display_coin(&self) -> DecCoin { + DecCoin::zero(self.display_mix_denom()) + } } impl Default for Network { diff --git a/nym-wallet/package.json b/nym-wallet/package.json index b7445ca95c..d1493c43b0 100644 --- a/nym-wallet/package.json +++ b/nym-wallet/package.json @@ -37,6 +37,7 @@ "bs58": "^4.0.1", "clsx": "^1.1.1", "date-fns": "^2.28.0", + "lodash": "^4.17.21", "notistack": "^2.0.3", "npm-run-all": "^4.1.5", "qrcode.react": "^1.0.1", diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index ea1d023117..d40bedc777 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -56,28 +56,27 @@ fn main() { mixnet::bond::bond_gateway, mixnet::bond::bond_mixnode, mixnet::bond::gateway_bond_details, - mixnet::bond::get_operator_rewards, + mixnet::bond::get_pending_operator_rewards, mixnet::bond::mixnode_bond_details, mixnet::bond::unbond_gateway, mixnet::bond::unbond_mixnode, - mixnet::bond::update_mixnode, + mixnet::bond::update_mixnode_cost_params, + mixnet::bond::update_mixnode_config, mixnet::bond::get_number_of_mixnode_delegators, mixnet::bond::get_mix_node_description, mixnet::delegate::delegate_to_mixnode, - mixnet::delegate::get_delegator_rewards, + mixnet::delegate::get_pending_delegator_rewards, mixnet::delegate::get_pending_delegation_events, mixnet::delegate::get_delegation_summary, - mixnet::delegate::get_all_pending_delegation_events, mixnet::delegate::get_all_mix_delegations, mixnet::delegate::undelegate_from_mixnode, mixnet::delegate::undelegate_all_from_mixnode, - mixnet::epoch::get_current_epoch, + mixnet::interval::get_current_interval, + mixnet::interval::get_pending_epoch_events, + mixnet::interval::get_pending_interval_events, mixnet::rewards::claim_delegator_reward, mixnet::rewards::claim_operator_reward, - mixnet::rewards::compound_operator_reward, - mixnet::rewards::compound_delegator_reward, mixnet::rewards::claim_locked_and_unlocked_delegator_reward, - mixnet::rewards::compound_locked_and_unlocked_delegator_reward, mixnet::send::send, network_config::add_validator, network_config::get_validator_api_urls, @@ -92,6 +91,8 @@ fn main() { utils::owns_mixnode, utils::get_env, utils::get_old_and_incorrect_hardcoded_fee, + utils::try_convert_pubkey_to_mix_id, + utils::default_mixnode_cost_params, validator_api::status::gateway_core_node_status, validator_api::status::mixnode_core_node_status, validator_api::status::mixnode_inclusion_probability, @@ -100,15 +101,13 @@ fn main() { validator_api::status::mixnode_status, vesting::rewards::vesting_claim_delegator_reward, vesting::rewards::vesting_claim_operator_reward, - vesting::rewards::vesting_compound_operator_reward, - vesting::rewards::vesting_compound_delegator_reward, vesting::bond::vesting_bond_gateway, vesting::bond::vesting_bond_mixnode, vesting::bond::vesting_unbond_gateway, vesting::bond::vesting_unbond_mixnode, - vesting::bond::vesting_update_mixnode, + vesting::bond::vesting_update_mixnode_cost_params, + vesting::bond::vesting_update_mixnode_config, vesting::bond::withdraw_vested_coins, - vesting::delegate::get_pending_vesting_delegation_events, vesting::delegate::vesting_delegate_to_mixnode, vesting::delegate::vesting_undelegate_from_mixnode, vesting::queries::delegated_free, @@ -130,7 +129,8 @@ fn main() { simulate::mixnet::simulate_unbond_gateway, simulate::mixnet::simulate_bond_mixnode, simulate::mixnet::simulate_unbond_mixnode, - simulate::mixnet::simulate_update_mixnode, + simulate::mixnet::simulate_update_mixnode_config, + simulate::mixnet::simulate_update_mixnode_cost_params, simulate::mixnet::simulate_delegate_to_mixnode, simulate::mixnet::simulate_undelegate_from_mixnode, simulate::vesting::simulate_vesting_delegate_to_mixnode, @@ -139,16 +139,13 @@ fn main() { simulate::vesting::simulate_vesting_unbond_gateway, simulate::vesting::simulate_vesting_bond_mixnode, simulate::vesting::simulate_vesting_unbond_mixnode, - simulate::vesting::simulate_vesting_update_mixnode, + simulate::vesting::simulate_vesting_update_mixnode_config, + simulate::vesting::simulate_vesting_update_mixnode_cost_params, simulate::vesting::simulate_withdraw_vested_coins, simulate::vesting::simulate_vesting_claim_delegator_reward, simulate::vesting::simulate_vesting_claim_operator_reward, - simulate::vesting::simulate_vesting_compound_operator_reward, - simulate::vesting::simulate_vesting_compound_delegator_reward, simulate::mixnet::simulate_claim_delegator_reward, simulate::mixnet::simulate_claim_operator_reward, - simulate::mixnet::simulate_compound_operator_reward, - simulate::mixnet::simulate_compound_delegator_reward, signatures::sign::sign, signatures::sign::verify, help::log::help_log_toggle_window, diff --git a/nym-wallet/src-tauri/src/operations/mixnet/admin.rs b/nym-wallet/src-tauri/src/operations/mixnet/admin.rs index faced61bce..5074c5500e 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/admin.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/admin.rs @@ -1,9 +1,12 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + use crate::error::BackendError; -use crate::nymd_client; use crate::state::WalletState; use mixnet_contract_common::ContractStateParams; +use nym_types::transaction::TransactionExecuteResult; use nym_wallet_types::admin::TauriContractStateParams; -use std::convert::TryInto; +use validator_client::nymd::traits::{MixnetQueryClient, MixnetSigningClient}; use validator_client::nymd::Fee; #[tauri::command] @@ -11,9 +14,15 @@ pub async fn get_contract_settings( state: tauri::State<'_, WalletState>, ) -> Result { log::info!(">>> Getting contract settings"); - let res = nymd_client!(state).get_contract_settings().await?.into(); - log::trace!("<<< {:?}", res); - Ok(res) + + let guard = state.read().await; + let reg = guard.registered_coins()?; + let client = &guard.current_client()?.nymd; + + let res = client.get_mixnet_contract_settings().await?; + let converted = TauriContractStateParams::from_mixnet_contract_contract_state_params(res, reg)?; + log::trace!("<<< {:?}", converted); + Ok(converted) } #[tauri::command] @@ -21,16 +30,25 @@ pub async fn update_contract_settings( params: TauriContractStateParams, fee: Option, state: tauri::State<'_, WalletState>, -) -> Result { - let mixnet_contract_settings_params: ContractStateParams = params.try_into()?; +) -> Result { + let guard = state.read().await; + let reg = guard.registered_coins()?; + let client = &guard.current_client()?.nymd; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + + let mixnet_contract_settings_params: ContractStateParams = + params.try_convert_to_mixnet_contract_params(reg)?; log::info!( ">>> Updating contract settings: {:?}", mixnet_contract_settings_params ); - nymd_client!(state) - .update_contract_settings(mixnet_contract_settings_params.clone(), fee) + let res = client + .update_contract_state_params(mixnet_contract_settings_params, fee) .await?; - let res = mixnet_contract_settings_params.into(); + + log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); - Ok(res) + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index 033186886a..08f9dc1daa 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -1,15 +1,18 @@ -use std::time::Duration; +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 use crate::error::BackendError; use crate::state::WalletState; -use crate::{Gateway, MixNode}; +use crate::{nymd_client, Gateway, MixNode}; +use mixnet_contract_common::{MixNodeConfigUpdate, NodeId}; use nym_types::currency::DecCoin; use nym_types::gateway::GatewayBond; -use nym_types::mixnode::MixNodeBond; +use nym_types::mixnode::{MixNodeCostParams, MixNodeDetails}; use nym_types::transaction::TransactionExecuteResult; -use reqwest::Error as ReqwestError; use serde::{Deserialize, Serialize}; -use validator_client::nymd::{Coin, Fee}; +use std::time::Duration; +use validator_client::nymd::traits::{MixnetQueryClient, MixnetSigningClient}; +use validator_client::nymd::Fee; #[derive(Debug, Serialize, Deserialize)] pub struct NodeDescription { @@ -69,15 +72,17 @@ pub async fn unbond_gateway( #[tauri::command] pub async fn bond_mixnode( mixnode: MixNode, + cost_params: MixNodeCostParams, owner_signature: String, pledge: DecCoin, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; + let reg = guard.registered_coins()?; let pledge_base = guard.attempt_convert_to_base_coin(pledge.clone())?; let fee_amount = guard.convert_tx_fee(fee.as_ref()); - + let cost_params = cost_params.try_convert_to_mixnet_contract_cost_params(reg)?; log::info!( ">>> Bond mixnode: identity_key = {}, pledge_display = {}, pledge_base = {}, fee = {:?}", mixnode.identity_key, @@ -88,7 +93,7 @@ pub async fn bond_mixnode( let res = guard .current_client()? .nymd - .bond_mixnode(mixnode, owner_signature, pledge_base, fee) + .bond_mixnode(mixnode, cost_params, owner_signature, pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -114,22 +119,49 @@ pub async fn unbond_mixnode( } #[tauri::command] -pub async fn update_mixnode( - profit_margin_percent: u8, +pub async fn update_mixnode_cost_params( + new_costs: MixNodeCostParams, + fee: Option, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.read().await; + let reg = guard.registered_coins()?; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + let cost_params = new_costs.try_convert_to_mixnet_contract_cost_params(reg)?; + log::info!( + ">>> Update mixnode cost parameters: new parameters = {}, fee {:?}", + cost_params.to_inline_json(), + fee, + ); + let res = guard + .current_client()? + .nymd + .update_mixnode_cost_params(cost_params, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + +#[tauri::command] +pub async fn update_mixnode_config( + update: MixNodeConfigUpdate, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Update mixnode: profit_margin_percent = {}, fee {:?}", - profit_margin_percent, + ">>> Update mixnode config: update = {}, fee {:?}", + update.to_inline_json(), fee, ); let res = guard .current_client()? .nymd - .update_mixnode_config(profit_margin_percent, fee) + .update_mixnode_config(update, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -141,25 +173,29 @@ pub async fn update_mixnode( #[tauri::command] pub async fn mixnode_bond_details( state: tauri::State<'_, WalletState>, -) -> Result, BackendError> { +) -> Result, BackendError> { log::info!(">>> Get mixnode bond details"); let guard = state.read().await; let client = guard.current_client()?; - let bond = client.nymd.owns_mixnode(client.nymd.address()).await?; - let res = bond - .map(|bond| { + let res = client.nymd.get_owned_mixnode(client.nymd.address()).await?; + let details = res + .mixnode_details + .map(|details| { guard .registered_coins() - .map(|reg| MixNodeBond::from_mixnet_contract_mixnode_bond(bond, reg)) + .map(|reg| MixNodeDetails::from_mixnet_contract_mixnode_details(details, reg)) }) .transpose()? .transpose()?; log::info!( - "<<< identity_key = {:?}", - res.as_ref().map(|r| r.mix_node.identity_key.to_string()) + "<<< mix_id/identity_key = {:?}", + details.as_ref().map(|r| ( + r.bond_information.id, + &r.bond_information.mix_node.identity_key + )) ); - log::trace!("<<< {:?}", res); - Ok(res) + log::trace!("<<< {:?}", details); + Ok(details) } #[tauri::command] @@ -169,8 +205,9 @@ pub async fn gateway_bond_details( log::info!(">>> Get gateway bond details"); let guard = state.read().await; let client = guard.current_client()?; - let bond = client.nymd.owns_gateway(client.nymd.address()).await?; + let bond = client.nymd.get_owned_gateway(client.nymd.address()).await?; let res = bond + .gateway .map(|bond| { guard .registered_coins() @@ -188,23 +225,36 @@ pub async fn gateway_bond_details( } #[tauri::command] -pub async fn get_operator_rewards( +pub async fn get_pending_operator_rewards( address: String, state: tauri::State<'_, WalletState>, ) -> Result { - log::info!(">>> Get operator rewards for {}", address); + log::info!(">>> Get pending operator rewards for {}", address); let guard = state.read().await; - let network = guard.current_network(); - let denom = network.base_mix_denom(); - let reward_amount = guard + let res = guard .current_client()? .nymd - .get_operator_rewards(address) + .get_pending_operator_reward(&address.parse()?) .await?; - let base_coin = Coin::new(reward_amount.u128(), denom); - let display_coin: DecCoin = guard.attempt_convert_to_display_dec_coin(base_coin.clone())?; + + // note to @MS: now we're able to obtain more information than just the pending reward + // the entire returned struct contains the following: + /* + pub amount_staked: Option, + pub amount_earned: Option, + pub amount_earned_detailed: Option, + pub mixnode_still_fully_bonded: bool, + */ + + let base_coin = res.amount_earned; + let display_coin = base_coin + .as_ref() + .map(|c| guard.attempt_convert_to_display_dec_coin(c.clone().into())) + .transpose()? + .unwrap_or_else(|| guard.default_zero_mix_display_coin()); + log::info!( - "<<< rewards_base = {}, rewards_display = {}", + "<<< rewards_base = {:?}, rewards_display = {}", base_coin, display_coin ); @@ -213,40 +263,15 @@ pub async fn get_operator_rewards( #[tauri::command] pub async fn get_number_of_mixnode_delegators( - identity: String, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - let client = guard.current_client()?; - let paged_delegations = client - .nymd - .get_mix_delegations_paged(identity, None, Some(20)) - .await?; - - Ok(paged_delegations.delegations.len()) -} - -async fn fetch_mix_node_description( - host: &str, - port: u16, -) -> Result { - let milli_second = Duration::from_millis(1000); - let client = reqwest::Client::builder().timeout(milli_second).build()?; - let response = client - .get(format!("http://{}:{}/description", host, port)) - .send() - .await; - - match response { - Ok(res) => { - let json = res.json::().await; - match json { - Ok(json) => Ok(json), - Err(e) => Err(e), - } - } - Err(e) => Err(e), - } + Ok(nymd_client!(state) + .get_mixnode_details(mix_id) + .await? + .mixnode_details + .map(|details| details.rewarding_details.unique_delegations) + .unwrap_or_default() as usize) } #[tauri::command] @@ -254,7 +279,12 @@ pub async fn get_mix_node_description( host: &str, port: u16, ) -> Result { - fetch_mix_node_description(host, port) - .await - .map_err(|e| BackendError::ReqwestError { source: e }) + Ok(reqwest::Client::builder() + .timeout(Duration::from_millis(1000)) + .build()? + .get(format!("http://{}:{}/description", host, port)) + .send() + .await? + .json() + .await?) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs index 7e6f27ae8f..7b94ede3ab 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs @@ -1,61 +1,89 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + use crate::error::BackendError; use crate::state::WalletState; -use crate::vesting::delegate::{ - get_pending_vesting_delegation_events, vesting_undelegate_from_mixnode, -}; -use crate::{api_client, nymd_client}; -use mixnet_contract_common::IdentityKey; +use crate::vesting::delegate::vesting_undelegate_from_mixnode; +use mixnet_contract_common::NodeId; use nym_types::currency::DecCoin; -use nym_types::delegation::{ - Delegation, DelegationEvent, DelegationRecord, DelegationWithEverything, - DelegationsSummaryResponse, +use nym_types::delegation::{Delegation, DelegationWithEverything, DelegationsSummaryResponse}; +use nym_types::deprecated::{ + convert_to_delegation_events, DelegationEvent, WrappedDelegationEvent, }; +use nym_types::mixnode::MixNodeCostParams; +use nym_types::pending_events::PendingEpochEvent; use nym_types::transaction::TransactionExecuteResult; -use std::collections::HashMap; -use validator_client::nymd::{Coin, Fee}; +use validator_client::nymd::traits::{MixnetQueryClient, MixnetSigningClient}; +use validator_client::nymd::Fee; #[tauri::command] pub async fn get_pending_delegation_events( state: tauri::State<'_, WalletState>, -) -> Result, BackendError> { - log::info!(">>> Get pending delegation events"); +) -> Result, BackendError> { + log::info!(">>> [DEPRECATED] Get all pending delegation events"); let guard = state.read().await; let reg = guard.registered_coins()?; let client = guard.current_client()?; - let events = client - .nymd - .get_pending_delegation_events(client.nymd.address().to_string(), None) - .await?; - log::info!("<<< {} pending delegation events", events.len()); - log::trace!("<<< pending delegation events = {:?}", events); - - Ok(events + let events = client.get_all_nymd_pending_epoch_events().await?; + let converted = events .into_iter() - .map(|event| DelegationEvent::from_mixnet_contract(event, reg)) - .collect::>()?) + .map(|e| PendingEpochEvent::try_from_mixnet_contract(e, reg)) + .collect::, _>>()?; + + let delegation_events = convert_to_delegation_events(converted); + + // we only care about events concerning THIS client + let mut client_specific_events = Vec::new(); + for delegation_event in delegation_events { + if delegation_event.address_matches(client.nymd.address().as_ref()) { + let node_identity = client + .nymd + .get_mixnode_details(delegation_event.mix_id) + .await? + .mixnode_details + .map(|d| d.bond_information.mix_node.identity_key) + .unwrap_or_default(); + + client_specific_events + .push(WrappedDelegationEvent::new(delegation_event, node_identity)); + } + } + + log::info!( + "<<< {} pending delegation events", + client_specific_events.len() + ); + log::trace!( + "<<< pending delegation events = {:?}", + client_specific_events + ); + + Ok(client_specific_events) } #[tauri::command] pub async fn delegate_to_mixnode( - identity: &str, + mix_id: NodeId, amount: DecCoin, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; + let client = guard.current_client()?; let delegation_base = guard.attempt_convert_to_base_coin(amount.clone())?; let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Delegate to mixnode: identity_key = {}, display_amount = {}, base_amount = {}, fee = {:?}", - identity, + ">>> Delegate to mixnode: mix_id = {}, display_amount = {}, base_amount = {}, fee = {:?}", + mix_id, amount, delegation_base, fee, ); - let res = nymd_client!(state) - .delegate_to_mixnode(identity, delegation_base, fee) + let res = client + .nymd + .delegate_to_mixnode(mix_id, delegation_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -66,7 +94,7 @@ pub async fn delegate_to_mixnode( #[tauri::command] pub async fn undelegate_from_mixnode( - identity: &str, + mix_id: NodeId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { @@ -74,14 +102,14 @@ pub async fn undelegate_from_mixnode( let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Undelegate from mixnode: identity_key = {}, fee = {:?}", - identity, + ">>> Undelegate from mixnode: mix_id = {}, fee = {:?}", + mix_id, fee ); let res = guard .current_client()? .nymd - .remove_mixnode_delegation(identity, fee) + .undelegate_from_mixnode(mix_id, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -92,35 +120,29 @@ pub async fn undelegate_from_mixnode( #[tauri::command] pub async fn undelegate_all_from_mixnode( - identity: &str, + mix_id: NodeId, uses_vesting_contract_tokens: bool, - fee: Option, - fee2: Option, + fee_liquid: Option, + fee_vesting: Option, state: tauri::State<'_, WalletState>, ) -> Result, BackendError> { log::info!( - ">>> Undelegate all from mixnode: identity_key = {}, uses_vesting_contract_tokens = {}, fee = {:?}", - identity, + ">>> Undelegate all from mixnode: mix_id = {}, uses_vesting_contract_tokens = {}, fee_liquid = {:?}, fee_vesting = {:?}", + mix_id, uses_vesting_contract_tokens, - fee + fee_liquid, + fee_vesting, ); let mut res: Vec = - vec![undelegate_from_mixnode(identity, fee, state.clone()).await?]; + vec![undelegate_from_mixnode(mix_id, fee_liquid, state.clone()).await?]; if uses_vesting_contract_tokens { - res.push(vesting_undelegate_from_mixnode(identity, fee2, state.clone()).await?); + res.push(vesting_undelegate_from_mixnode(mix_id, fee_vesting, state).await?); } Ok(res) } -struct DelegationWithHistory { - pub delegation: Delegation, - pub amount_sum: DecCoin, - pub history: Vec, - pub uses_vesting_contract_tokens: bool, -} - #[tauri::command] pub async fn get_all_mix_delegations( state: tauri::State<'_, WalletState>, @@ -131,168 +153,93 @@ pub async fn get_all_mix_delegations( let client = guard.current_client()?; let reg = guard.registered_coins()?; - // TODO: add endpoint to validator API to get a single mix node bond - let mixnodes = client.validator_api.get_mixnodes().await?; - let address = client.nymd.address(); let network = guard.current_network(); - let display_mix_denom = network.display_mix_denom(); - let base_mix_denom = network.base_mix_denom(); + let base_mix_denom = network.base_mix_denom().to_string(); + let vesting_contract = client.nymd.vesting_contract_address(); log::info!(" >>> Get delegations"); let delegations = client.get_all_delegator_delegations(address).await?; log::info!(" <<< {} delegations", delegations.len()); - // first get pending events from the mixnet contract (operations made with unlocked tokens) - let mut pending_events_for_account = get_pending_delegation_events(state.clone()).await?; - - // then get pending events from the vesting contract (operations made with locked tokens) - let pending_vesting_events = get_pending_vesting_delegation_events(state.clone()).await?; - for event in pending_vesting_events { - pending_events_for_account.push(event); - } + let pending_events_for_account = get_pending_delegation_events(state.clone()).await?; log::info!( " <<< {} pending delegation events for account", pending_events_for_account.len() ); - let mut map: HashMap = HashMap::new(); + let mut with_everything: Vec = Vec::with_capacity(delegations.len()); - for pending_event in &pending_events_for_account { - if delegations - .iter() - .any(|d| d.node_identity == pending_event.node_identity) - { - let amount = pending_event - .amount - .clone() - .unwrap_or_else(|| DecCoin::zero(display_mix_denom)); - let delegation = DelegationWithHistory { - delegation: Delegation { - amount: amount.clone(), - node_identity: pending_event.node_identity.clone(), - proxy: pending_event.proxy.clone(), // TODO: ask @MS about delegations via vesting contract => surely we'd have proxy there? - owner: pending_event.address.clone(), - block_height: pending_event.block_height, - }, - amount_sum: amount, - uses_vesting_contract_tokens: false, - history: vec![], - }; - map.insert(delegation.delegation.node_identity.clone(), delegation); - } - } - - for d in delegations { - // create record of delegation - let delegated_on_iso_datetime = client - .nymd - .get_block_timestamp(Some(d.block_height as u32)) - .await? - .to_rfc3339(); - let amount = guard.attempt_convert_to_display_dec_coin(d.amount.clone().into())?; - - let record = DelegationRecord { - amount: amount.clone(), - block_height: d.block_height, - delegated_on_iso_datetime, - uses_vesting_contract_tokens: d.proxy.is_some(), - }; - - let entry = map - .entry(d.node_identity.clone()) - .or_insert(DelegationWithHistory { - delegation: Delegation::from_mixnet_contract(d, reg)?, - history: vec![], - amount_sum: DecCoin::zero(display_mix_denom), - uses_vesting_contract_tokens: false, - }); - - debug_assert_eq!(entry.amount_sum.denom, amount.denom); - - entry.history.push(record); - entry.amount_sum.amount += amount.amount; - entry.uses_vesting_contract_tokens = - entry.uses_vesting_contract_tokens || entry.delegation.proxy.is_some(); - } - - let mut with_everything: Vec = vec![]; - - for item in map { - let d = item.1.delegation; - let history = item.1.history; - let Delegation { - owner, - node_identity, - amount, - block_height, - proxy, - } = d; - let uses_vesting_contract_tokens = item.1.uses_vesting_contract_tokens; + for delegation in delegations { + let d = Delegation::from_mixnet_contract(delegation, reg)?; + let uses_vesting_contract_tokens = d + .proxy + .as_ref() + .map(|p| p.as_str() == vesting_contract.as_ref()) + .unwrap_or_default(); log::trace!( - " --- Delegation: node_identity = {}, amount = {}", - node_identity, - amount + " --- Delegation: mix_id = {}, amount = {}", + d.mix_id, + d.amount ); - let mixnode = mixnodes - .iter() - .find(|m| m.mix_node.identity_key == node_identity); + let mixnode = client.get_mixnode_details(d.mix_id).await?.mixnode_details; - let pledge_amount = mixnode - .map(|m| guard.attempt_convert_to_display_dec_coin(m.pledge_amount.clone().into())) + let accumulated_by_operator = mixnode + .as_ref() + .map(|m| { + guard.display_coin_from_base_decimal(&base_mix_denom, m.rewarding_details.operator) + }) .transpose()?; - let total_delegation = mixnode - .map(|m| guard.attempt_convert_to_display_dec_coin(m.total_delegation.clone().into())) + let accumulated_by_delegates = mixnode + .as_ref() + .map(|m| { + guard.display_coin_from_base_decimal(&base_mix_denom, m.rewarding_details.delegates) + }) .transpose()?; - let profit_margin_percent: Option = mixnode.map(|m| m.mix_node.profit_margin_percent); + let cost_params = mixnode + .as_ref() + .map(|m| { + MixNodeCostParams::from_mixnet_contract_mixnode_cost_params( + m.rewarding_details.cost_params.clone(), + reg, + ) + }) + .transpose()?; log::trace!(" >>> Get accumulated rewards: address = {}", address); - let accumulated_rewards = match client + let pending_reward = client .nymd - .get_delegator_rewards(address.to_string(), node_identity.clone(), proxy.clone()) - .await - { - Ok(rewards) => { - let reward = Coin::new(rewards.u128(), base_mix_denom); - let amount = guard.attempt_convert_to_display_dec_coin(reward)?; - log::trace!(" <<< rewards = {}, amount = {}", rewards, amount); + .get_pending_delegator_reward(address, d.mix_id, d.proxy.clone()) + .await?; + + let accumulated_rewards = match &pending_reward.amount_earned { + Some(reward) => { + let amount = guard.attempt_convert_to_display_dec_coin(reward.clone().into())?; + log::trace!(" <<< rewards = {:?}, amount = {}", pending_reward, amount); Some(amount) } - Err(_) => { + None => { log::trace!(" <<< no rewards waiting"); None } }; - let pending_events = - filter_pending_events(&node_identity, pending_events_for_account.clone()); - log::trace!( - " --- pending events for mixnode = {}", - pending_events.len() - ); - - log::trace!( - " >>> Get stake saturation: node_identity = {}", - node_identity - ); - let stake_saturation = api_client!(state) - .get_mixnode_stake_saturation(&node_identity) - .await - .ok() - .map(|r| r.saturation); + log::trace!(" >>> Get stake saturation: mix_id = {}", d.mix_id); + let stake_saturation = client.nymd.get_mixnode_stake_saturation(d.mix_id).await?; log::trace!(" <<< {:?}", stake_saturation); log::trace!( - " >>> Get average uptime percentage: node_identity = {}", - node_identity + " >>> Get average uptime percentage: mix_iid = {}", + d.mix_id ); - let avg_uptime_percent = api_client!(state) - .get_mixnode_avg_uptime(&node_identity) + let avg_uptime_percent = client + .validator_api + .get_mixnode_avg_uptime(d.mix_id) .await .ok() .map(|r| r.avg_uptime); @@ -300,10 +247,11 @@ pub async fn get_all_mix_delegations( log::trace!( " >>> Convert delegated on block height to timestamp: block_height = {}", - d.block_height + d.height ); - let timestamp = nymd_client!(state) - .get_block_timestamp(Some(d.block_height as u32)) + let timestamp = client + .nymd + .get_block_timestamp(Some(d.height as u32)) .await?; let delegated_on_iso_datetime = timestamp.to_rfc3339(); log::trace!( @@ -312,21 +260,29 @@ pub async fn get_all_mix_delegations( delegated_on_iso_datetime ); + let pending_events = filter_pending_events(d.mix_id, &pending_events_for_account); + log::trace!( + " --- pending events for mixnode = {}", + pending_events.len() + ); + with_everything.push(DelegationWithEverything { - owner: owner.to_string(), - node_identity: node_identity.to_string(), - amount: item.1.amount_sum, - block_height, + owner: d.owner, + mix_id: d.mix_id, + node_identity: mixnode + .map(|m| m.bond_information.mix_node.identity_key) + .unwrap_or_default(), + amount: d.amount, + block_height: d.height, uses_vesting_contract_tokens, delegated_on_iso_datetime, - stake_saturation, - accumulated_rewards, - profit_margin_percent, - pledge_amount, + stake_saturation: stake_saturation.uncapped_saturation, + accumulated_by_operator, avg_uptime_percent, - total_delegation, + accumulated_by_delegates, + cost_params, + unclaimed_rewards: accumulated_rewards, pending_events, - history, }) } log::trace!("<<< {:?}", with_everything); @@ -335,41 +291,54 @@ pub async fn get_all_mix_delegations( } fn filter_pending_events( - node_identity: &str, - pending_events: Vec, + mix_id: NodeId, + pending_events: &[WrappedDelegationEvent], ) -> Vec { pending_events .iter() - .filter(|e| e.node_identity == node_identity) + .filter(|e| e.event.mix_id == mix_id) .cloned() - .collect::>() + .map(|e| e.event) + .collect() } #[tauri::command] -pub async fn get_delegator_rewards( +pub async fn get_pending_delegator_rewards( address: String, - mix_identity: IdentityKey, + mix_id: NodeId, proxy: Option, state: tauri::State<'_, WalletState>, ) -> Result { log::info!( - ">>> Get delegator rewards: mix_identity = {}, proxy = {:?}", - mix_identity, + ">>> Get pending delegator rewards: mix_id = {}, proxy = {:?}", + mix_id, proxy ); let guard = state.read().await; - let network = guard.current_network(); - let denom = network.base_mix_denom(); - let reward_amount = guard + let res = guard .current_client()? .nymd - .get_delegator_rewards(address, mix_identity, proxy) + .get_pending_delegator_reward(&address.parse()?, mix_id, proxy) .await?; - let base_coin = Coin::new(reward_amount.u128(), denom); - let display_coin: DecCoin = guard.attempt_convert_to_display_dec_coin(base_coin.clone())?; + + // note to @MS: now we're able to obtain more information than just the pending reward + // the entire returned struct contains the following: + /* + pub amount_staked: Option, + pub amount_earned: Option, + pub amount_earned_detailed: Option, + pub mixnode_still_fully_bonded: bool, + */ + + let base_coin = res.amount_earned; + let display_coin = base_coin + .as_ref() + .map(|c| guard.attempt_convert_to_display_dec_coin(c.clone().into())) + .transpose()? + .unwrap_or_else(|| guard.default_zero_mix_display_coin()); log::info!( - "<<< rewards_base = {}, rewards_display = {}", + "<<< rewards_base = {:?}, rewards_display = {}", base_coin, display_coin ); @@ -393,7 +362,7 @@ pub async fn get_delegation_summary( for d in &delegations { debug_assert_eq!(d.amount.denom, display_mix_denom); total_delegations.amount += d.amount.amount; - if let Some(rewards) = &d.accumulated_rewards { + if let Some(rewards) = &d.unclaimed_rewards { debug_assert_eq!(rewards.denom, display_mix_denom); total_rewards.amount += rewards.amount; } @@ -412,21 +381,3 @@ pub async fn get_delegation_summary( total_rewards, }) } - -#[tauri::command] -pub async fn get_all_pending_delegation_events( - state: tauri::State<'_, WalletState>, -) -> Result, BackendError> { - log::info!(">>> Get all pending delegation events"); - - // get pending events from mixnet and vesting contract - let mut pending_events_for_account = get_pending_delegation_events(state.clone()).await?; - let pending_vesting_events = get_pending_vesting_delegation_events(state.clone()).await?; - - // combine them - for event in pending_vesting_events { - pending_events_for_account.push(event); - } - - Ok(pending_events_for_account) -} diff --git a/nym-wallet/src-tauri/src/operations/mixnet/epoch.rs b/nym-wallet/src-tauri/src/operations/mixnet/epoch.rs deleted file mode 100644 index 2ca8fe0c05..0000000000 --- a/nym-wallet/src-tauri/src/operations/mixnet/epoch.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::error::BackendError; -use crate::nymd_client; -use crate::state::WalletState; -use nym_wallet_types::epoch::Epoch; - -#[tauri::command] -pub async fn get_current_epoch( - state: tauri::State<'_, WalletState>, -) -> Result { - log::info!(">>> Get curren epoch"); - let interval = nymd_client!(state).get_current_epoch().await?; - log::info!("<<< curren epoch = {}", interval); - Ok(interval.into()) -} diff --git a/nym-wallet/src-tauri/src/operations/mixnet/interval.rs b/nym-wallet/src-tauri/src/operations/mixnet/interval.rs new file mode 100644 index 0000000000..ce88d840b4 --- /dev/null +++ b/nym-wallet/src-tauri/src/operations/mixnet/interval.rs @@ -0,0 +1,58 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::BackendError; +use crate::nymd_client; +use crate::state::WalletState; +use nym_types::pending_events::{PendingEpochEvent, PendingIntervalEvent}; +use nym_wallet_types::interval::Interval; +use validator_client::nymd::traits::MixnetQueryClient; + +#[tauri::command] +pub async fn get_current_interval( + state: tauri::State<'_, WalletState>, +) -> Result { + log::info!(">>> Get current interval"); + let res = nymd_client!(state).get_current_interval_details().await?; + log::info!("<<< current interval = {:?}", res); + Ok(res.interval.into()) +} + +#[tauri::command] +pub async fn get_pending_epoch_events( + state: tauri::State<'_, WalletState>, +) -> Result, BackendError> { + log::info!(">>> Get pending epoch events"); + let guard = state.read().await; + let reg = guard.registered_coins()?; + let client = guard.current_client()?; + let res = client.get_all_nymd_pending_epoch_events().await?; + + log::info!("<<< got = {:?} events", res.len()); + + let converted = res + .into_iter() + .map(|e| PendingEpochEvent::try_from_mixnet_contract(e, reg)) + .collect::, _>>()?; + Ok(converted) +} + +#[tauri::command] +pub async fn get_pending_interval_events( + state: tauri::State<'_, WalletState>, +) -> Result, BackendError> { + log::info!(">>> Get pending interval events"); + + let guard = state.read().await; + let reg = guard.registered_coins()?; + let client = guard.current_client()?; + let res = client.get_all_nymd_pending_interval_events().await?; + + log::info!("<<< got = {:?} events", res.len()); + + let converted = res + .into_iter() + .map(|e| PendingIntervalEvent::try_from_mixnet_contract(e, reg)) + .collect::, _>>()?; + Ok(converted) +} diff --git a/nym-wallet/src-tauri/src/operations/mixnet/mod.rs b/nym-wallet/src-tauri/src/operations/mixnet/mod.rs index 40ebc405d8..cb94278ee2 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/mod.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/mod.rs @@ -2,6 +2,6 @@ pub mod account; pub mod admin; pub mod bond; pub mod delegate; -pub mod epoch; +pub mod interval; pub mod rewards; pub mod send; diff --git a/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs b/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs index 983c870c61..6305ef981d 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs @@ -1,9 +1,9 @@ use crate::error::BackendError; -use crate::nymd_client; use crate::state::WalletState; -use crate::vesting::rewards::{vesting_claim_delegator_reward, vesting_compound_delegator_reward}; -use mixnet_contract_common::IdentityKey; +use crate::vesting::rewards::vesting_claim_delegator_reward; +use mixnet_contract_common::NodeId; use nym_types::transaction::TransactionExecuteResult; +use validator_client::nymd::traits::{MixnetQueryClient, MixnetSigningClient}; use validator_client::nymd::Fee; #[tauri::command] @@ -11,35 +11,14 @@ pub async fn claim_operator_reward( fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { - // TODO: handle operator bonding with vesting contract - log::info!(">>> Claim operator reward"); + // // TODO: handle operator bonding with vesting contract + log::info!(">>> Withdraw operator reward"); let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); let res = guard .current_client()? .nymd - .execute_claim_operator_reward(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 compound_operator_reward( - fee: Option, - state: tauri::State<'_, WalletState>, -) -> Result { - // TODO: handle operator bonding with vesting contract - log::info!(">>> Compound operator reward"); - let guard = state.read().await; - let fee_amount = guard.convert_tx_fee(fee.as_ref()); - let res = guard - .current_client()? - .nymd - .execute_compound_operator_reward(fee) + .withdraw_operator_reward(fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -50,44 +29,17 @@ pub async fn compound_operator_reward( #[tauri::command] pub async fn claim_delegator_reward( - mix_identity: IdentityKey, + mix_id: NodeId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { - log::info!( - ">>> Claim delegator reward: identity_key = {}", - mix_identity - ); + log::info!(">>> Withdraw delegator reward: mix_id = {}", mix_id); let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); let res = guard .current_client()? .nymd - .execute_claim_delegator_reward(mix_identity, 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 compound_delegator_reward( - mix_identity: IdentityKey, - fee: Option, - state: tauri::State<'_, WalletState>, -) -> Result { - log::info!( - ">>> Compound delegator reward: identity_key = {}", - mix_identity - ); - let guard = state.read().await; - let fee_amount = guard.convert_tx_fee(fee.as_ref()); - let res = guard - .current_client()? - .nymd - .execute_compound_delegator_reward(mix_identity, fee) + .withdraw_delegator_reward(mix_id, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -98,32 +50,34 @@ pub async fn compound_delegator_reward( #[tauri::command] pub async fn claim_locked_and_unlocked_delegator_reward( - mix_identity: IdentityKey, + mix_id: NodeId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result, BackendError> { log::info!( - ">>> Claim delegator reward (locked and unlocked): identity_key = {}", - mix_identity + ">>> Claim delegator reward (locked and unlocked): mix_id = {}", + mix_id ); - log::trace!(">>> Get delegations: identity_key = {}", mix_identity); - let address = nymd_client!(state).address().to_string(); - let delegations = nymd_client!(state) - .get_delegator_delegations_paged(address.clone(), None, None) // get all delegations, ignoring paging - .await? - .delegations; + let guard = state.read().await; + let client = guard.current_client()?; + + log::trace!(">>> Get delegations: mix_id = {}", mix_id); + let address = client.nymd.address(); + let delegations = client.get_all_delegator_delegations(address).await?; log::trace!("<<< {} delegations", delegations.len()); + let vesting_contract = client.nymd.vesting_contract_address().to_string(); + let liquid_delegation = client.get_delegation_details(mix_id, address, None).await?; + let vesting_delegation = client + .get_delegation_details(mix_id, address, Some(vesting_contract)) + .await?; + + drop(guard); + // decide which contracts to use, could be neither - let did_delegate_with_mixnet_contract = delegations - .iter() - .filter(|f| f.node_identity == mix_identity) - .any(|f| f.proxy.is_none()); - let did_delegate_with_vesting_contract = delegations - .iter() - .filter(|f| f.node_identity == mix_identity) - .any(|f| f.proxy.is_some()); + let did_delegate_with_mixnet_contract = liquid_delegation.delegation.is_some(); + let did_delegate_with_vesting_contract = vesting_delegation.delegation.is_some(); log::trace!( "<<< Delegations done with: mixnet contract = {}, vesting contract = {}", @@ -133,58 +87,10 @@ pub async fn claim_locked_and_unlocked_delegator_reward( let mut res: Vec = vec![]; if did_delegate_with_mixnet_contract { - res.push(claim_delegator_reward(mix_identity.clone(), fee.clone(), state.clone()).await?); + res.push(claim_delegator_reward(mix_id, fee.clone(), state.clone()).await?); } if did_delegate_with_vesting_contract { - res.push(vesting_claim_delegator_reward(mix_identity, fee, state).await?); - } - log::trace!("<<< {:?}", res); - Ok(res) -} - -#[tauri::command] -pub async fn compound_locked_and_unlocked_delegator_reward( - mix_identity: IdentityKey, - fee: Option, - state: tauri::State<'_, WalletState>, -) -> Result, BackendError> { - log::info!( - ">>> Compound delegator reward (locked and unlocked): identity_key = {}", - mix_identity - ); - - log::trace!(">>> Get delegations: identity_key = {}", mix_identity); - let address = nymd_client!(state).address().to_string(); - let delegations = nymd_client!(state) - .get_delegator_delegations_paged(address.clone(), None, None) // get all delegations, ignoring paging - .await? - .delegations; - log::trace!("<<< {} delegations", delegations.len()); - - // decide which contracts to use, could be neither - let did_delegate_with_mixnet_contract = delegations - .iter() - .filter(|f| f.node_identity == mix_identity) - .any(|f| f.proxy.is_none()); - let did_delegate_with_vesting_contract = delegations - .iter() - .filter(|f| f.node_identity == mix_identity) - .any(|f| f.proxy.is_some()); - - log::trace!( - "<<< Delegations done with: mixnet contract = {}, vesting contract = {}", - did_delegate_with_mixnet_contract, - did_delegate_with_vesting_contract - ); - - let mut res: Vec = vec![]; - if did_delegate_with_mixnet_contract { - res.push( - compound_delegator_reward(mix_identity.clone(), fee.clone(), state.clone()).await?, - ); - } - if did_delegate_with_vesting_contract { - res.push(vesting_compound_delegator_reward(mix_identity, fee, state).await?); + res.push(vesting_claim_delegator_reward(mix_id, fee, state).await?); } log::trace!("<<< {:?}", res); Ok(res) diff --git a/nym-wallet/src-tauri/src/operations/simulate/admin.rs b/nym-wallet/src-tauri/src/operations/simulate/admin.rs index 50a559105b..29636bebf2 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/admin.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/admin.rs @@ -13,14 +13,18 @@ pub async fn simulate_update_contract_settings( state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; - let mixnet_contract_settings_params: ContractStateParams = params.try_into()?; + let reg = guard.registered_coins()?; + let mixnet_contract_settings_params: ContractStateParams = + params.try_convert_to_mixnet_contract_params(reg)?; let client = guard.current_client()?; let mixnet_contract = client.nymd.mixnet_contract_address(); let msg = client.nymd.wrap_contract_execute_message( mixnet_contract, - &ExecuteMsg::UpdateContractStateParams(mixnet_contract_settings_params), + &ExecuteMsg::UpdateContractStateParams { + updated_parameters: mixnet_contract_settings_params, + }, vec![], )?; diff --git a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs index 794a1f09fe..c41e692356 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs @@ -4,9 +4,36 @@ use crate::error::BackendError; use crate::operations::simulate::FeeDetails; use crate::WalletState; -use mixnet_contract_common::IdentityKey; -use mixnet_contract_common::{ExecuteMsg, Gateway, MixNode}; +use mixnet_contract_common::MixNodeConfigUpdate; +use mixnet_contract_common::{ExecuteMsg, Gateway, MixNode, NodeId}; use nym_types::currency::DecCoin; +use nym_types::mixnode::MixNodeCostParams; + +async fn simulate_mixnet_operation( + msg: ExecuteMsg, + raw_funds: Option, + state: &WalletState, +) -> Result { + let guard = state.read().await; + let funds = if let Some(funds) = raw_funds + .map(|c| guard.attempt_convert_to_base_coin(c)) + .transpose()? + { + vec![funds] + } else { + Vec::new() + }; + + let client = guard.current_client()?; + let mixnet_contract = client.nymd.mixnet_contract_address(); + + let msg = client + .nymd + .wrap_contract_execute_message(mixnet_contract, &msg, funds)?; + + let result = client.nymd.simulate(vec![msg]).await?; + guard.create_detailed_fee(result) +} #[tauri::command] pub async fn simulate_bond_gateway( @@ -15,202 +42,118 @@ pub async fn simulate_bond_gateway( owner_signature: String, state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - let pledge = guard.attempt_convert_to_base_coin(pledge)?; - - let client = guard.current_client()?; - let mixnet_contract = client.nymd.mixnet_contract_address(); - - // TODO: I'm still not 100% convinced whether this should be exposed here or handled somewhere else in the client code - let msg = client.nymd.wrap_contract_execute_message( - mixnet_contract, - &ExecuteMsg::BondGateway { + simulate_mixnet_operation( + ExecuteMsg::BondGateway { gateway, owner_signature, }, - vec![pledge], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + Some(pledge), + &state, + ) + .await } #[tauri::command] pub async fn simulate_unbond_gateway( state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - let client = guard.current_client()?; - let mixnet_contract = client.nymd.mixnet_contract_address(); - - let msg = client.nymd.wrap_contract_execute_message( - mixnet_contract, - &ExecuteMsg::UnbondGateway {}, - vec![], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + simulate_mixnet_operation(ExecuteMsg::UnbondGateway {}, None, &state).await } #[tauri::command] pub async fn simulate_bond_mixnode( mixnode: MixNode, + cost_params: MixNodeCostParams, owner_signature: String, pledge: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; - let pledge = guard.attempt_convert_to_base_coin(pledge)?; + let reg = guard.registered_coins()?; + let cost_params = cost_params.try_convert_to_mixnet_contract_cost_params(reg)?; - let client = guard.current_client()?; - let mixnet_contract = client.nymd.mixnet_contract_address(); - - let msg = client.nymd.wrap_contract_execute_message( - mixnet_contract, - &ExecuteMsg::BondMixnode { + simulate_mixnet_operation( + ExecuteMsg::BondMixnode { mix_node: mixnode, + cost_params, owner_signature, }, - vec![pledge], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + Some(pledge), + &state, + ) + .await } #[tauri::command] pub async fn simulate_unbond_mixnode( state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - let client = guard.current_client()?; - let mixnet_contract = client.nymd.mixnet_contract_address(); - - let msg = client.nymd.wrap_contract_execute_message( - mixnet_contract, - &ExecuteMsg::UnbondMixnode {}, - vec![], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + simulate_mixnet_operation(ExecuteMsg::UnbondMixnode {}, None, &state).await } #[tauri::command] -pub async fn simulate_update_mixnode( - profit_margin_percent: u8, +pub async fn simulate_update_mixnode_cost_params( + new_costs: MixNodeCostParams, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; - let client = guard.current_client()?; - let mixnet_contract = client.nymd.mixnet_contract_address(); + let reg = guard.registered_coins()?; + let new_costs = new_costs.try_convert_to_mixnet_contract_cost_params(reg)?; - let msg = client.nymd.wrap_contract_execute_message( - mixnet_contract, - &ExecuteMsg::UpdateMixnodeConfig { - profit_margin_percent, - }, - vec![], - )?; + simulate_mixnet_operation( + ExecuteMsg::UpdateMixnodeCostParams { new_costs }, + None, + &state, + ) + .await +} - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) +#[tauri::command] +pub async fn simulate_update_mixnode_config( + update: MixNodeConfigUpdate, + state: tauri::State<'_, WalletState>, +) -> Result { + simulate_mixnet_operation( + ExecuteMsg::UpdateMixnodeConfig { new_config: update }, + None, + &state, + ) + .await } #[tauri::command] pub async fn simulate_delegate_to_mixnode( - identity: &str, + mix_id: NodeId, amount: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - let delegation = guard.attempt_convert_to_base_coin(amount)?; - - let client = guard.current_client()?; - let mixnet_contract = client.nymd.mixnet_contract_address(); - - let msg = client.nymd.wrap_contract_execute_message( - mixnet_contract, - &ExecuteMsg::DelegateToMixnode { - mix_identity: identity.to_string(), - }, - vec![delegation], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + simulate_mixnet_operation( + ExecuteMsg::DelegateToMixnode { mix_id }, + Some(amount), + &state, + ) + .await } #[tauri::command] pub async fn simulate_undelegate_from_mixnode( - identity: &str, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - let client = guard.current_client()?; - let mixnet_contract = client.nymd.mixnet_contract_address(); - - let msg = client.nymd.wrap_contract_execute_message( - mixnet_contract, - &ExecuteMsg::UndelegateFromMixnode { - mix_identity: identity.to_string(), - }, - vec![], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + simulate_mixnet_operation(ExecuteMsg::UndelegateFromMixnode { mix_id }, None, &state).await } #[tauri::command] pub async fn simulate_claim_operator_reward( state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - let client = guard.current_client()?; - - let result = client.nymd.simulate_claim_operator_reward(None).await?; - guard.create_detailed_fee(result) -} - -#[tauri::command] -pub async fn simulate_compound_operator_reward( - state: tauri::State<'_, WalletState>, -) -> Result { - let guard = state.read().await; - let client = guard.current_client()?; - - let result = client.nymd.simulate_compound_operator_reward(None).await?; - guard.create_detailed_fee(result) + simulate_mixnet_operation(ExecuteMsg::WithdrawOperatorReward {}, None, &state).await } #[tauri::command] pub async fn simulate_claim_delegator_reward( - mix_identity: IdentityKey, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - let client = guard.current_client()?; - - let result = client - .nymd - .simulate_claim_delegator_reward(mix_identity, None) - .await?; - guard.create_detailed_fee(result) -} - -#[tauri::command] -pub async fn simulate_compound_delegator_reward( - mix_identity: IdentityKey, - state: tauri::State<'_, WalletState>, -) -> Result { - let guard = state.read().await; - let client = guard.current_client()?; - - let result = client - .nymd - .simulate_compound_delegator_reward(mix_identity, None) - .await?; - guard.create_detailed_fee(result) + simulate_mixnet_operation(ExecuteMsg::WithdrawDelegatorReward { mix_id }, None, &state).await } diff --git a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs index d8850d202c..bd9a5b341a 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs @@ -4,11 +4,38 @@ use crate::error::BackendError; use crate::operations::simulate::FeeDetails; use crate::WalletState; -use mixnet_contract_common::IdentityKey; -use mixnet_contract_common::{Gateway, MixNode}; +use mixnet_contract_common::MixNodeConfigUpdate; +use mixnet_contract_common::{Gateway, MixNode, NodeId}; use nym_types::currency::DecCoin; +use nym_types::mixnode::MixNodeCostParams; use vesting_contract_common::ExecuteMsg; +async fn simulate_vesting_operation( + msg: ExecuteMsg, + raw_funds: Option, + state: &WalletState, +) -> Result { + let guard = state.read().await; + let funds = if let Some(funds) = raw_funds + .map(|c| guard.attempt_convert_to_base_coin(c)) + .transpose()? + { + vec![funds] + } else { + Vec::new() + }; + + let client = guard.current_client()?; + let vesting_contract = client.nymd.vesting_contract_address(); + + let msg = client + .nymd + .wrap_contract_execute_message(vesting_contract, &msg, funds)?; + + let result = client.nymd.simulate(vec![msg]).await?; + guard.create_detailed_fee(result) +} + #[tauri::command] pub async fn simulate_vesting_bond_gateway( gateway: Gateway, @@ -17,157 +44,113 @@ pub async fn simulate_vesting_bond_gateway( state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; - let pledge = guard.attempt_convert_to_base_coin(pledge)?; + let amount = guard.attempt_convert_to_base_coin(pledge)?.into(); - let client = guard.current_client()?; - let vesting_contract = client.nymd.vesting_contract_address(); - - let msg = client.nymd.wrap_contract_execute_message( - vesting_contract, - &ExecuteMsg::BondGateway { + simulate_vesting_operation( + ExecuteMsg::BondGateway { gateway, owner_signature, - amount: pledge.into(), + amount, }, - vec![], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + None, + &state, + ) + .await } #[tauri::command] pub async fn simulate_vesting_unbond_gateway( state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - - let client = guard.current_client()?; - let vesting_contract = client.nymd.vesting_contract_address(); - - let msg = client.nymd.wrap_contract_execute_message( - vesting_contract, - &ExecuteMsg::UnbondGateway {}, - vec![], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + simulate_vesting_operation(ExecuteMsg::UnbondGateway {}, None, &state).await } #[tauri::command] pub async fn simulate_vesting_bond_mixnode( mixnode: MixNode, + cost_params: MixNodeCostParams, owner_signature: String, pledge: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; - let pledge = guard.attempt_convert_to_base_coin(pledge)?; + let reg = guard.registered_coins()?; + let cost_params = cost_params.try_convert_to_mixnet_contract_cost_params(reg)?; + let amount = guard.attempt_convert_to_base_coin(pledge)?.into(); - let client = guard.current_client()?; - let vesting_contract = client.nymd.vesting_contract_address(); - - let msg = client.nymd.wrap_contract_execute_message( - vesting_contract, - &ExecuteMsg::BondMixnode { + simulate_vesting_operation( + ExecuteMsg::BondMixnode { mix_node: mixnode, + cost_params, owner_signature, - amount: pledge.into(), + amount, }, - vec![], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + None, + &state, + ) + .await } #[tauri::command] pub async fn simulate_vesting_unbond_mixnode( state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - - let client = guard.current_client()?; - let vesting_contract = client.nymd.vesting_contract_address(); - - let msg = client.nymd.wrap_contract_execute_message( - vesting_contract, - &ExecuteMsg::UnbondMixnode {}, - vec![], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + simulate_vesting_operation(ExecuteMsg::UnbondMixnode {}, None, &state).await } #[tauri::command] -pub async fn simulate_vesting_update_mixnode( - profit_margin_percent: u8, +pub async fn simulate_vesting_update_mixnode_cost_params( + new_costs: MixNodeCostParams, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; + let reg = guard.registered_coins()?; + let new_costs = new_costs.try_convert_to_mixnet_contract_cost_params(reg)?; - let client = guard.current_client()?; - let vesting_contract = client.nymd.vesting_contract_address(); + simulate_vesting_operation( + ExecuteMsg::UpdateMixnodeCostParams { new_costs }, + None, + &state, + ) + .await +} - let msg = client.nymd.wrap_contract_execute_message( - vesting_contract, - &ExecuteMsg::UpdateMixnodeConfig { - profit_margin_percent, - }, - vec![], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) +#[tauri::command] +pub async fn simulate_vesting_update_mixnode_config( + update: MixNodeConfigUpdate, + state: tauri::State<'_, WalletState>, +) -> Result { + simulate_vesting_operation( + ExecuteMsg::UpdateMixnodeConfig { new_config: update }, + None, + &state, + ) + .await } #[tauri::command] pub async fn simulate_vesting_delegate_to_mixnode( - identity: &str, + mix_id: NodeId, amount: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; let amount = guard.attempt_convert_to_base_coin(amount)?.into(); - let client = guard.current_client()?; - let vesting_contract = client.nymd.vesting_contract_address(); - - let msg = client.nymd.wrap_contract_execute_message( - vesting_contract, - &ExecuteMsg::DelegateToMixnode { - mix_identity: identity.to_string(), - amount, - }, - vec![], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + simulate_vesting_operation( + ExecuteMsg::DelegateToMixnode { mix_id, amount }, + None, + &state, + ) + .await } #[tauri::command] pub async fn simulate_vesting_undelegate_from_mixnode( - identity: &str, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - - let client = guard.current_client()?; - let vesting_contract = client.nymd.vesting_contract_address(); - - let msg = client.nymd.wrap_contract_execute_message( - vesting_contract, - &ExecuteMsg::UndelegateFromMixnode { - mix_identity: identity.to_string(), - }, - vec![], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + simulate_vesting_operation(ExecuteMsg::UndelegateFromMixnode { mix_id }, None, &state).await } #[tauri::command] @@ -177,74 +160,20 @@ pub async fn simulate_withdraw_vested_coins( ) -> Result { let guard = state.read().await; let amount = guard.attempt_convert_to_base_coin(amount)?.into(); - - let client = guard.current_client()?; - let vesting_contract = client.nymd.vesting_contract_address(); - - let msg = client.nymd.wrap_contract_execute_message( - vesting_contract, - &ExecuteMsg::WithdrawVestedCoins { amount }, - vec![], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + simulate_vesting_operation(ExecuteMsg::WithdrawVestedCoins { amount }, None, &state).await } #[tauri::command] pub async fn simulate_vesting_claim_operator_reward( state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - let client = guard.current_client()?; - - let result = client - .nymd - .simulate_vesting_claim_operator_reward(None) - .await?; - guard.create_detailed_fee(result) -} - -#[tauri::command] -pub async fn simulate_vesting_compound_operator_reward( - state: tauri::State<'_, WalletState>, -) -> Result { - let guard = state.read().await; - let client = guard.current_client()?; - - let result = client - .nymd - .simulate_vesting_compound_operator_reward(None) - .await?; - guard.create_detailed_fee(result) + simulate_vesting_operation(ExecuteMsg::ClaimOperatorReward {}, None, &state).await } #[tauri::command] pub async fn simulate_vesting_claim_delegator_reward( - mix_identity: IdentityKey, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - let client = guard.current_client()?; - - let result = client - .nymd - .simulate_vesting_claim_delegator_reward(mix_identity, None) - .await?; - guard.create_detailed_fee(result) -} - -#[tauri::command] -pub async fn simulate_vesting_compound_delegator_reward( - mix_identity: IdentityKey, - state: tauri::State<'_, WalletState>, -) -> Result { - let guard = state.read().await; - let client = guard.current_client()?; - - let result = client - .nymd - .simulate_vesting_compound_delegator_reward(mix_identity, None) - .await?; - guard.create_detailed_fee(result) + simulate_vesting_operation(ExecuteMsg::ClaimDelegatorReward { mix_id }, None, &state).await } diff --git a/nym-wallet/src-tauri/src/operations/validator_api/status.rs b/nym-wallet/src-tauri/src/operations/validator_api/status.rs index 7ff60d3e6b..56fdc23e9c 100644 --- a/nym-wallet/src-tauri/src/operations/validator_api/status.rs +++ b/nym-wallet/src-tauri/src/operations/validator_api/status.rs @@ -4,28 +4,29 @@ use crate::api_client; use crate::error::BackendError; use crate::state::WalletState; +use mixnet_contract_common::{IdentityKeyRef, NodeId}; use validator_client::models::{ - CoreNodeStatusResponse, InclusionProbabilityResponse, MixnodeStatusResponse, - RewardEstimationResponse, StakeSaturationResponse, + GatewayCoreStatusResponse, InclusionProbabilityResponse, MixnodeCoreStatusResponse, + MixnodeStatusResponse, RewardEstimationResponse, StakeSaturationResponse, }; #[tauri::command] pub async fn mixnode_core_node_status( - identity: &str, + mix_id: NodeId, since: Option, state: tauri::State<'_, WalletState>, -) -> Result { +) -> Result { Ok(api_client!(state) - .get_mixnode_core_status_count(identity, since) + .get_mixnode_core_status_count(mix_id, since) .await?) } #[tauri::command] pub async fn gateway_core_node_status( - identity: &str, + identity: IdentityKeyRef<'_>, since: Option, state: tauri::State<'_, WalletState>, -) -> Result { +) -> Result { Ok(api_client!(state) .get_gateway_core_status_count(identity, since) .await?) @@ -33,38 +34,38 @@ pub async fn gateway_core_node_status( #[tauri::command] pub async fn mixnode_status( - identity: &str, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { - Ok(api_client!(state).get_mixnode_status(identity).await?) + Ok(api_client!(state).get_mixnode_status(mix_id).await?) } #[tauri::command] pub async fn mixnode_reward_estimation( - identity: &str, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state) - .get_mixnode_reward_estimation(identity) + .get_mixnode_reward_estimation(mix_id) .await?) } #[tauri::command] pub async fn mixnode_stake_saturation( - identity: &str, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state) - .get_mixnode_stake_saturation(identity) + .get_mixnode_stake_saturation(mix_id) .await?) } #[tauri::command] pub async fn mixnode_inclusion_probability( - identity: &str, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state) - .get_mixnode_inclusion_probability(identity) + .get_mixnode_inclusion_probability(mix_id) .await?) } diff --git a/nym-wallet/src-tauri/src/operations/vesting/bond.rs b/nym-wallet/src-tauri/src/operations/vesting/bond.rs index c567e4e908..99f0086d9c 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/bond.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/bond.rs @@ -2,8 +2,10 @@ use crate::error::BackendError; use crate::nymd_client; use crate::state::WalletState; use crate::{Gateway, MixNode}; +use mixnet_contract_common::MixNodeConfigUpdate; use nym_types::currency::DecCoin; +use nym_types::mixnode::MixNodeCostParams; use nym_types::transaction::TransactionExecuteResult; use validator_client::nymd::{Fee, VestingSigningClient}; @@ -60,26 +62,29 @@ pub async fn vesting_unbond_gateway( #[tauri::command] pub async fn vesting_bond_mixnode( mixnode: MixNode, + cost_params: MixNodeCostParams, owner_signature: String, pledge: DecCoin, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; + let reg = guard.registered_coins()?; let pledge_base = guard.attempt_convert_to_base_coin(pledge.clone())?; let fee_amount = guard.convert_tx_fee(fee.as_ref()); + let cost_params = cost_params.try_convert_to_mixnet_contract_cost_params(reg)?; log::info!( - ">>> Bond mixnode with locked tokens: identity_key = {}, pledge_display = {}, pledge_base = {}, fee = {:?}", - mixnode.identity_key, - pledge, - pledge_base, - fee - ); + ">>> Bond mixnode with locked tokens: identity_key = {}, pledge_display = {}, pledge_base = {}, fee = {:?}", + mixnode.identity_key, + pledge, + pledge_base, + fee + ); let res = guard .current_client()? .nymd - .vesting_bond_mixnode(mixnode, &owner_signature, pledge_base, fee) + .vesting_bond_mixnode(mixnode, cost_params, &owner_signature, pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -140,22 +145,50 @@ pub async fn withdraw_vested_coins( } #[tauri::command] -pub async fn vesting_update_mixnode( - profit_margin_percent: u8, +pub async fn vesting_update_mixnode_cost_params( + new_costs: MixNodeCostParams, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; + let reg = guard.registered_coins()?; let fee_amount = guard.convert_tx_fee(fee.as_ref()); + let cost_params = new_costs.try_convert_to_mixnet_contract_cost_params(reg)?; + log::info!( - ">>> Update mixnode bonded with locked tokens: profit_margin_percent = {}, fee = {:?}", - profit_margin_percent, + ">>> Update mixnode cost params with locked tokens: parameters = {}, fee = {:?}", + cost_params.to_inline_json(), fee, ); let res = guard .current_client()? .nymd - .vesting_update_mixnode_config(profit_margin_percent, fee) + .vesting_update_mixnode_cost_params(cost_params, 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 vesting_update_mixnode_config( + update: MixNodeConfigUpdate, + fee: Option, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.read().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + log::info!( + ">>> Update mixnode config with locked tokens: update = {}, fee = {:?}", + update.to_inline_json(), + fee, + ); + let res = guard + .current_client()? + .nymd + .vesting_update_mixnode_config(update, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); diff --git a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs index 9ddde467a8..28bb8eaa6d 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs @@ -1,40 +1,16 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + use crate::error::BackendError; use crate::state::WalletState; +use mixnet_contract_common::NodeId; use nym_types::currency::DecCoin; -use nym_types::delegation::DelegationEvent; use nym_types::transaction::TransactionExecuteResult; use validator_client::nymd::{Fee, VestingSigningClient}; -#[tauri::command] -pub async fn get_pending_vesting_delegation_events( - state: tauri::State<'_, WalletState>, -) -> Result, BackendError> { - log::info!(">>> Get pending delegations from vesting contract"); - - let guard = state.read().await; - let reg = guard.registered_coins()?; - let client = &guard.current_client()?.nymd; - let vesting_contract = client.vesting_contract_address(); - - let events = client - .get_pending_delegation_events( - client.address().to_string(), - Some(vesting_contract.to_string()), - ) - .await?; - - log::info!("<<< {} events", events.len()); - log::trace!("<<< {:?}", events); - - Ok(events - .into_iter() - .map(|event| DelegationEvent::from_mixnet_contract(event, reg)) - .collect::>()?) -} - #[tauri::command] pub async fn vesting_delegate_to_mixnode( - identity: &str, + mix_id: NodeId, amount: DecCoin, fee: Option, state: tauri::State<'_, WalletState>, @@ -44,16 +20,16 @@ pub async fn vesting_delegate_to_mixnode( let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Delegate to mixnode with locked tokens: identity_key = {}, amount_display = {}, amount_base = {}, fee = {:?}", - identity, - amount, - delegation, - fee - ); + ">>> Delegate to mixnode with locked tokens: mix_id = {}, amount_display = {}, amount_base = {}, fee = {:?}", + mix_id, + amount, + delegation, + fee + ); let res = guard .current_client()? .nymd - .vesting_delegate_to_mixnode(identity, delegation, fee) + .vesting_delegate_to_mixnode(mix_id, delegation, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -64,21 +40,21 @@ pub async fn vesting_delegate_to_mixnode( #[tauri::command] pub async fn vesting_undelegate_from_mixnode( - identity: &str, + mix_id: NodeId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Undelegate from mixnode delegated with locked tokens: identity_key = {}, fee = {:?}", - identity, + ">>> Undelegate from mixnode delegated with locked tokens: mix_id = {}, fee = {:?}", + mix_id, fee, ); let res = guard .current_client()? .nymd - .vesting_undelegate_from_mixnode(identity, fee) + .vesting_undelegate_from_mixnode(mix_id, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); diff --git a/nym-wallet/src-tauri/src/operations/vesting/queries.rs b/nym-wallet/src-tauri/src/operations/vesting/queries.rs index 7d0d726a1c..d05608fb25 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/queries.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/queries.rs @@ -1,3 +1,6 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + use crate::error::BackendError; use crate::nymd_client; use crate::state::WalletState; diff --git a/nym-wallet/src-tauri/src/operations/vesting/rewards.rs b/nym-wallet/src-tauri/src/operations/vesting/rewards.rs index b3a190c1ff..347ae3576f 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/rewards.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/rewards.rs @@ -1,6 +1,9 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + use crate::error::BackendError; use crate::state::WalletState; -use mixnet_contract_common::IdentityKey; +use mixnet_contract_common::NodeId; use nym_types::transaction::TransactionExecuteResult; use validator_client::nymd::Fee; @@ -15,27 +18,7 @@ pub async fn vesting_claim_operator_reward( let res = guard .current_client()? .nymd - .execute_vesting_claim_operator_reward(None) - .await?; - log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); - Ok(TransactionExecuteResult::from_execute_result( - res, fee_amount, - )?) -} - -#[tauri::command] -pub async fn vesting_compound_operator_reward( - fee: Option, - state: tauri::State<'_, WalletState>, -) -> Result { - log::info!(">>> Vesting account: compound operator reward"); - let guard = state.read().await; - let fee_amount = guard.convert_tx_fee(fee.as_ref()); - let res = guard - .current_client()? - .nymd - .execute_vesting_compound_operator_reward(fee) + .execute_vesting_withdraw_operator_reward(None) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -46,44 +29,20 @@ pub async fn vesting_compound_operator_reward( #[tauri::command] pub async fn vesting_claim_delegator_reward( - mix_identity: IdentityKey, + mix_id: NodeId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { log::info!( - ">>> Vesting account: claim delegator reward: identity_key = {}", - mix_identity + ">>> Vesting account: claim delegator reward: mix_id = {}", + mix_id ); let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); let res = guard .current_client()? .nymd - .execute_vesting_claim_delegator_reward(mix_identity, 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 vesting_compound_delegator_reward( - mix_identity: IdentityKey, - fee: Option, - state: tauri::State<'_, WalletState>, -) -> Result { - log::info!( - ">>> Vesting account: compound delegator reward: identity_key = {}", - mix_identity - ); - let guard = state.read().await; - let fee_amount = guard.convert_tx_fee(fee.as_ref()); - let res = guard - .current_client()? - .nymd - .execute_vesting_compound_delegator_reward(mix_identity, fee) + .execute_vesting_withdraw_delegator_reward(mix_id, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); diff --git a/nym-wallet/src-tauri/src/state.rs b/nym-wallet/src-tauri/src/state.rs index 458aa3e418..572c0bddf4 100644 --- a/nym-wallet/src-tauri/src/state.rs +++ b/nym-wallet/src-tauri/src/state.rs @@ -1,9 +1,10 @@ use crate::config; use crate::error::BackendError; use crate::simulate::SimulateResult; +use cosmwasm_std::Decimal; use itertools::Itertools; use log::warn; -use nym_types::currency::{DecCoin, RegisteredCoins}; +use nym_types::currency::{DecCoin, Denom, RegisteredCoins}; use nym_types::fees::FeeDetails; use nym_wallet_types::network::Network; use nym_wallet_types::network_config; @@ -105,6 +106,24 @@ impl WalletStateInner { Ok(registered_coins.attempt_convert_to_display_dec_coin(coin)?) } + pub(crate) fn display_coin_from_base_decimal( + &self, + base_denom: &Denom, + base_amount: Decimal, + ) -> Result { + let registered_coins = self + .registered_coins + .get(&self.current_network) + .ok_or_else(|| BackendError::UnknownCoinDenom(base_denom.clone()))?; + + Ok(registered_coins + .attempt_create_display_coin_from_base_dec_amount(base_denom, base_amount)?) + } + + pub(crate) fn default_zero_mix_display_coin(&self) -> DecCoin { + self.current_network.default_zero_mix_display_coin() + } + pub(crate) fn registered_coins(&self) -> Result<&RegisteredCoins, BackendError> { self.registered_coins .get(&self.current_network) diff --git a/nym-wallet/src-tauri/src/utils.rs b/nym-wallet/src-tauri/src/utils.rs index 304004a6ef..1efcd43ea1 100644 --- a/nym-wallet/src-tauri/src/utils.rs +++ b/nym-wallet/src-tauri/src/utils.rs @@ -1,9 +1,16 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + use crate::error::BackendError; use crate::nymd_client; use crate::state::WalletState; +use cosmwasm_std::Decimal; +use mixnet_contract_common::{IdentityKey, NodeId, Percent}; use nym_types::currency::DecCoin; +use nym_types::mixnode::MixNodeCostParams; use nym_wallet_types::app::AppEnv; use serde::{Deserialize, Serialize}; +use validator_client::nymd::traits::MixnetQueryClient; use validator_client::nymd::{tx, Coin, CosmosCoin, Gas, GasPrice}; fn get_env_as_option(key: &str) -> Option { @@ -25,19 +32,54 @@ pub fn get_env() -> AppEnv { #[tauri::command] pub async fn owns_mixnode(state: tauri::State<'_, WalletState>) -> Result { Ok(nymd_client!(state) - .owns_mixnode(nymd_client!(state).address()) + .get_owned_mixnode(nymd_client!(state).address()) .await? + .mixnode_details .is_some()) } #[tauri::command] pub async fn owns_gateway(state: tauri::State<'_, WalletState>) -> Result { Ok(nymd_client!(state) - .owns_gateway(nymd_client!(state).address()) + .get_owned_gateway(nymd_client!(state).address()) .await? + .gateway .is_some()) } +#[tauri::command] +pub async fn try_convert_pubkey_to_mix_id( + state: tauri::State<'_, WalletState>, + mix_identity: IdentityKey, +) -> Result, BackendError> { + let res = nymd_client!(state) + .get_mixnode_details_by_identity(mix_identity) + .await?; + Ok(res.map(|mixnode_details| mixnode_details.mix_id())) +} + +#[tauri::command] +pub async fn default_mixnode_cost_params( + state: tauri::State<'_, WalletState>, + profit_margin_percent: Percent, +) -> Result { + // attaches the old pre-update default operating cost of 40 nym per interval + let guard = state.read().await; + + // since this is only a temporary solution until users are required to provide their own cost + // params, we can make the assumption that it's always safe to use the mix denom here + let current_network = guard.current_network(); + let denom = current_network.mix_denom().display; + + Ok(MixNodeCostParams { + profit_margin_percent, + interval_operating_cost: DecCoin { + denom: denom.into(), + amount: Decimal::from_atomics(40u32, 0).unwrap(), + }, + }) +} + #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize)] pub enum Operation { Upload, diff --git a/nym-wallet/src/components/Accounts/AccountAvatar.tsx b/nym-wallet/src/components/Accounts/AccountAvatar.tsx index 9335c1a743..c5ed939f2d 100644 --- a/nym-wallet/src/components/Accounts/AccountAvatar.tsx +++ b/nym-wallet/src/components/Accounts/AccountAvatar.tsx @@ -4,7 +4,7 @@ import stc from 'string-to-color'; import { TAccount } from 'src/types'; export const AccountAvatar = ({ name, small }: { name: TAccount['name']; small?: boolean }) => ( - + {name?.split('')[0]} diff --git a/nym-wallet/src/components/Accounts/AccountOverview.tsx b/nym-wallet/src/components/Accounts/AccountOverview.tsx index 5df9cdf2ed..95c3245ae3 100644 --- a/nym-wallet/src/components/Accounts/AccountOverview.tsx +++ b/nym-wallet/src/components/Accounts/AccountOverview.tsx @@ -6,7 +6,7 @@ import { AccountAvatar } from './AccountAvatar'; export const AccountOverview = ({ account, onClick }: { account: AccountEntry; onClick: () => void }) => ( - - + + + + Accounts + + + + + + Switch between accounts + + + + {accounts?.map(({ id, address }) => ( + { + if (selectedAccount?.id !== id) { + setAccountToSwitchTo(id); + } + }} + /> + ))} + + + + + + + ); }; diff --git a/nym-wallet/src/components/Accounts/modals/AddAccountModal.tsx b/nym-wallet/src/components/Accounts/modals/AddAccountModal.tsx index 9c03cb164a..8a76dba709 100644 --- a/nym-wallet/src/components/Accounts/modals/AddAccountModal.tsx +++ b/nym-wallet/src/components/Accounts/modals/AddAccountModal.tsx @@ -115,7 +115,7 @@ const NameAccount = ({ onNext, onBack }: { onNext: (value: string) => void; onBa {error} { setValue(e.target.value); diff --git a/nym-wallet/src/components/Accounts/modals/ConfirmPasswordModal.tsx b/nym-wallet/src/components/Accounts/modals/ConfirmPasswordModal.tsx index 29694fcda8..07baa9b8d8 100644 --- a/nym-wallet/src/components/Accounts/modals/ConfirmPasswordModal.tsx +++ b/nym-wallet/src/components/Accounts/modals/ConfirmPasswordModal.tsx @@ -1,6 +1,6 @@ import React, { useContext } from 'react'; -import { Box, Paper, Dialog, DialogTitle, IconButton, Typography } from '@mui/material'; -import { ArrowBack } from '@mui/icons-material'; +import { Paper, Dialog, DialogTitle, Typography } from '@mui/material'; +import { useTheme } from '@mui/material/styles'; import { ConfirmPassword } from 'src/components/ConfirmPassword'; import { AccountsContext } from 'src/context'; @@ -14,28 +14,32 @@ export const ConfirmPasswordModal = ({ onConfirm: (password: string) => Promise; }) => { const { isLoading, error } = useContext(AccountsContext); + const theme = useTheme(); return ( - - Switch account - - Confirm password - - - + + + Switch account + + Confirm password + + + + ); }; diff --git a/nym-wallet/src/components/Accounts/modals/EditAccountModal.tsx b/nym-wallet/src/components/Accounts/modals/EditAccountModal.tsx index 91a0a65e9d..8ddbb207b3 100644 --- a/nym-wallet/src/components/Accounts/modals/EditAccountModal.tsx +++ b/nym-wallet/src/components/Accounts/modals/EditAccountModal.tsx @@ -12,6 +12,7 @@ import { Typography, } from '@mui/material'; import { Close } from '@mui/icons-material'; +import { useTheme } from '@mui/material/styles'; import { AccountsContext } from 'src/context'; export const EditAccountModal = () => { @@ -19,6 +20,8 @@ export const EditAccountModal = () => { const { accountToEdit, dialogToDisplay, setDialogToDisplay, handleEditAccount } = useContext(AccountsContext); + const theme = useTheme(); + useEffect(() => { setAccountName(accountToEdit ? accountToEdit?.id : ''); }, [accountToEdit]); @@ -28,48 +31,51 @@ export const EditAccountModal = () => { open={dialogToDisplay === 'Edit'} onClose={() => setDialogToDisplay('Accounts')} fullWidth - PaperComponent={Paper} - PaperProps={{ elevation: 0 }} + PaperProps={{ + style: { border: `1px solid ${theme.palette.nym.nymWallet.modal.border}` }, + }} > - - - Edit account name - setDialogToDisplay('Accounts')}> - - - - - New wallet address - - - - - + + + Edit account name + setDialogToDisplay('Accounts')}> + + + + + New wallet address + + + + + setAccountName(e.target.value)} + autoFocus + /> + + + + - + disableElevation + variant="contained" + size="large" + onClick={() => { + if (accountToEdit) { + handleEditAccount({ ...accountToEdit, id: accountName }); + setDialogToDisplay('Accounts'); + } + }} + disabled={!accountName?.length} + > + Edit + + + ); }; diff --git a/nym-wallet/src/components/Accounts/modals/MnemonicModal.tsx b/nym-wallet/src/components/Accounts/modals/MnemonicModal.tsx index cffdeea213..bede36211b 100644 --- a/nym-wallet/src/components/Accounts/modals/MnemonicModal.tsx +++ b/nym-wallet/src/components/Accounts/modals/MnemonicModal.tsx @@ -15,6 +15,7 @@ import { ArrowBackSharp } from '@mui/icons-material'; import { AccountsContext } from 'src/context'; import { useClipboard } from 'use-clipboard-copy'; import { PasswordInput, Mnemonic } from 'src/components'; +import { StyledBackButton } from 'src/components/StyledBackButton'; export const MnemonicModal = () => { const [password, setPassword] = useState(''); @@ -50,9 +51,6 @@ export const MnemonicModal = () => { Display mnemonic - - - theme.palette.text.disabled }}> {`Display mnemonic for: ${accountMnemonic?.accountName}`} @@ -80,7 +78,8 @@ export const MnemonicModal = () => { )} - + + {!accountMnemonic.value && ( )} diff --git a/nym-wallet/src/components/Nav.tsx b/nym-wallet/src/components/Nav.tsx index ccb1dcd10d..8d28658443 100644 --- a/nym-wallet/src/components/Nav.tsx +++ b/nym-wallet/src/components/Nav.tsx @@ -28,18 +28,18 @@ export const Nav = () => { Icon: ArrowBack, onClick: handleShowReceiveModal, }, - { - label: 'Bonding', - route: '/bonding', - Icon: Bonding, - onClick: () => navigate('/bonding'), - }, { label: 'Delegation', route: '/delegation', Icon: Delegate, onClick: () => navigate('/delegation'), }, + { + label: 'Bonding', + route: '/bonding', + Icon: Bonding, + onClick: () => navigate('/bonding'), + }, { label: 'Docs', route: '/admin', diff --git a/nym-wallet/src/components/NetworkSelector.tsx b/nym-wallet/src/components/NetworkSelector.tsx index 537815b6fb..00da844d3b 100644 --- a/nym-wallet/src/components/NetworkSelector.tsx +++ b/nym-wallet/src/components/NetworkSelector.tsx @@ -40,7 +40,7 @@ export const NetworkSelector = () => {