diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3ae81652a4..229060b381 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,14 +9,24 @@ on: - 'explorer/**' jobs: + matrix_prep: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + # creates the matrix strategy from build_matrix_includes.json + - uses: actions/checkout@v2 + - id: set-matrix + uses: JoshuaTheMiller/conditional-build-matrix@main + with: + inputFile: '.github/workflows/build_matrix_includes.json' + filter: '[?runOnEvent==`${{ github.event_name }}` || runOnEvent==`always`]' build: + needs: matrix_prep + strategy: + matrix: ${{fromJson(needs.matrix_prep.outputs.matrix)}} runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.rust == 'nightly' || matrix.rust == 'beta' || matrix.os == 'windows-latest' }} - strategy: - matrix: - rust: [stable, beta, nightly] - os: [ubuntu-latest, macos-latest, windows-latest] - 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 @@ -51,6 +61,13 @@ jobs: command: fmt args: --all -- --check + - uses: actions-rs/clippy-check@v1 + name: Clippy checks +# if: matrix.os == 'ubuntu-latest' && matrix.rust == 'stable' + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features + - name: Run clippy uses: actions-rs/cargo@v1 if: ${{ matrix.rust != 'nightly' }} @@ -65,78 +82,21 @@ jobs: with: command: clean - # BUILD - - name: Build gateway with coconut feature + - name: Build all binaries with coconut enabled uses: actions-rs/cargo@v1 with: command: build - args: --bin nym-gateway --features=coconut + args: --all --features=coconut - - name: Build native client with coconut feature - uses: actions-rs/cargo@v1 - with: - command: build - args: --bin nym-client --features=coconut - - - name: Build socks5 client with coconut feature - uses: actions-rs/cargo@v1 - with: - command: build - args: --bin nym-socks5-client --features=coconut - - - name: Build validator-api with coconut feature - uses: actions-rs/cargo@v1 - with: - command: build - args: --bin nym-validator-api --features=coconut - -# TEST - - name: Test gateway with coconut feature + - name: Run all tests with coconut enabled uses: actions-rs/cargo@v1 with: command: test - args: --bin nym-gateway --features=coconut + args: --all --features=coconut - - name: Test native client with coconut feature - uses: actions-rs/cargo@v1 - with: - command: test - args: --bin nym-client --features=coconut - - - name: Test socks5 client with coconut feature - uses: actions-rs/cargo@v1 - with: - command: test - args: --bin nym-socks5-client --features=coconut - - - name: Test validator-api with coconut feature - uses: actions-rs/cargo@v1 - with: - command: test - args: --bin nym-validator-api --features=coconut - -# CLIPPY - - - name: Run clippy on gateway with coconut feature + - name: Run clippy with coconut enabled uses: actions-rs/cargo@v1 + if: ${{ matrix.rust != 'nightly' }} with: command: clippy - args: --bin nym-gateway --features=coconut -- -D warnings - - - name: Run clippy on native client with coconut feature - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --bin nym-client --features=coconut -- -D warnings - - - name: Run clippy on socks5 client with coconut feature - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --bin nym-socks5-client --features=coconut -- -D warnings - - - name: Run clippy on validator-api with coconut feature - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --bin nym-validator-api --features=coconut -- -D warnings \ No newline at end of file + args: --features=coconut -- -D warnings \ No newline at end of file diff --git a/.github/workflows/build_matrix_includes.json b/.github/workflows/build_matrix_includes.json new file mode 100644 index 0000000000..7f383efcbf --- /dev/null +++ b/.github/workflows/build_matrix_includes.json @@ -0,0 +1,50 @@ +[ + { + "os":"ubuntu-latest", + "rust":"stable", + "runOnEvent":"always" + }, + + { + "os":"windows-latest", + "rust":"stable", + "runOnEvent":"pull_request" + }, + { + "os":"macos-latest", + "rust":"stable", + "runOnEvent":"pull_request" + }, + + { + "os":"ubuntu-latest", + "rust":"beta", + "runOnEvent":"pull_request" + }, + { + "os":"windows-latest", + "rust":"beta", + "runOnEvent":"pull_request" + }, + { + "os":"macos-latest", + "rust":"beta", + "runOnEvent":"pull_request" + }, + + { + "os":"ubuntu-latest", + "rust":"nightly", + "runOnEvent":"pull_request" + }, + { + "os":"windows-latest", + "rust":"nightly", + "runOnEvent":"pull_request" + }, + { + "os":"macos-latest", + "rust":"nightly", + "runOnEvent":"pull_request" + } +] \ No newline at end of file diff --git a/.github/workflows/clippy_check.yml b/.github/workflows/clippy_check.yml deleted file mode 100644 index 4a35898557..0000000000 --- a/.github/workflows/clippy_check.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Clippy check - -on: - push: - paths-ignore: - - 'explorer/**' - -jobs: - clippy_check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - run: rustup component add clippy - - uses: actions-rs/clippy-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features \ No newline at end of file diff --git a/.github/workflows/contract_matrix_includes.json b/.github/workflows/contract_matrix_includes.json new file mode 100644 index 0000000000..134938eb53 --- /dev/null +++ b/.github/workflows/contract_matrix_includes.json @@ -0,0 +1,14 @@ +[ + { + "rust":"stable", + "runOnEvent":"always" + }, + { + "rust":"beta", + "runOnEvent":"pull_request" + }, + { + "rust":"nightly", + "runOnEvent":"pull_request" + } +] \ No newline at end of file diff --git a/.github/workflows/erc20_bridge_contract.yml b/.github/workflows/erc20_bridge_contract.yml new file mode 100644 index 0000000000..9046fda870 --- /dev/null +++ b/.github/workflows/erc20_bridge_contract.yml @@ -0,0 +1,58 @@ +name: ERC20 Bridge Contract + +on: [ push, pull_request ] + +jobs: + matrix_prep: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + # creates the matrix strategy from build_matrix_includes.json + - uses: actions/checkout@v2 + - id: set-matrix + uses: JoshuaTheMiller/conditional-build-matrix@main + with: + inputFile: '.github/workflows/contract_matrix_includes.json' + filter: '[?runOnEvent==`${{ github.event_name }}` || runOnEvent==`always`]' + erc20-bridge-contract: + needs: matrix_prep + strategy: + matrix: ${{fromJson(needs.matrix_prep.outputs.matrix)}} + # since it's going to be compiled into wasm, there's absolutely + # no point in running CI on different OS-es + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.rust == 'nightly' }} + steps: + - uses: actions/checkout@v2 + + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + target: wasm32-unknown-unknown + override: true + components: rustfmt, clippy + + - uses: actions-rs/cargo@v1 + env: + RUSTFLAGS: '-C link-arg=-s' + with: + command: build + args: --manifest-path contracts/erc20-bridge/Cargo.toml --target wasm32-unknown-unknown + + - uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path contracts/erc20-bridge/Cargo.toml + + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --manifest-path contracts/erc20-bridge/Cargo.toml -- --check + + - uses: actions-rs/cargo@v1 + if: ${{ matrix.rust != 'nightly' }} + with: + command: clippy + args: --manifest-path contracts/erc20-bridge/Cargo.toml -- -D warnings diff --git a/.github/workflows/mixnet_contract.yml b/.github/workflows/mixnet_contract.yml index 611decb81b..c2c57057c1 100644 --- a/.github/workflows/mixnet_contract.yml +++ b/.github/workflows/mixnet_contract.yml @@ -9,14 +9,26 @@ on: - 'explorer/**' jobs: + matrix_prep: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + # creates the matrix strategy from build_matrix_includes.json + - uses: actions/checkout@v2 + - id: set-matrix + uses: JoshuaTheMiller/conditional-build-matrix@main + with: + inputFile: '.github/workflows/contract_matrix_includes.json' + filter: '[?runOnEvent==`${{ github.event_name }}` || runOnEvent==`always`]' mixnet-contract: # since it's going to be compiled into wasm, there's absolutely # no point in running CI on different OS-es runs-on: ubuntu-latest continue-on-error: ${{ matrix.rust == 'nightly' }} + needs: matrix_prep strategy: - matrix: - rust: [ stable, beta, nightly ] + matrix: ${{fromJson(needs.matrix_prep.outputs.matrix)}} steps: - uses: actions/checkout@v2 @@ -29,6 +41,8 @@ jobs: components: rustfmt, clippy - uses: actions-rs/cargo@v1 + env: + RUSTFLAGS: '-C link-arg=-s' with: command: build args: --manifest-path contracts/mixnet/Cargo.toml --target wasm32-unknown-unknown diff --git a/.github/workflows/network-explorer-lint.yml b/.github/workflows/network-explorer-lint.yml index 290403c0bb..f980a1ea03 100644 --- a/.github/workflows/network-explorer-lint.yml +++ b/.github/workflows/network-explorer-lint.yml @@ -11,7 +11,7 @@ defaults: jobs: build: - runs-on: ubuntu-latest + runs-on: custom-runner-linux steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 diff --git a/.github/workflows/network-explorer.yml b/.github/workflows/network-explorer.yml index 3c7a1959c1..1dd2c89637 100644 --- a/.github/workflows/network-explorer.yml +++ b/.github/workflows/network-explorer.yml @@ -11,7 +11,7 @@ defaults: jobs: build: - runs-on: ubuntu-latest + runs-on: custom-runner-linux steps: - uses: actions/checkout@v2 - name: Install rsync diff --git a/.github/workflows/tauri-wallet-types.yml b/.github/workflows/tauri-wallet-types.yml index 9569390145..0196d3459d 100644 --- a/.github/workflows/tauri-wallet-types.yml +++ b/.github/workflows/tauri-wallet-types.yml @@ -11,7 +11,7 @@ on: jobs: nym-wallet-types: runs-on: ubuntu-latest - if: github.event_name != 'pull_request' + if: ${{ github.event_name != 'pull_request' }} steps: - name: Prepare run: sudo apt-get update && sudo apt-get install -y libpango1.0-dev libatk1.0-dev libgdk-pixbuf2.0-dev libsoup2.4-dev librust-gdk-dev libwebkit2gtk-4.0-dev diff --git a/.github/workflows/wasm_client_build.yml b/.github/workflows/wasm_client_build.yml index b9d40cb65b..cd064c4748 100644 --- a/.github/workflows/wasm_client_build.yml +++ b/.github/workflows/wasm_client_build.yml @@ -1,9 +1,6 @@ name: Wasm Client on: - push: - paths-ignore: - - 'explorer/**' pull_request: paths-ignore: - 'explorer/**' @@ -22,10 +19,11 @@ jobs: override: true components: rustfmt, clippy - - uses: actions-rs/cargo@v1 - with: - command: build - args: --manifest-path clients/webassembly/Cargo.toml --target wasm32-unknown-unknown +# token credentials (non-coconut) don't work for wasm right now +# - uses: actions-rs/cargo@v1 +# with: +# command: build +# args: --manifest-path clients/webassembly/Cargo.toml --target wasm32-unknown-unknown - uses: actions-rs/cargo@v1 with: @@ -47,4 +45,4 @@ jobs: # - uses: actions-rs/cargo@v1 # with: # command: clippy -# args: --manifest-path clients/webassembly/Cargo.toml --target wasm32-unknown-unknown -- -D warnings \ No newline at end of file +# args: --manifest-path clients/webassembly/Cargo.toml --target wasm32-unknown-unknown -- -D warnings diff --git a/.gitignore b/.gitignore index ac7542786c..32512e37ec 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,8 @@ validator-api/v4.json validator-api/v6.json **/node_modules validator-api/keypair +contracts/mixnet/code_id +contracts/mixnet/Justfile +contracts/mixnet/Makefile +validator-config +*.patch \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 034a979e0f..c27860c548 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,6 +93,12 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "arrayvec" version = "0.7.1" @@ -228,6 +234,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "az" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6dff4a1892b54d70af377bf7a17064192e822865791d812957f21e3108c325" + [[package]] name = "base-x" version = "0.2.8" @@ -296,6 +308,18 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitvec" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7774144344a4faa177370406a7ff5f1da24303817368584c6206c8303eb07848" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2" version = "0.8.1" @@ -315,7 +339,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcd555c66291d5f836dbb6883b48660ece810fe25a31f3bdfb911945dff2691f" dependencies = [ "arrayref", - "arrayvec", + "arrayvec 0.7.1", "cc", "cfg-if 1.0.0", "constant_time_eq", @@ -405,12 +429,24 @@ version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538" +[[package]] +name = "byte-slice-cast" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0796d76a983651b4a0ddda16203032759f2fd9103d9181f7c65c06ee8872e6" + [[package]] name = "byte-tools" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" +[[package]] +name = "bytemuck" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72957246c41db82b8ef88a5486143830adeb8227ef9837740bdec67724cf2c5b" + [[package]] name = "byteorder" version = "1.4.3" @@ -943,6 +979,8 @@ name = "credentials" version = "0.1.0" dependencies = [ "coconut-interface", + "crypto", + "network-defaults", "thiserror", "url", "validator-client", @@ -1538,6 +1576,14 @@ dependencies = [ "termcolor", ] +[[package]] +name = "erc20-bridge-contract" +version = "0.1.0" +dependencies = [ + "schemars", + "serde", +] + [[package]] name = "error-chain" version = "0.12.4" @@ -1547,6 +1593,49 @@ dependencies = [ "version_check", ] +[[package]] +name = "ethabi" +version = "14.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01317735d563b3bad2d5f90d2e1799f414165408251abb762510f40e790e69a" +dependencies = [ + "anyhow", + "ethereum-types", + "hex", + "serde", + "serde_json", + "sha3", + "thiserror", + "uint", +] + +[[package]] +name = "ethbloom" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb684ac8fa8f6c5759f788862bb22ec6fe3cb392f6bfd08e3c64b603661e3f8" +dependencies = [ + "crunchy", + "fixed-hash", + "impl-rlp", + "impl-serde", + "tiny-keccak", +] + +[[package]] +name = "ethereum-types" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64b5df66a228d85e4b17e5d6c6aa43b0310898ffe8a85988c4c032357aaabfd" +dependencies = [ + "ethbloom", + "fixed-hash", + "impl-rlp", + "impl-serde", + "primitive-types", + "uint", +] + [[package]] name = "explorer-api" version = "0.1.0" @@ -1642,6 +1731,30 @@ dependencies = [ "winapi", ] +[[package]] +name = "fixed" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d333a26ec13a023c6dff4b7584de4d323cfee2e508f5dd2bbee6669e4f7efdf" +dependencies = [ + "az", + "bytemuck", + "half", + "typenum", +] + +[[package]] +name = "fixed-hash" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" +dependencies = [ + "byteorder", + "rand 0.8.4", + "rustc-hex", + "static_assertions", +] + [[package]] name = "flate2" version = "1.0.22" @@ -1738,6 +1851,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + [[package]] name = "futf" version = "0.1.4" @@ -1847,6 +1966,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + [[package]] name = "futures-util" version = "0.3.17" @@ -1882,20 +2007,27 @@ name = "gateway-client" version = "0.1.0" dependencies = [ "coconut-interface", + "credentials", "crypto", "fluvio-wasm-timer", "futures", "gateway-requests", "getrandom 0.2.3", + "json", "log", + "network-defaults", "nymsphinx", "rand 0.7.3", + "secp256k1", + "thiserror", "tokio", "tokio-tungstenite", "tungstenite", + "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-utils", + "web3", ] [[package]] @@ -2271,6 +2403,12 @@ 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" @@ -2352,6 +2490,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-literal" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e4590e13640f19f249fe3e4eca5113bc4289f2497710378190e7f4bd96f45b" + [[package]] name = "hkd32" version = "0.6.0" @@ -2583,6 +2727,44 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "impl-codec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161ebdfec3c8e3b52bf61c4f3550a1eea4f9579d10dc1b936f3171ebdcd6c443" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-rlp" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" +dependencies = [ + "rlp", +] + +[[package]] +name = "impl-serde" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47ca4d2b6931707a55fce5cf66aff80e2178c8b63bbb4ecb5695cbc870ddf6f" +dependencies = [ + "serde", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5dacb10c5b3bb92d46ba347505a9041e676bb20ad220101326bffb0c93031ee" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "indenter" version = "0.3.3" @@ -2741,6 +2923,27 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" + +[[package]] +name = "jsonrpc-core" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f7f76aef2d054868398427f6c54943cf3d1caa9a7ec7d0c38d69df97a965eb" +dependencies = [ + "futures", + "futures-executor", + "futures-util", + "log", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "k256" version = "0.9.6" @@ -2986,10 +3189,15 @@ dependencies = [ name = "mixnet-contract" version = "0.1.0" dependencies = [ + "az", "cosmwasm-std", + "fixed", + "log", + "network-defaults", "schemars", "serde", "serde_repr", + "thiserror", "ts-rs", ] @@ -3107,6 +3315,7 @@ checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d" name = "network-defaults" version = "0.1.0" dependencies = [ + "hex-literal", "serde", "time 0.3.3", "url", @@ -3299,6 +3508,7 @@ dependencies = [ name = "nym-gateway" version = "0.11.0" dependencies = [ + "bip39", "clap", "coconut-interface", "config", @@ -3307,12 +3517,15 @@ dependencies = [ "dashmap", "dirs", "dotenv", + "erc20-bridge-contract", "futures", + "gateway-client", "gateway-requests", "humantime-serde", "log", "mixnet-client", "mixnode-common", + "network-defaults", "nymsphinx", "pemstore", "pretty_env_logger", @@ -3327,6 +3540,7 @@ dependencies = [ "url", "validator-client", "version-checker", + "web3", ] [[package]] @@ -3438,6 +3652,8 @@ dependencies = [ "pin-project", "pretty_env_logger", "rand 0.7.3", + "rand 0.8.4", + "rand_chacha 0.3.1", "reqwest", "rocket", "rocket_cors", @@ -3713,6 +3929,32 @@ dependencies = [ "system-deps 3.2.0", ] +[[package]] +name = "parity-scale-codec" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373b1a4c1338d9cd3d1fa53b3a11bdab5ab6bd80a20f7f7becd76953ae2be909" +dependencies = [ + "arrayvec 0.7.1", + "bitvec", + "byte-slice-cast", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1557010476e0595c9b568d16dcfb81b93cdeb157612726f5170d31aa707bed27" +dependencies = [ + "proc-macro-crate 1.1.0", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "parking" version = "2.0.0" @@ -4079,6 +4321,19 @@ dependencies = [ "log", ] +[[package]] +name = "primitive-types" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06345ee39fbccfb06ab45f3a1a5798d9dafa04cb8921a76d227040003a234b0e" +dependencies = [ + "fixed-hash", + "impl-codec", + "impl-rlp", + "impl-serde", + "uint", +] + [[package]] name = "proc-macro-crate" version = "0.1.5" @@ -4249,6 +4504,12 @@ dependencies = [ "scheduled-thread-pool", ] +[[package]] +name = "radium" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" + [[package]] name = "rand" version = "0.6.5" @@ -4648,6 +4909,16 @@ dependencies = [ "opaque-debug 0.3.0", ] +[[package]] +name = "rlp" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "999508abb0ae792aabed2460c45b89106d97fe4adac593bdaef433c2605847b5" +dependencies = [ + "bytes", + "rustc-hex", +] + [[package]] name = "rocket" version = "0.5.0-rc.1" @@ -4796,6 +5067,12 @@ dependencies = [ "quote", ] +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + [[package]] name = "rustc_version" version = "0.2.3" @@ -4926,6 +5203,24 @@ dependencies = [ "untrusted", ] +[[package]] +name = "secp256k1" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d03ceae636d0fed5bae6a7f4f664354c5f4fcedf6eef053fef17e49f837d0a" +dependencies = [ + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "827cb7cce42533829c792fc51b82fbf18b125b45a702ef2c8be77fce65463a7b" +dependencies = [ + "cc", +] + [[package]] name = "security-framework" version = "2.4.2" @@ -5292,6 +5587,21 @@ dependencies = [ "ordered-buffer", ] +[[package]] +name = "soketto" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4919971d141dbadaa0e82b5d369e2d7666c98e4625046140615ca363e50d4daa" +dependencies = [ + "base64", + "bytes", + "futures", + "httparse", + "log", + "rand 0.8.4", + "sha-1 0.9.8", +] + [[package]] name = "soup-sys" version = "0.10.0" @@ -5891,6 +6201,12 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.37" @@ -6283,6 +6599,15 @@ dependencies = [ "syn", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tokio" version = "1.12.0" @@ -6380,6 +6705,7 @@ checksum = "08d3725d3efa29485e87311c5b699de63cde14b00ed4d256b8318aa30ca452cd" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "log", "pin-project-lite", @@ -6851,6 +7177,52 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web3" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd24abe6f2b68e0677f843059faea87bcbd4892e39f02886f366d8222c3c540d" +dependencies = [ + "arrayvec 0.5.2", + "base64", + "bytes", + "derive_more", + "ethabi", + "ethereum-types", + "futures", + "futures-timer", + "headers", + "hex", + "jsonrpc-core", + "log", + "parking_lot", + "pin-project", + "reqwest", + "rlp", + "secp256k1", + "serde", + "serde_json", + "soketto", + "tiny-keccak", + "tokio", + "tokio-stream", + "tokio-util", + "url", + "web3-async-native-tls", +] + +[[package]] +name = "web3-async-native-tls" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f6d8d1636b2627fe63518d5a9b38a569405d9c9bc665c43c9c341de57227ebb" +dependencies = [ + "native-tls", + "thiserror", + "tokio", + "url", +] + [[package]] name = "webkit2gtk" version = "0.14.0" @@ -7061,6 +7433,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + [[package]] name = "x11-dl" version = "2.19.1" diff --git a/Cargo.toml b/Cargo.toml index 02ceec5e95..b1e1a1c7f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ members = [ "common/config", "common/credentials", "common/crypto", + "common/erc20-bridge-contract", "common/mixnet-contract", "common/mixnode-common", "common/network-defaults", @@ -62,4 +63,4 @@ default-members = [ "validator-api", ] -exclude = ["explorer", "contracts"] +exclude = ["explorer", "contracts", "tokenomics-py"] diff --git a/README.md b/README.md index 155ebc01ea..cd149437b8 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,6 @@ SPDX-License-Identifier: Apache-2.0 ## The Nym Privacy Platform -This repository contains the Nym mixnet. - The platform is composed of multiple Rust crates. Top-level executable binary crates include: * nym-mixnode - shuffles [Sphinx](https://github.com/nymtech/sphinx) packets together to provide privacy against network-level attackers. @@ -33,6 +31,45 @@ There's a `.env.sample-dev` file provided which you can rename to `.env` if you You can chat to us in [Keybase](https://keybase.io). Download their chat app, then click **Teams -> Join a team**. Type **nymtech.friends** into the team name and hit **continue**. For general chat, hang out in the **#general** channel. Our development takes places in the **#dev** channel. Node operators should be in the **#node-operators** channel. +### Rewards + +Node, node operator and delegator rewards are determined according to the principles laid out in the section 6 of [Nym Whitepaper](https://nymtech.net/nym-whitepaper.pdf). Below is a TLDR of the variables and formulas involved in calculating the epoch rewards. Initial reward pool is set to 250 million Nym, making the circulating supply 750 million Nym. + +|Symbol|Definition| +|---|---| +||global share of rewards available, starts at 2% of the reward pool. +||node reward for mixnode `i`. +||ratio of total node stake (node bond + all delegations) to the token circulating supply. +||ratio of stake operator has plaged to their node to the token circulating supply. +||fraction of total effort undertaken by node `i`, set to `1/k` in testnet Milhon. +||number of nodes stakeholders are incentivised to create, set by the validators, a matter of governance. Currently determined by the `active set` size, and set to 5000 in testnet Milhon. +||Sybil attack resistance parameter - the higher this parameter is set the stronger the reduction in competitivness gets for a Sybil attacker. +||declared profit margin of operator `i`, defaults to 10% in testnet Milhon. +||uptime of node `i`, scaled to 0 - 1, for the rewarding epoch +||cost of operating node `i` for the duration of the rewarding eopoch, set to 40 Nym for testnet Milhon. + +Node reward for node `i` is determined as: + + + +where: + + + +and + + + +Operator of node `i` is credited with the following amount: + + + +Delegate with stake `s` recieves: + + + +where `s'` is stake `s` scaled over total token circulating supply. + ### Licensing and copyright information This program is available as open source under the terms of the Apache 2.0 license. However, some elements are being licensed under CC0-1.0 and MIT. For accurate information, please check individual files. diff --git a/clients/client-core/Cargo.toml b/clients/client-core/Cargo.toml index 3d13ae8234..73078fbfdb 100644 --- a/clients/client-core/Cargo.toml +++ b/clients/client-core/Cargo.toml @@ -30,3 +30,6 @@ validator-client = { path = "../../common/client-libs/validator-client" } [dev-dependencies] tempfile = "3.1.0" + +[features] +coconut = [] \ No newline at end of file diff --git a/clients/client-core/src/config/mod.rs b/clients/client-core/src/config/mod.rs index 6f28610494..7a91bc345c 100644 --- a/clients/client-core/src/config/mod.rs +++ b/clients/client-core/src/config/mod.rs @@ -22,7 +22,10 @@ const DEFAULT_MESSAGE_STREAM_AVERAGE_DELAY: Duration = Duration::from_millis(20) const DEFAULT_AVERAGE_PACKET_DELAY: Duration = Duration::from_millis(50); const DEFAULT_TOPOLOGY_REFRESH_RATE: Duration = Duration::from_secs(5 * 60); // every 5min const DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT: Duration = Duration::from_millis(5_000); -const DEFAULT_GATEWAY_RESPONSE_TIMEOUT: Duration = Duration::from_millis(1_500); +// Set this to a high value for now, so that we don't risk sporadic timeouts that might cause +// bought bandwidth tokens to not have time to be spent; Once we remove the gateway from the +// bandwidth bridging protocol, we can come back to a smaller timeout value +const DEFAULT_GATEWAY_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5 * 60); pub fn missing_string_value() -> String { MISSING_VALUE.to_string() @@ -100,6 +103,17 @@ impl Config { self::Client::::default_reply_encryption_key_store_path(&id); } + #[cfg(not(feature = "coconut"))] + if self + .client + .backup_bandwidth_token_keys_dir + .as_os_str() + .is_empty() + { + self.client.backup_bandwidth_token_keys_dir = + self::Client::::default_backup_bandwidth_token_keys_dir(&id); + } + self.client.id = id; } @@ -111,6 +125,16 @@ impl Config { self.client.gateway_listener = gateway_listener.into(); } + #[cfg(not(feature = "coconut"))] + pub fn with_eth_private_key>(&mut self, eth_private_key: S) { + self.client.eth_private_key = eth_private_key.into(); + } + + #[cfg(not(feature = "coconut"))] + pub fn with_eth_endpoint>(&mut self, eth_endpoint: S) { + self.client.eth_endpoint = eth_endpoint.into(); + } + pub fn set_custom_validator_apis(&mut self, validator_api_urls: Vec) { self.client.validator_api_urls = validator_api_urls; } @@ -173,6 +197,21 @@ impl Config { self.client.gateway_listener.clone() } + #[cfg(not(feature = "coconut"))] + pub fn get_backup_bandwidth_token_keys_dir(&self) -> PathBuf { + self.client.backup_bandwidth_token_keys_dir.clone() + } + + #[cfg(not(feature = "coconut"))] + pub fn get_eth_endpoint(&self) -> String { + self.client.eth_endpoint.clone() + } + + #[cfg(not(feature = "coconut"))] + pub fn get_eth_private_key(&self) -> String { + self.client.eth_private_key.clone() + } + // Debug getters pub fn get_average_packet_delay(&self) -> Duration { self.debug.average_packet_delay @@ -268,6 +307,20 @@ pub struct Client { /// Address of the gateway listener to which all client requests should be sent. gateway_listener: String, + /// Path to directory containing public/private keys used for bandwidth token purchase. + /// Those are saved in case of emergency, to be able to reclaim bandwidth tokens. + /// The public key is the name of the file, while the private key is the content. + #[cfg(not(feature = "coconut"))] + backup_bandwidth_token_keys_dir: PathBuf, + + /// Ethereum private key. + #[cfg(not(feature = "coconut"))] + eth_private_key: String, + + /// Address to an Ethereum full node. + #[cfg(not(feature = "coconut"))] + eth_endpoint: String, + /// nym_home_directory specifies absolute path to the home nym Clients directory. /// It is expected to use default value and hence .toml file should not redefine this field. nym_root_directory: PathBuf, @@ -292,6 +345,12 @@ impl Default for Client { reply_encryption_key_store_path: Default::default(), gateway_id: "".to_string(), gateway_listener: "".to_string(), + #[cfg(not(feature = "coconut"))] + backup_bandwidth_token_keys_dir: Default::default(), + #[cfg(not(feature = "coconut"))] + eth_private_key: "".to_string(), + #[cfg(not(feature = "coconut"))] + eth_endpoint: "".to_string(), nym_root_directory: T::default_root_directory(), super_struct: Default::default(), } @@ -326,6 +385,11 @@ impl Client { fn default_reply_encryption_key_store_path(id: &str) -> PathBuf { T::default_data_directory(Some(id)).join("reply_key_store") } + + #[cfg(not(feature = "coconut"))] + fn default_backup_bandwidth_token_keys_dir(id: &str) -> PathBuf { + T::default_data_directory(Some(id)).join("backup_bandwidth_token_keys") + } } #[derive(Debug, Default, Deserialize, PartialEq, Serialize)] diff --git a/clients/native/src/client/config/template.rs b/clients/native/src/client/config/template.rs index 66b33d7e3c..22ddb22f91 100644 --- a/clients/native/src/client/config/template.rs +++ b/clients/native/src/client/config/template.rs @@ -42,6 +42,17 @@ public_encryption_key_file = '{{ client.public_encryption_key_file }}' # sent but not received back. reply_encryption_key_store_path = '{{ client.reply_encryption_key_store_path }}' +# Path to directory containing public/private keys used for bandwidth token purchase. +# Those are saved in case of emergency, to be able to reclaim bandwidth tokens. +# The public key is the name of the file, while the private key is the content. +backup_bandwidth_token_keys_dir = '{{ client.backup_bandwidth_token_keys_dir }}' + +# Ethereum private key. +eth_private_key = '{{ client.eth_private_key }}' + +# Addess to an Ethereum full node. +eth_endpoint = '{{ client.eth_endpoint }}' + ##### additional client config options ##### # ID of the gateway from which the client should be fetching messages. diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index 0cdde68aa3..49b8de71ff 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -35,10 +35,7 @@ use nymsphinx::anonymous_replies::ReplySurb; use nymsphinx::receiver::ReconstructedMessage; use tokio::runtime::Runtime; -#[cfg(feature = "coconut")] -use coconut_interface::Credential; -#[cfg(feature = "coconut")] -use credentials::{bandwidth::prepare_for_spending, obtain_aggregate_verification_key}; +use gateway_client::bandwidth::BandwidthController; pub(crate) mod config; @@ -168,31 +165,6 @@ impl NymClient { .start(self.runtime.handle()) } - #[cfg(feature = "coconut")] - async fn prepare_coconut_credential(&self) -> Credential { - let verification_key = obtain_aggregate_verification_key( - &self.config.get_base().get_validator_api_endpoints(), - ) - .await - .expect("could not obtain aggregate verification key of validators"); - - let bandwidth_credential = credentials::bandwidth::obtain_signature( - &self.key_manager.identity_keypair().public_key().to_bytes(), - &self.config.get_base().get_validator_api_endpoints(), - ) - .await - .expect("could not obtain bandwidth credential"); - // the above would presumably be loaded from a file - - // the below would only be executed once we know where we want to spend it (i.e. which gateway and stuff) - prepare_for_spending( - &self.key_manager.identity_keypair().public_key().to_bytes(), - &bandwidth_credential, - &verification_key, - ) - .expect("could not prepare out bandwidth credential for spending") - } - fn start_gateway_client( &mut self, mixnet_message_sender: MixnetMessageSender, @@ -212,7 +184,17 @@ impl NymClient { self.runtime.block_on(async { #[cfg(feature = "coconut")] - let coconut_credential = self.prepare_coconut_credential().await; + let bandwidth_controller = BandwidthController::new( + self.config.get_base().get_validator_api_endpoints(), + *self.key_manager.identity_keypair().public_key(), + ); + #[cfg(not(feature = "coconut"))] + let bandwidth_controller = BandwidthController::new( + self.config.get_base().get_eth_endpoint(), + self.config.get_base().get_eth_private_key(), + self.config.get_base().get_backup_bandwidth_token_keys_dir(), + ) + .expect("Could not create bandwidth controller"); let mut gateway_client = GatewayClient::new( gateway_address, @@ -222,13 +204,11 @@ impl NymClient { mixnet_message_sender, ack_sender, self.config.get_base().get_gateway_response_timeout(), + Some(bandwidth_controller), ); gateway_client - .authenticate_and_start( - #[cfg(feature = "coconut")] - Some(coconut_credential), - ) + .authenticate_and_start() .await .expect("could not authenticate and start up the gateway connection"); diff --git a/clients/native/src/commands/init.rs b/clients/native/src/commands/init.rs index 1994fe216e..a1e4aaeea9 100644 --- a/clients/native/src/commands/init.rs +++ b/clients/native/src/commands/init.rs @@ -22,7 +22,7 @@ use topology::{filter::VersionFilterable, gateway}; use url::Url; pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { - App::new("init") + let app = App::new("init") .about("Initialise a Nym client. Do this first!") .arg(Arg::with_name("id") .long("id") @@ -54,7 +54,21 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { .long("fastmode") .hidden(true) // this will prevent this flag from being displayed in `--help` .help("Mostly debug-related option to increase default traffic rate so that you would not need to modify config post init") - ) + ); + #[cfg(not(feature = "coconut"))] + let app = app + .arg(Arg::with_name("eth_endpoint") + .long("eth_endpoint") + .help("URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20 tokens") + .takes_value(true) + .required(true)) + .arg(Arg::with_name("eth_private_key") + .long("eth_private_key") + .help("Ethereum private key used for obtaining bandwidth tokens from ERC20 tokens") + .takes_value(true) + .required(true)); + + app } async fn register_with_gateway( diff --git a/clients/native/src/commands/mod.rs b/clients/native/src/commands/mod.rs index 475f14a9b6..b7ebad71fb 100644 --- a/clients/native/src/commands/mod.rs +++ b/clients/native/src/commands/mod.rs @@ -43,5 +43,14 @@ pub(crate) fn override_config(mut config: Config, matches: &ArgMatches) -> Confi config = config.with_port(port.unwrap()); } + #[cfg(not(feature = "coconut"))] + if let Some(eth_endpoint) = matches.value_of("eth_endpoint") { + config.get_base_mut().with_eth_endpoint(eth_endpoint); + } + #[cfg(not(feature = "coconut"))] + if let Some(eth_private_key) = matches.value_of("eth_private_key") { + config.get_base_mut().with_eth_private_key(eth_private_key); + } + config } diff --git a/clients/native/src/commands/run.rs b/clients/native/src/commands/run.rs index d08f4ea764..9b46ec7bbc 100644 --- a/clients/native/src/commands/run.rs +++ b/clients/native/src/commands/run.rs @@ -10,7 +10,7 @@ use log::*; use version_checker::is_minor_version_compatible; pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { - App::new("run") + let app = App::new("run") .about("Run the Nym client with provided configuration client optionally overriding set parameters") .arg(Arg::with_name("id") .long("id") @@ -38,7 +38,19 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { .long("port") .help("Port for the socket (if applicable) to listen on") .takes_value(true) - ) + ); + #[cfg(not(feature = "coconut"))] + let app = app + .arg(Arg::with_name("eth_endpoint") + .long("eth_endpoint") + .help("URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20 tokens") + .takes_value(true)) + .arg(Arg::with_name("eth_private_key") + .long("eth_private_key") + .help("Ethereum private key used for obtaining bandwidth tokens from ERC20 tokens") + .takes_value(true)); + + app } // this only checks compatibility between config the binary. It does not take into consideration diff --git a/clients/socks5/src/client/config/template.rs b/clients/socks5/src/client/config/template.rs index 70e196d3c1..775c97ddf4 100644 --- a/clients/socks5/src/client/config/template.rs +++ b/clients/socks5/src/client/config/template.rs @@ -42,6 +42,17 @@ public_encryption_key_file = '{{ client.public_encryption_key_file }}' # sent but not received back. reply_encryption_key_store_path = '{{ client.reply_encryption_key_store_path }}' +# Path to directory containing public/private keys used for bandwidth token purchase. +# Those are saved in case of emergency, to be able to reclaim bandwidth tokens. +# The public key is the name of the file, while the private key is the content. +backup_bandwidth_token_keys_dir = '{{ client.backup_bandwidth_token_keys_dir }}' + +# Ethereum private key. +eth_private_key = '{{ client.eth_private_key }}' + +# Addess to an Ethereum full node. +eth_endpoint = '{{ client.eth_endpoint }}' + ##### additional client config options ##### # ID of the gateway from which the client should be fetching messages. diff --git a/clients/socks5/src/client/mod.rs b/clients/socks5/src/client/mod.rs index 3882694f20..c6e187145a 100644 --- a/clients/socks5/src/client/mod.rs +++ b/clients/socks5/src/client/mod.rs @@ -34,10 +34,7 @@ use nymsphinx::addressing::clients::Recipient; use nymsphinx::addressing::nodes::NodeIdentity; use tokio::runtime::Runtime; -#[cfg(feature = "coconut")] -use coconut_interface::Credential; -#[cfg(feature = "coconut")] -use credentials::{bandwidth::prepare_for_spending, obtain_aggregate_verification_key}; +use gateway_client::bandwidth::BandwidthController; pub(crate) mod config; @@ -156,31 +153,6 @@ impl NymClient { .start(self.runtime.handle()) } - #[cfg(feature = "coconut")] - async fn prepare_coconut_credential(&self) -> Credential { - let verification_key = obtain_aggregate_verification_key( - &self.config.get_base().get_validator_api_endpoints(), - ) - .await - .expect("could not obtain aggregate verification key of validators"); - - let bandwidth_credential = credentials::bandwidth::obtain_signature( - &self.key_manager.identity_keypair().public_key().to_bytes(), - &self.config.get_base().get_validator_api_endpoints(), - ) - .await - .expect("could not obtain bandwidth credential"); - // the above would presumably be loaded from a file - - // the below would only be executed once we know where we want to spend it (i.e. which gateway and stuff) - prepare_for_spending( - &self.key_manager.identity_keypair().public_key().to_bytes(), - &bandwidth_credential, - &verification_key, - ) - .expect("could not prepare out bandwidth credential for spending") - } - fn start_gateway_client( &mut self, mixnet_message_sender: MixnetMessageSender, @@ -200,7 +172,17 @@ impl NymClient { self.runtime.block_on(async { #[cfg(feature = "coconut")] - let coconut_credential = self.prepare_coconut_credential().await; + let bandwidth_controller = BandwidthController::new( + self.config.get_base().get_validator_api_endpoints(), + *self.key_manager.identity_keypair().public_key(), + ); + #[cfg(not(feature = "coconut"))] + let bandwidth_controller = BandwidthController::new( + self.config.get_base().get_eth_endpoint(), + self.config.get_base().get_eth_private_key(), + self.config.get_base().get_backup_bandwidth_token_keys_dir(), + ) + .expect("Could not create bandwidth controller"); let mut gateway_client = GatewayClient::new( gateway_address, @@ -210,13 +192,11 @@ impl NymClient { mixnet_message_sender, ack_sender, self.config.get_base().get_gateway_response_timeout(), + Some(bandwidth_controller), ); gateway_client - .authenticate_and_start( - #[cfg(feature = "coconut")] - Some(coconut_credential), - ) + .authenticate_and_start() .await .expect("could not authenticate and start up the gateway connection"); diff --git a/clients/socks5/src/commands/init.rs b/clients/socks5/src/commands/init.rs index f24dc53322..14bcbda8c6 100644 --- a/clients/socks5/src/commands/init.rs +++ b/clients/socks5/src/commands/init.rs @@ -20,7 +20,7 @@ use topology::{filter::VersionFilterable, gateway}; use url::Url; pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { - App::new("init") + let app = App::new("init") .about("Initialise a Nym client. Do this first!") .arg(Arg::with_name("id") .long("id") @@ -54,7 +54,21 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { .long("fastmode") .hidden(true) // this will prevent this flag from being displayed in `--help` .help("Mostly debug-related option to increase default traffic rate so that you would not need to modify config post init") - ) + ); + #[cfg(not(feature = "coconut"))] + let app = app + .arg(Arg::with_name("eth_endpoint") + .long("eth_endpoint") + .help("URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20 tokens") + .takes_value(true) + .required(true)) + .arg(Arg::with_name("eth_private_key") + .long("eth_private_key") + .help("Ethereum private key used for obtaining bandwidth tokens from ERC20 tokens") + .takes_value(true) + .required(true)); + + app } async fn register_with_gateway( diff --git a/clients/socks5/src/commands/mod.rs b/clients/socks5/src/commands/mod.rs index 7b6ff6c4b5..405b445c50 100644 --- a/clients/socks5/src/commands/mod.rs +++ b/clients/socks5/src/commands/mod.rs @@ -39,5 +39,14 @@ pub(crate) fn override_config(mut config: Config, matches: &ArgMatches) -> Confi config = config.with_port(port.unwrap()); } + #[cfg(not(feature = "coconut"))] + if let Some(eth_endpoint) = matches.value_of("eth_endpoint") { + config.get_base_mut().with_eth_endpoint(eth_endpoint); + } + #[cfg(not(feature = "coconut"))] + if let Some(eth_private_key) = matches.value_of("eth_private_key") { + config.get_base_mut().with_eth_private_key(eth_private_key); + } + config } diff --git a/clients/socks5/src/commands/run.rs b/clients/socks5/src/commands/run.rs index facd85144f..2ddf728d3a 100644 --- a/clients/socks5/src/commands/run.rs +++ b/clients/socks5/src/commands/run.rs @@ -10,7 +10,7 @@ use log::*; use version_checker::is_minor_version_compatible; pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { - App::new("run") + let app = App::new("run") .about("Run the Nym client with provided configuration client optionally overriding set parameters") .arg(Arg::with_name("id") .long("id") @@ -44,7 +44,19 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { .long("port") .help("Port for the socket to listen on") .takes_value(true) - ) + ); + #[cfg(not(feature = "coconut"))] + let app = app + .arg(Arg::with_name("eth_endpoint") + .long("eth_endpoint") + .help("URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20 tokens") + .takes_value(true)) + .arg(Arg::with_name("eth_private_key") + .long("eth_private_key") + .help("Ethereum private key used for obtaining bandwidth tokens from ERC20 tokens") + .takes_value(true)); + + app } // this only checks compatibility between config the binary. It does not take into consideration diff --git a/clients/webassembly/src/client/mod.rs b/clients/webassembly/src/client/mod.rs index ed1397dc43..2ac0e8ef1c 100644 --- a/clients/webassembly/src/client/mod.rs +++ b/clients/webassembly/src/client/mod.rs @@ -17,10 +17,7 @@ use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; use wasm_utils::{console_log, console_warn}; -#[cfg(feature = "coconut")] -use coconut_interface::Credential; -#[cfg(feature = "coconut")] -use credentials::{bandwidth::prepare_for_spending, obtain_aggregate_verification_key}; +use gateway_client::bandwidth::BandwidthController; pub(crate) mod received_processor; @@ -103,35 +100,15 @@ impl NymClient { self.self_recipient().to_string() } - #[cfg(feature = "coconut")] - async fn prepare_coconut_credential(validators: &[Url], identity_bytes: &[u8]) -> Credential { - let verification_key = obtain_aggregate_verification_key(validators) - .await - .expect("could not obtain aggregate verification key of validators"); - - let bandwidth_credential = - credentials::bandwidth::obtain_signature(identity_bytes, validators) - .await - .expect("could not obtain bandwidth credential"); - // the above would presumably be loaded from a file - - // the below would only be executed once we know where we want to spend it (i.e. which gateway and stuff) - prepare_for_spending(identity_bytes, &bandwidth_credential, &verification_key) - .expect("could not prepare out bandwidth credential for spending") - } - // Right now it's impossible to have async exported functions to take `&self` rather than self pub async fn initial_setup(self) -> Self { #[cfg(feature = "coconut")] - let coconut_credential = { - let validator_server = self.validator_server.clone(); - let identity_public_key = self.identity.public_key().clone(); - Self::prepare_coconut_credential( - &vec![validator_server], - &identity_public_key.to_bytes(), - ) - .await - }; + let bandwidth_controller = Some(BandwidthController::new( + vec![self.validator_server.clone()], + *self.identity.public_key(), + )); + #[cfg(not(feature = "coconut"))] + let bandwidth_controller = None; let mut client = self.get_and_update_topology().await; let gateway = client.choose_gateway(); @@ -147,13 +124,11 @@ impl NymClient { mixnet_messages_sender, ack_sender, DEFAULT_GATEWAY_RESPONSE_TIMEOUT, + bandwidth_controller, ); gateway_client - .authenticate_and_start( - #[cfg(feature = "coconut")] - Some(coconut_credential), - ) + .authenticate_and_start() .await .expect("could not authenticate and start up the gateway connection"); diff --git a/common/client-libs/gateway-client/Cargo.toml b/common/client-libs/gateway-client/Cargo.toml index 247352ac31..844191d8b6 100644 --- a/common/client-libs/gateway-client/Cargo.toml +++ b/common/client-libs/gateway-client/Cargo.toml @@ -10,14 +10,19 @@ edition = "2018" # TODO: (for this and other crates), similarly to 'tokio', import only required "futures" modules rather than # the entire crate futures = "0.3" +json = "0.12.4" log = "0.4" +thiserror = "1.0" +url = "2.2" rand = { version = "0.7.3", features = ["wasm-bindgen"] } # internal +credentials = { path = "../../credentials" } crypto = { path = "../../crypto" } gateway-requests = { path = "../../../gateway/gateway-requests" } nymsphinx = { path = "../../nymsphinx" } coconut-interface = { path = "../../coconut-interface", optional = true } +network-defaults = { path = "../../network-defaults" } [dependencies.tungstenite] version = "0.13" @@ -31,6 +36,12 @@ features = ["macros", "rt", "net", "sync", "time"] [target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-tungstenite] version = "0.14" +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.secp256k1] +version = "0.20.3" + +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.web3] +version = "0.17.0" + # wasm-only dependencies [target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen] version = "0.2" diff --git a/common/client-libs/gateway-client/src/bandwidth.rs b/common/client-libs/gateway-client/src/bandwidth.rs new file mode 100644 index 0000000000..12a68e6429 --- /dev/null +++ b/common/client-libs/gateway-client/src/bandwidth.rs @@ -0,0 +1,226 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::GatewayClientError; +#[cfg(feature = "coconut")] +use credentials::coconut::{ + bandwidth::{obtain_signature, prepare_for_spending}, + utils::obtain_aggregate_verification_key, +}; +#[cfg(not(feature = "coconut"))] +use credentials::token::bandwidth::TokenCredential; +#[cfg(not(feature = "coconut"))] +use crypto::asymmetric::identity; +use crypto::asymmetric::identity::PublicKey; +#[cfg(not(feature = "coconut"))] +use network_defaults::{ + eth_contract::ETH_JSON_ABI, BANDWIDTH_VALUE, ETH_BURN_FUNCTION_NAME, ETH_CONTRACT_ADDRESS, + ETH_MIN_BLOCK_DEPTH, TOKENS_TO_BURN, +}; +#[cfg(not(feature = "coconut"))] +use rand::rngs::OsRng; +#[cfg(not(feature = "coconut"))] +use secp256k1::SecretKey; +#[cfg(not(feature = "coconut"))] +use std::io::Write; +#[cfg(not(feature = "coconut"))] +use std::str::FromStr; +#[cfg(not(feature = "coconut"))] +use web3::{ + contract::{Contract, Options}, + transports::Http, + types::{Address, Bytes, U256, U64}, + Web3, +}; + +#[cfg(not(feature = "coconut"))] +pub fn eth_contract(web3: Web3) -> Contract { + Contract::from_json( + web3.eth(), + Address::from(ETH_CONTRACT_ADDRESS), + json::parse(ETH_JSON_ABI) + .expect("Invalid json abi") + .dump() + .as_bytes(), + ) + .expect("Invalid json abi") +} + +#[derive(Clone)] +pub struct BandwidthController { + #[cfg(feature = "coconut")] + validator_endpoints: Vec, + #[cfg(feature = "coconut")] + identity: PublicKey, + #[cfg(not(feature = "coconut"))] + contract: Contract, + #[cfg(not(feature = "coconut"))] + eth_private_key: SecretKey, + #[cfg(not(feature = "coconut"))] + backup_bandwidth_token_keys_dir: std::path::PathBuf, +} + +impl BandwidthController { + #[cfg(feature = "coconut")] + pub fn new(validator_endpoints: Vec, identity: PublicKey) -> Self { + BandwidthController { + validator_endpoints, + identity, + } + } + + #[cfg(not(feature = "coconut"))] + pub fn new( + eth_endpoint: String, + eth_private_key: String, + backup_bandwidth_token_keys_dir: std::path::PathBuf, + ) -> Result { + // Fail early, on invalid url + let transport = + Http::new(ð_endpoint).map_err(|_| GatewayClientError::InvalidURL(eth_endpoint))?; + let web3 = web3::Web3::new(transport); + // Fail early, on invalid abi + let contract = eth_contract(web3); + let eth_private_key = secp256k1::SecretKey::from_str(ð_private_key) + .map_err(|_| GatewayClientError::InvalidEthereumPrivateKey)?; + + Ok(BandwidthController { + contract, + eth_private_key, + backup_bandwidth_token_keys_dir, + }) + } + + #[cfg(not(feature = "coconut"))] + fn backup_keypair(&self, keypair: &identity::KeyPair) -> Result<(), GatewayClientError> { + std::fs::create_dir_all(&self.backup_bandwidth_token_keys_dir)?; + let file_path = self + .backup_bandwidth_token_keys_dir + .join(keypair.public_key().to_base58_string()); + let mut file = std::fs::File::create(file_path)?; + file.write_all(&keypair.private_key().to_bytes())?; + + Ok(()) + } + + #[cfg(feature = "coconut")] + pub async fn prepare_coconut_credential( + &self, + ) -> Result { + let verification_key = obtain_aggregate_verification_key(&self.validator_endpoints).await?; + + let bandwidth_credential = + obtain_signature(&self.identity.to_bytes(), &self.validator_endpoints).await?; + // the above would presumably be loaded from a file + + // the below would only be executed once we know where we want to spend it (i.e. which gateway and stuff) + Ok(prepare_for_spending( + &self.identity.to_bytes(), + &bandwidth_credential, + &verification_key, + )?) + } + + #[cfg(not(feature = "coconut"))] + pub async fn prepare_token_credential( + &self, + gateway_identity: PublicKey, + ) -> Result { + let mut rng = OsRng; + + let kp = identity::KeyPair::new(&mut rng); + self.backup_keypair(&kp)?; + + let verification_key = *kp.public_key(); + let signed_verification_key = kp.private_key().sign(&verification_key.to_bytes()); + self.buy_token_credential(verification_key, signed_verification_key) + .await?; + + let message: Vec = verification_key + .to_bytes() + .iter() + .chain(gateway_identity.to_bytes().iter()) + .copied() + .collect(); + let signature = kp.private_key().sign(&message); + + Ok(TokenCredential::new( + verification_key, + gateway_identity, + BANDWIDTH_VALUE, + signature, + )) + } + + #[cfg(not(feature = "coconut"))] + pub async fn buy_token_credential( + &self, + verification_key: PublicKey, + signed_verification_key: identity::Signature, + ) -> Result<(), GatewayClientError> { + // 0 means a transaction failure, 1 means success + let confirmations = if cfg!(debug_assertions) { + 1 + } else { + ETH_MIN_BLOCK_DEPTH + }; + // 15 seconds per confirmation block + 10 seconds of network overhead + log::info!( + "Waiting for Ethereum transaction. This should take about {} seconds", + confirmations * 15 + 10 + ); + let recipt = self + .contract + .signed_call_with_confirmations( + ETH_BURN_FUNCTION_NAME, + ( + U256::from(TOKENS_TO_BURN), + U256::from(&verification_key.to_bytes()), + Bytes(signed_verification_key.to_bytes().to_vec()), + ), + Options::default(), + confirmations, + &self.eth_private_key, + ) + .await?; + if Some(U64::from(0)) == recipt.status { + Err(GatewayClientError::BurnTokenError( + web3::Error::InvalidResponse(format!( + "Transaction status is 0 (failure): {:?}", + recipt.logs, + )), + )) + } else { + log::info!( + "Bought bandwidth on Ethereum: {} MB", + BANDWIDTH_VALUE / 1024 / 1024 + ); + Ok(()) + } + } +} + +#[cfg(not(feature = "coconut"))] +#[cfg(test)] +mod tests { + use super::*; + use network_defaults::ETH_EVENT_NAME; + + #[test] + fn parse_contract() { + let transport = + Http::new("https://rinkeby.infura.io/v3/00000000000000000000000000000000").unwrap(); + let web3 = web3::Web3::new(transport); + // test no panic occurs + eth_contract(web3); + } + + #[test] + fn check_event_name_constant_against_abi() { + let transport = + Http::new("https://rinkeby.infura.io/v3/00000000000000000000000000000000").unwrap(); + let web3 = web3::Web3::new(transport); + let contract = eth_contract(web3); + assert!(contract.abi().event(ETH_EVENT_NAME).is_ok()); + } +} diff --git a/common/client-libs/gateway-client/src/client.rs b/common/client-libs/gateway-client/src/client.rs index da9fee7bd2..1ec592fb0f 100644 --- a/common/client-libs/gateway-client/src/client.rs +++ b/common/client-libs/gateway-client/src/client.rs @@ -1,6 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::bandwidth::BandwidthController; use crate::cleanup_socket_message; use crate::error::GatewayClientError; use crate::packet_router::PacketRouter; @@ -8,6 +9,10 @@ pub use crate::packet_router::{ AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender, }; use crate::socket_state::{PartiallyDelegated, SocketState}; +#[cfg(feature = "coconut")] +use coconut_interface::Credential; +#[cfg(not(feature = "coconut"))] +use credentials::token::bandwidth::TokenCredential; use crypto::asymmetric::identity; use futures::{FutureExt, SinkExt, StreamExt}; use gateway_requests::authentication::encrypted_address::EncryptedAddressBytes; @@ -30,15 +35,11 @@ use fluvio_wasm_timer as wasm_timer; #[cfg(target_arch = "wasm32")] use wasm_utils::websocket::JSWebsocket; -#[cfg(feature = "coconut")] -use coconut_interface::Credential; - const DEFAULT_RECONNECTION_ATTEMPTS: usize = 10; const DEFAULT_RECONNECTION_BACKOFF: Duration = Duration::from_secs(5); pub struct GatewayClient { authenticated: bool, - #[cfg(feature = "coconut")] bandwidth_remaining: i64, gateway_address: String, gateway_identity: identity::PublicKey, @@ -47,6 +48,7 @@ pub struct GatewayClient { connection: SocketState, packet_router: PacketRouter, response_timeout_duration: Duration, + bandwidth_controller: Option, // reconnection related variables /// Specifies whether client should try to reconnect to gateway on connection failure. @@ -69,20 +71,19 @@ impl GatewayClient { mixnet_message_sender: MixnetMessageSender, ack_sender: AcknowledgementSender, response_timeout_duration: Duration, + bandwidth_controller: Option, ) -> Self { GatewayClient { authenticated: false, - - #[cfg(feature = "coconut")] bandwidth_remaining: 0, gateway_address, - gateway_identity, local_identity, shared_key, connection: SocketState::NotConnected, packet_router: PacketRouter::new(ack_sender, mixnet_message_sender), response_timeout_duration, + bandwidth_controller, should_reconnect_on_failure: true, reconnection_attempts: DEFAULT_RECONNECTION_ATTEMPTS, reconnection_backoff: DEFAULT_RECONNECTION_BACKOFF, @@ -118,10 +119,7 @@ impl GatewayClient { GatewayClient { authenticated: false, - - #[cfg(feature = "coconut")] bandwidth_remaining: 0, - gateway_address, gateway_identity, local_identity, @@ -129,6 +127,7 @@ impl GatewayClient { connection: SocketState::NotConnected, packet_router, response_timeout_duration, + bandwidth_controller: None, should_reconnect_on_failure: false, reconnection_attempts: DEFAULT_RECONNECTION_ATTEMPTS, reconnection_backoff: DEFAULT_RECONNECTION_BACKOFF, @@ -139,7 +138,6 @@ impl GatewayClient { self.gateway_identity } - #[cfg(feature = "coconut")] pub fn remaining_bandwidth(&self) -> i64 { self.bandwidth_remaining } @@ -208,14 +206,7 @@ impl GatewayClient { for i in 1..self.reconnection_attempts { info!("attempt {}...", i); - if self - .authenticate_and_start( - #[cfg(feature = "coconut")] - None, - ) - .await - .is_ok() - { + if self.authenticate_and_start().await.is_ok() { info!("managed to reconnect!"); return Ok(()); } @@ -234,13 +225,7 @@ impl GatewayClient { // final attempt (done separately to be able to return a proper error) info!("attempt {}", self.reconnection_attempts); - match self - .authenticate_and_start( - #[cfg(feature = "coconut")] - None, - ) - .await - { + match self.authenticate_and_start().await { Ok(_) => { info!("managed to reconnect!"); Ok(()) @@ -451,8 +436,12 @@ impl GatewayClient { ClientControlRequest::new_authenticate(self_address, encrypted_address, iv).into(); match self.send_websocket_message(msg).await? { - ServerResponse::Authenticate { status } => { + ServerResponse::Authenticate { + status, + bandwidth_remaining, + } => { self.authenticated = status; + self.bandwidth_remaining = bandwidth_remaining; Ok(()) } ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), @@ -478,22 +467,15 @@ impl GatewayClient { } #[cfg(feature = "coconut")] - pub async fn claim_coconut_bandwidth( + async fn claim_coconut_bandwidth( &mut self, - coconut_credential: Credential, + credential: Credential, ) -> Result<(), GatewayClientError> { - if !self.authenticated { - return Err(GatewayClientError::NotAuthenticated); - } - if self.shared_key.is_none() { - return Err(GatewayClientError::NoSharedKeyAvailable); - } - let mut rng = OsRng; let iv = IV::new_random(&mut rng); let msg = ClientControlRequest::new_enc_coconut_bandwidth_credential( - &coconut_credential, + &credential, self.shared_key.as_ref().unwrap(), iv, ) @@ -507,7 +489,64 @@ impl GatewayClient { Ok(()) } - #[cfg(feature = "coconut")] + #[cfg(not(feature = "coconut"))] + async fn claim_token_bandwidth( + &mut self, + credential: TokenCredential, + ) -> Result<(), GatewayClientError> { + let mut rng = OsRng; + + let iv = IV::new_random(&mut rng); + + let msg = ClientControlRequest::new_enc_token_bandwidth_credential( + &credential, + self.shared_key.as_ref().unwrap(), + iv, + ) + .into(); + self.bandwidth_remaining = match self.send_websocket_message(msg).await? { + ServerResponse::Bandwidth { available_total } => Ok(available_total), + ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), + _ => Err(GatewayClientError::UnexpectedResponse), + }?; + + Ok(()) + } + + pub async fn claim_bandwidth(&mut self) -> Result<(), GatewayClientError> { + if !self.authenticated { + return Err(GatewayClientError::NotAuthenticated); + } + if self.shared_key.is_none() { + return Err(GatewayClientError::NoSharedKeyAvailable); + } + if self.bandwidth_controller.is_none() { + return Err(GatewayClientError::NoBandwidthControllerAvailable); + } + + warn!("Not enough bandwidth. Trying to get more bandwidth, this might take a while"); + + #[cfg(feature = "coconut")] + let credential = self + .bandwidth_controller + .as_ref() + .unwrap() + .prepare_coconut_credential() + .await?; + #[cfg(not(feature = "coconut"))] + let credential = self + .bandwidth_controller + .as_ref() + .unwrap() + .prepare_token_credential(self.gateway_identity) + .await?; + + #[cfg(feature = "coconut")] + return self.claim_coconut_bandwidth(credential).await; + #[cfg(not(feature = "coconut"))] + return self.claim_token_bandwidth(credential).await; + } + fn estimate_required_bandwidth(&self, packets: &[MixPacket]) -> i64 { packets .iter() @@ -522,9 +561,16 @@ impl GatewayClient { if !self.authenticated { return Err(GatewayClientError::NotAuthenticated); } - #[cfg(feature = "coconut")] - if self.estimate_required_bandwidth(&packets) < self.bandwidth_remaining { - return Err(GatewayClientError::NotEnoughBandwidth); + if self.estimate_required_bandwidth(&packets) > self.bandwidth_remaining { + // Try to claim more bandwidth first, and return an error only if that is still not + // enough (the current granularity for bandwidth should be sufficient) + self.claim_bandwidth().await?; + if self.estimate_required_bandwidth(&packets) > self.bandwidth_remaining { + return Err(GatewayClientError::NotEnoughBandwidth( + self.estimate_required_bandwidth(&packets), + self.bandwidth_remaining, + )); + } } if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); @@ -590,9 +636,16 @@ impl GatewayClient { if !self.authenticated { return Err(GatewayClientError::NotAuthenticated); } - #[cfg(feature = "coconut")] if (mix_packet.sphinx_packet().len() as i64) > self.bandwidth_remaining { - return Err(GatewayClientError::NotEnoughBandwidth); + // Try to claim more bandwidth first, and return an error only if that is still not + // enough + self.claim_bandwidth().await?; + if (mix_packet.sphinx_packet().len() as i64) > self.bandwidth_remaining { + return Err(GatewayClientError::NotEnoughBandwidth( + mix_packet.sphinx_packet().len() as i64, + self.bandwidth_remaining, + )); + } } if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); @@ -629,10 +682,6 @@ impl GatewayClient { if !self.authenticated { return Err(GatewayClientError::NotAuthenticated); } - #[cfg(feature = "coconut")] - if self.bandwidth_remaining <= 0 { - return Err(GatewayClientError::NotEnoughBandwidth); - } if self.connection.is_partially_delegated() { return Ok(()); } @@ -660,22 +709,12 @@ impl GatewayClient { Ok(()) } - pub async fn authenticate_and_start( - &mut self, - #[cfg(feature = "coconut")] coconut_credential: Option, - ) -> Result, GatewayClientError> { + pub async fn authenticate_and_start(&mut self) -> Result, GatewayClientError> { if !self.connection.is_established() { self.establish_connection().await?; } let shared_key = self.perform_initial_authentication().await?; - #[cfg(feature = "coconut")] - { - if let Some(coconut_credential) = coconut_credential { - self.claim_coconut_bandwidth(coconut_credential).await?; - } - } - // this call is NON-blocking self.start_listening_for_mixnet_messages()?; diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs index 170314e484..f396b3e06b 100644 --- a/common/client-libs/gateway-client/src/error.rs +++ b/common/client-libs/gateway-client/src/error.rs @@ -2,39 +2,83 @@ // SPDX-License-Identifier: Apache-2.0 use gateway_requests::registration::handshake::error::HandshakeError; -use std::fmt::{self, Error, Formatter}; use std::io; +use thiserror::Error; use tungstenite::Error as WsError; #[cfg(target_arch = "wasm32")] use wasm_bindgen::JsValue; +#[cfg(not(feature = "coconut"))] +use web3::Error as Web3Error; -#[derive(Debug)] +#[derive(Debug, Error)] pub enum GatewayClientError { + #[error("Connection to the gateway is not established")] ConnectionNotEstablished, + + #[error("Gateway returned an error response - {0}")] GatewayError(String), - NetworkError(WsError), + + #[error("There was a network error - {0}")] + NetworkError(#[from] WsError), // TODO: see if `JsValue` is a reasonable type for this #[cfg(target_arch = "wasm32")] + #[error("There was a network error")] NetworkErrorWasm(JsValue), - NoSharedKeyAvailable, - ConnectionAbruptlyClosed, - MalformedResponse, - SerializeCredential, - NotAuthenticated, - NotEnoughBandwidth, - UnexpectedResponse, - ConnectionInInvalidState, - RegistrationFailure(HandshakeError), - AuthenticationFailure, - Timeout, -} + #[cfg(not(feature = "coconut"))] + #[error("Could not backup keypair - {0}")] + IOError(#[from] std::io::Error), -impl From for GatewayClientError { - fn from(err: WsError) -> Self { - GatewayClientError::NetworkError(err) - } + #[cfg(not(feature = "coconut"))] + #[error("Could not burn ERC20 token in Ethereum smart contract - {0}")] + BurnTokenError(#[from] Web3Error), + + #[cfg(not(feature = "coconut"))] + #[error("Invalid Ethereum private key")] + InvalidEthereumPrivateKey, + + #[error("Invalid URL - {0}")] + InvalidURL(String), + + #[error("No shared key was provided or obtained")] + NoSharedKeyAvailable, + + #[error("No bandwidth controller provided")] + NoBandwidthControllerAvailable, + + #[error("Credential error - {0}")] + CredentialError(#[from] credentials::error::Error), + + #[error("Connection was abruptly closed")] + ConnectionAbruptlyClosed, + + #[error("Received response was malformed")] + MalformedResponse, + + #[error("Credential could not be serialized")] + SerializeCredential, + + #[error("Client is not authenticated")] + NotAuthenticated, + + #[error("Client does not have enough bandwidth: estimated {0}, remaining: {1}")] + NotEnoughBandwidth(i64, i64), + + #[error("Received an unexpected response")] + UnexpectedResponse, + + #[error("Connection is in an invalid state - please send a bug report")] + ConnectionInInvalidState, + + #[error("Failed to finish registration handshake - {0}")] + RegistrationFailure(HandshakeError), + + #[error("Authentication failure")] + AuthenticationFailure, + + #[error("Timed out")] + Timeout, } impl GatewayClientError { @@ -54,60 +98,3 @@ impl GatewayClientError { } } } - -#[cfg(target_arch = "wasm32")] -impl From for GatewayClientError { - fn from(err: JsValue) -> Self { - GatewayClientError::NetworkErrorWasm(err) - } -} - -// better human readable representation of the error, mostly so that GatewayClientError -// would implement std::error::Error -impl fmt::Display for GatewayClientError { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { - match self { - GatewayClientError::ConnectionNotEstablished => { - write!(f, "connection to the gateway is not established") - } - GatewayClientError::NoSharedKeyAvailable => { - write!(f, "no shared key was provided or obtained") - } - GatewayClientError::NotAuthenticated => write!(f, "client is not authenticated"), - - GatewayClientError::NetworkError(err) => { - write!(f, "there was a network error - {}", err) - } - #[cfg(target_arch = "wasm32")] - GatewayClientError::NetworkErrorWasm(err) => { - write!(f, "there was a network error - {:?}", err) - } - - GatewayClientError::ConnectionAbruptlyClosed => { - write!(f, "connection was abruptly closed") - } - GatewayClientError::Timeout => write!(f, "timed out"), - GatewayClientError::MalformedResponse => write!(f, "received response was malformed"), - GatewayClientError::ConnectionInInvalidState => write!( - f, - "connection is in an invalid state - please send a bug report" - ), - GatewayClientError::RegistrationFailure(handshake_err) => write!( - f, - "failed to finish registration handshake - {}", - handshake_err - ), - GatewayClientError::AuthenticationFailure => write!(f, "authentication failure"), - GatewayClientError::GatewayError(err) => { - write!(f, "gateway returned an error response - {}", err) - } - GatewayClientError::UnexpectedResponse => write!(f, "received an unexpected response"), - GatewayClientError::NotEnoughBandwidth => { - write!(f, "client does not have enough bandwidth") - } - GatewayClientError::SerializeCredential => { - write!(f, "credential could not be serialized") - } - } - } -} diff --git a/common/client-libs/gateway-client/src/lib.rs b/common/client-libs/gateway-client/src/lib.rs index f1f0987392..dc7b17d8b4 100644 --- a/common/client-libs/gateway-client/src/lib.rs +++ b/common/client-libs/gateway-client/src/lib.rs @@ -8,6 +8,7 @@ pub use packet_router::{ }; use tungstenite::{protocol::Message, Error as WsError}; +pub mod bandwidth; pub mod client; pub mod error; pub mod packet_router; diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index b82e9907b3..9fba57208f 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -119,7 +119,7 @@ impl PartiallyDelegated { } .is_err() { - panic!("failed to send back `mixnet_receiver_future` result on the oneshot channel") + warn!("failed to send back `mixnet_receiver_future` result on the oneshot channel") } }; diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 0e2533b37d..c50a9cffd0 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -10,9 +10,9 @@ use mixnet_contract::StateParams; use crate::{validator_api, ValidatorClientError}; use coconut_interface::{BlindSignRequestBody, BlindedSignatureResponse, VerificationKeyResponse}; -#[cfg(feature = "nymd-client")] -use mixnet_contract::RawDelegationData; use mixnet_contract::{GatewayBond, MixNodeBond}; +#[cfg(feature = "nymd-client")] +use mixnet_contract::{RawDelegationData, RewardingIntervalResponse}; use url::Url; #[cfg(feature = "nymd-client")] @@ -172,6 +172,43 @@ impl Client { Ok(self.nymd.get_state_params().await?) } + pub async fn get_current_rewarding_interval( + &self, + ) -> Result + where + C: CosmWasmClient + Sync, + { + Ok(self.nymd.get_current_rewarding_interval().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_epoch_reward_percent(&self) -> Result + where + C: CosmWasmClient + Sync, + { + Ok(self.nymd.get_epoch_reward_percent().await?) + } + // basically handles paging for us pub async fn get_all_nymd_mixnodes(&self) -> Result, ValidatorClientError> where diff --git a/common/client-libs/validator-client/src/nymd/fee_helpers.rs b/common/client-libs/validator-client/src/nymd/fee_helpers.rs index f63daf83b4..95b505704f 100644 --- a/common/client-libs/validator-client/src/nymd/fee_helpers.rs +++ b/common/client-libs/validator-client/src/nymd/fee_helpers.rs @@ -23,10 +23,11 @@ pub enum Operation { BondGateway, UnbondGateway, - DelegateToGateway, - UndelegateFromGateway, UpdateStateParams, + + BeginMixnodeRewarding, + FinishMixnodeRewarding, } pub(crate) fn calculate_fee(gas_price: &GasPrice, gas_limit: Gas) -> Coin { @@ -43,13 +44,13 @@ impl fmt::Display for Operation { Operation::Send => f.write_str("Send"), Operation::BondMixnode => f.write_str("BondMixnode"), Operation::UnbondMixnode => f.write_str("UnbondMixnode"), - Operation::DelegateToMixnode => f.write_str("DelegateToMixnode"), - Operation::UndelegateFromMixnode => f.write_str("UndelegateFromMixnode"), Operation::BondGateway => f.write_str("BondGateway"), Operation::UnbondGateway => f.write_str("UnbondGateway"), - Operation::DelegateToGateway => f.write_str("DelegateToGateway"), - Operation::UndelegateFromGateway => f.write_str("UndelegateFromGateway"), + Operation::DelegateToMixnode => f.write_str("DelegateToMixnode"), + Operation::UndelegateFromMixnode => f.write_str("UndelegateFromMixnode"), Operation::UpdateStateParams => f.write_str("UpdateStateParams"), + Operation::BeginMixnodeRewarding => f.write_str("BeginMixnodeRewarding"), + Operation::FinishMixnodeRewarding => f.write_str("FinishMixnodeRewarding"), } } } @@ -71,10 +72,10 @@ impl Operation { Operation::BondGateway => 175_000u64.into(), Operation::UnbondGateway => 175_000u64.into(), - Operation::DelegateToGateway => 175_000u64.into(), - Operation::UndelegateFromGateway => 175_000u64.into(), Operation::UpdateStateParams => 175_000u64.into(), + Operation::BeginMixnodeRewarding => 175_000u64.into(), + Operation::FinishMixnodeRewarding => 175_000u64.into(), } } diff --git a/common/client-libs/validator-client/src/nymd/mod.rs b/common/client-libs/validator-client/src/nymd/mod.rs index 54477ca013..815076a664 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -11,12 +11,13 @@ use crate::nymd::fee_helpers::Operation; use crate::nymd::wallet::DirectSecp256k1HdWallet; use cosmrs::rpc::endpoint::broadcast; use cosmrs::rpc::{Error as TendermintRpcError, HttpClientUrl}; -use cosmwasm_std::Coin; +use cosmwasm_std::{Coin, Uint128}; use mixnet_contract::{ Addr, Delegation, ExecuteMsg, Gateway, GatewayOwnershipResponse, IdentityKey, LayerDistribution, MixNode, MixOwnershipResponse, PagedAllDelegationsResponse, PagedGatewayResponse, PagedMixDelegationsResponse, PagedMixnodeResponse, - PagedReverseMixDelegationsResponse, QueryMsg, RawDelegationData, StateParams, + PagedReverseMixDelegationsResponse, QueryMsg, RawDelegationData, RewardingIntervalResponse, + StateParams, }; use serde::Serialize; use std::collections::HashMap; @@ -27,10 +28,11 @@ pub use crate::nymd::cosmwasm_client::signing_client::SigningCosmWasmClient; pub use crate::nymd::gas_price::GasPrice; pub use cosmrs::rpc::HttpClient as QueryNymdClient; pub use cosmrs::tendermint::block::Height; +pub use cosmrs::tendermint::hash; pub use cosmrs::tendermint::Time as TendermintTime; pub use cosmrs::tx::{Fee, Gas}; pub use cosmrs::Coin as CosmosCoin; -pub use cosmrs::{AccountId, Denom}; +pub use cosmrs::{AccountId, Decimal, Denom}; pub use signing_client::Client as SigningNymdClient; pub mod cosmwasm_client; @@ -184,6 +186,21 @@ impl NymdClient { self.client.get_height().await } + /// Obtains the hash of a block specified by the provided height. + /// + /// # Arguments + /// + /// * `height`: height of the block for which we want to obtain the hash. + pub async fn get_block_hash(&self, height: u32) -> Result + where + C: CosmWasmClient + Sync, + { + self.client + .get_block(Some(height)) + .await + .map(|block| block.block_id.hash) + } + pub async fn get_balance(&self, address: &AccountId) -> Result, NymdError> where C: CosmWasmClient + Sync, @@ -201,6 +218,18 @@ impl NymdClient { .await } + pub async fn get_current_rewarding_interval( + &self, + ) -> Result + where + C: CosmWasmClient + Sync, + { + let request = QueryMsg::CurrentRewardingInterval {}; + self.client + .query_contract_smart(self.contract_address()?, &request) + .await + } + pub async fn get_layer_distribution(&self) -> Result where C: CosmWasmClient + Sync, @@ -211,6 +240,46 @@ impl NymdClient { .await } + pub async fn get_reward_pool(&self) -> Result + where + C: CosmWasmClient + Sync, + { + let request = QueryMsg::GetRewardPool {}; + self.client + .query_contract_smart(self.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.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.contract_address()?, &request) + .await + } + + pub async fn get_epoch_reward_percent(&self) -> Result + where + C: CosmWasmClient + Sync, + { + let request = QueryMsg::GetEpochRewardPercent {}; + self.client + .query_contract_smart(self.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 where @@ -642,6 +711,54 @@ impl NymdClient { ) .await } + + pub async fn begin_mixnode_rewarding( + &self, + rewarding_interval_nonce: u32, + ) -> Result + where + C: SigningCosmWasmClient + Sync, + { + let fee = self.get_fee(Operation::BeginMixnodeRewarding); + + let req = ExecuteMsg::BeginMixnodeRewarding { + rewarding_interval_nonce, + }; + self.client + .execute( + self.address(), + self.contract_address()?, + &req, + fee, + "Beginning mixnode rewarding procedure", + Vec::new(), + ) + .await + } + + pub async fn finish_mixnode_rewarding( + &self, + rewarding_interval_nonce: u32, + ) -> Result + where + C: SigningCosmWasmClient + Sync, + { + let fee = self.get_fee(Operation::FinishMixnodeRewarding); + + let req = ExecuteMsg::FinishMixnodeRewarding { + rewarding_interval_nonce, + }; + self.client + .execute( + self.address(), + self.contract_address()?, + &req, + fee, + "Finishing mixnode rewarding procedure", + Vec::new(), + ) + .await + } } fn cosmwasm_coin_to_cosmos_coin(coin: Coin) -> CosmosCoin { diff --git a/common/credentials/Cargo.toml b/common/credentials/Cargo.toml index 273d099898..8536669e00 100644 --- a/common/credentials/Cargo.toml +++ b/common/credentials/Cargo.toml @@ -11,4 +11,6 @@ url = "2.2" # I guess temporarily until we get serde support in coconut up and running coconut-interface = { path = "../coconut-interface" } +crypto = { path = "../crypto" } +network-defaults = { path = "../network-defaults" } validator-client = { path = "../client-libs/validator-client" } diff --git a/common/credentials/src/bandwidth.rs b/common/credentials/src/coconut/bandwidth.rs similarity index 92% rename from common/credentials/src/bandwidth.rs rename to common/credentials/src/coconut/bandwidth.rs index 5f3c898526..39364cf018 100644 --- a/common/credentials/src/bandwidth.rs +++ b/common/credentials/src/coconut/bandwidth.rs @@ -8,11 +8,10 @@ use url::Url; +use super::utils::{obtain_aggregate_signature, prepare_credential_for_spending}; use crate::error::Error; -use crate::utils::{obtain_aggregate_signature, prepare_credential_for_spending}; use coconut_interface::{hash_to_scalar, Credential, Parameters, Signature, VerificationKey}; - -const BANDWIDTH_VALUE: u64 = 10 * 1024 * 1024 * 1024; // 10 GB +use network_defaults::BANDWIDTH_VALUE; pub const PUBLIC_ATTRIBUTES: u32 = 1; pub const PRIVATE_ATTRIBUTES: u32 = 1; diff --git a/common/credentials/src/coconut/mod.rs b/common/credentials/src/coconut/mod.rs new file mode 100644 index 0000000000..bf480ad58a --- /dev/null +++ b/common/credentials/src/coconut/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod bandwidth; +pub mod utils; diff --git a/common/credentials/src/utils.rs b/common/credentials/src/coconut/utils.rs similarity index 100% rename from common/credentials/src/utils.rs rename to common/credentials/src/coconut/utils.rs diff --git a/common/credentials/src/lib.rs b/common/credentials/src/lib.rs index 099f348079..436f788261 100644 --- a/common/credentials/src/lib.rs +++ b/common/credentials/src/lib.rs @@ -1,8 +1,8 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -pub mod bandwidth; +pub mod coconut; pub mod error; -mod utils; +pub mod token; -pub use utils::{obtain_aggregate_signature, obtain_aggregate_verification_key}; +pub use coconut::utils::{obtain_aggregate_signature, obtain_aggregate_verification_key}; diff --git a/common/credentials/src/token/bandwidth.rs b/common/credentials/src/token/bandwidth.rs new file mode 100644 index 0000000000..79ed4cc9aa --- /dev/null +++ b/common/credentials/src/token/bandwidth.rs @@ -0,0 +1,140 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crypto::asymmetric::identity::{PublicKey, Signature, PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH}; + +use crate::error::Error; +use std::convert::TryInto; + +#[cfg(not(feature = "coconut"))] +pub struct TokenCredential { + verification_key: PublicKey, + gateway_identity: PublicKey, + bandwidth: u64, + signature: Signature, +} + +#[cfg(not(feature = "coconut"))] +impl TokenCredential { + pub fn new( + verification_key: PublicKey, + gateway_identity: PublicKey, + bandwidth: u64, + signature: Signature, + ) -> Self { + TokenCredential { + verification_key, + gateway_identity, + bandwidth, + signature, + } + } + + pub fn verification_key(&self) -> PublicKey { + self.verification_key + } + + pub fn gateway_identity(&self) -> PublicKey { + self.gateway_identity + } + + pub fn bandwidth(&self) -> u64 { + self.bandwidth + } + + pub fn signature_bytes(&self) -> [u8; 64] { + self.signature.to_bytes() + } + + pub fn verify_signature(&self) -> bool { + let message: Vec = self + .verification_key + .to_bytes() + .iter() + .chain(self.gateway_identity.to_bytes().iter()) + .copied() + .collect(); + self.verification_key + .verify(&message, &self.signature) + .is_ok() + } + + pub fn to_bytes(&self) -> Vec { + self.verification_key + .to_bytes() + .iter() + .chain(self.gateway_identity.to_bytes().iter()) + .chain(self.bandwidth.to_be_bytes().iter()) + .chain(self.signature.to_bytes().iter()) + .copied() + .collect() + } + + pub fn from_bytes(b: &[u8]) -> Result { + if b.len() != 2 * PUBLIC_KEY_LENGTH + 8 + SIGNATURE_LENGTH { + return Err(Error::BandwidthCredentialError); + } + let verification_key = PublicKey::from_bytes(&b[..PUBLIC_KEY_LENGTH]) + .map_err(|_| Error::BandwidthCredentialError)?; + let gateway_identity = PublicKey::from_bytes(&b[PUBLIC_KEY_LENGTH..2 * PUBLIC_KEY_LENGTH]) + .map_err(|_| Error::BandwidthCredentialError)?; + let bandwidth = u64::from_be_bytes( + b[2 * PUBLIC_KEY_LENGTH..2 * PUBLIC_KEY_LENGTH + 8] + .try_into() + // unwrapping is safe because we know we have 8 bytes + .unwrap(), + ); + let signature = Signature::from_bytes(&b[2 * PUBLIC_KEY_LENGTH + 8..]) + .map_err(|_| Error::BandwidthCredentialError)?; + Ok(TokenCredential { + verification_key, + gateway_identity, + bandwidth, + signature, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(not(feature = "coconut"))] + #[test] + fn token_serde() { + // pre-generated, valid values + let verification_key = PublicKey::from_bytes(&[ + 103, 105, 71, 177, 149, 245, 26, 32, 73, 121, 76, 50, 94, 88, 119, 231, 91, 229, 167, + 56, 39, 62, 185, 39, 83, 246, 153, 27, 17, 155, 109, 73, + ]) + .unwrap(); + let gateway_identity = PublicKey::from_bytes(&[ + 37, 113, 137, 189, 157, 82, 35, 2, 187, 136, 61, 119, 98, 5, 245, 82, 46, 124, 67, 45, + 165, 255, 53, 222, 185, 252, 6, 148, 128, 15, 206, 19, + ]) + .unwrap(); + let signature = Signature::from_bytes(&[ + 117, 251, 162, 217, 57, 2, 50, 210, 206, 81, 236, 90, 74, 201, 69, 237, 240, 247, 214, + 158, 220, 89, 235, 222, 85, 134, 73, 73, 8, 60, 25, 39, 183, 28, 83, 193, 31, 174, 25, + 24, 38, 215, 205, 228, 159, 135, 35, 4, 171, 59, 100, 157, 12, 249, 77, 52, 143, 4, 32, + 28, 147, 70, 182, 14, + ]) + .unwrap(); + let credential = TokenCredential::new(verification_key, gateway_identity, 1024, signature); + let serialized_credential = credential.to_bytes(); + let deserialized_credential = TokenCredential::from_bytes(&serialized_credential).unwrap(); + assert_eq!( + credential.verification_key, + deserialized_credential.verification_key + ); + assert_eq!( + credential.gateway_identity, + deserialized_credential.gateway_identity + ); + assert_eq!(credential.bandwidth, deserialized_credential.bandwidth); + assert_eq!( + credential.signature.to_bytes(), + deserialized_credential.signature.to_bytes() + ); + } +} diff --git a/common/credentials/src/token/mod.rs b/common/credentials/src/token/mod.rs new file mode 100644 index 0000000000..ac23bbe001 --- /dev/null +++ b/common/credentials/src/token/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod bandwidth; diff --git a/common/erc20-bridge-contract/Cargo.toml b/common/erc20-bridge-contract/Cargo.toml new file mode 100644 index 0000000000..46cb8f4809 --- /dev/null +++ b/common/erc20-bridge-contract/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "erc20-bridge-contract" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +schemars = "0.8" +serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/common/erc20-bridge-contract/src/keys.rs b/common/erc20-bridge-contract/src/keys.rs new file mode 100644 index 0000000000..a4757918e2 --- /dev/null +++ b/common/erc20-bridge-contract/src/keys.rs @@ -0,0 +1,45 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +// Serializable structures for what we find in common/crypto +#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, JsonSchema)] +pub struct PublicKey([u8; 32]); + +impl PublicKey { + pub fn new(bytes: [u8; 32]) -> Self { + PublicKey(bytes) + } + pub fn to_bytes(&self) -> [u8; 32] { + self.0 + } +} + +impl AsRef<[u8]> for PublicKey { + #[inline] + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct Signature([u8; 32], [u8; 32]); + +impl Signature { + pub fn new(bytes: [u8; 64]) -> Self { + let mut sig1 = [0u8; 32]; + let mut sig2 = [0u8; 32]; + sig1.copy_from_slice(&bytes[..32]); + sig2.copy_from_slice(&bytes[32..]); + + Signature(sig1, sig2) + } + pub fn to_bytes(&self) -> [u8; 64] { + let mut res = [0u8; 64]; + res[..32].copy_from_slice(&self.0); + res[32..].copy_from_slice(&self.1); + res + } +} diff --git a/common/erc20-bridge-contract/src/lib.rs b/common/erc20-bridge-contract/src/lib.rs new file mode 100644 index 0000000000..f878fe6526 --- /dev/null +++ b/common/erc20-bridge-contract/src/lib.rs @@ -0,0 +1,3 @@ +pub mod keys; +pub mod msg; +pub mod payment; diff --git a/common/erc20-bridge-contract/src/msg.rs b/common/erc20-bridge-contract/src/msg.rs new file mode 100644 index 0000000000..bc0bedcc6b --- /dev/null +++ b/common/erc20-bridge-contract/src/msg.rs @@ -0,0 +1,30 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::keys::PublicKey; +use crate::payment::LinkPaymentData; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct InstantiateMsg {} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ExecuteMsg { + LinkPayment { data: LinkPaymentData }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + GetPayments { + limit: Option, + start_after: Option, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct MigrateMsg {} diff --git a/common/erc20-bridge-contract/src/payment.rs b/common/erc20-bridge-contract/src/payment.rs new file mode 100644 index 0000000000..fe2126cdb0 --- /dev/null +++ b/common/erc20-bridge-contract/src/payment.rs @@ -0,0 +1,73 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::keys::{PublicKey, Signature}; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct Payment { + verification_key: PublicKey, + gateway_identity: PublicKey, + bandwidth: u64, +} + +impl Payment { + pub fn new(verification_key: PublicKey, gateway_identity: PublicKey, bandwidth: u64) -> Self { + Payment { + verification_key, + gateway_identity, + bandwidth, + } + } + + pub fn verification_key(&self) -> PublicKey { + self.verification_key + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct LinkPaymentData { + pub verification_key: PublicKey, + pub gateway_identity: PublicKey, + pub bandwidth: u64, + pub signature: Signature, +} + +impl LinkPaymentData { + pub fn new( + verification_key: [u8; 32], + gateway_identity: [u8; 32], + bandwidth: u64, + signature: [u8; 64], + ) -> Self { + LinkPaymentData { + verification_key: PublicKey::new(verification_key), + gateway_identity: PublicKey::new(gateway_identity), + bandwidth, + signature: Signature::new(signature), + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct PagedPaymentResponse { + pub payments: Vec, + pub per_page: usize, + pub start_next_after: Option, +} + +impl PagedPaymentResponse { + pub fn new( + payments: Vec, + per_page: usize, + start_next_after: Option, + ) -> Self { + PagedPaymentResponse { + payments, + per_page, + start_next_after, + } + } +} diff --git a/common/mixnet-contract/Cargo.toml b/common/mixnet-contract/Cargo.toml index 67c94e3355..fba7fecfd1 100644 --- a/common/mixnet-contract/Cargo.toml +++ b/common/mixnet-contract/Cargo.toml @@ -8,10 +8,18 @@ edition = "2018" [dependencies] # this branch is identical to 0.14.1 with addition of updated k256 dependency required to help poor cargo choose correct version -cosmwasm-std = { git = "https://github.com/jstuczyn/cosmwasm", branch="0.14.1-updatedk256" } +cosmwasm-std = { git = "https://github.com/jstuczyn/cosmwasm", branch = "0.14.1-updatedk256" } #cosmwasm-std = { version = "0.14.1" } serde = { version = "1.0", features = ["derive"] } serde_repr = "0.1" schemars = "0.8" -ts-rs = "3.0" +ts-rs = { version = "3.0", optional = true } +thiserror = "1.0" +network-defaults = { path = "../network-defaults" } +fixed = "1.1" +az = "1.1" +log = "0.4.14" + +[features] +default = ["ts-rs"] diff --git a/common/mixnet-contract/src/error.rs b/common/mixnet-contract/src/error.rs new file mode 100644 index 0000000000..b338d12125 --- /dev/null +++ b/common/mixnet-contract/src/error.rs @@ -0,0 +1,9 @@ +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, +} diff --git a/common/mixnet-contract/src/gateway.rs b/common/mixnet-contract/src/gateway.rs index 25c9fe0bab..23ab18b11d 100644 --- a/common/mixnet-contract/src/gateway.rs +++ b/common/mixnet-contract/src/gateway.rs @@ -7,9 +7,9 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; use std::fmt::Display; -use ts_rs::TS; -#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize, JsonSchema, TS)] +#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize, JsonSchema)] pub struct Gateway { pub host: String, pub mix_port: u16, diff --git a/common/mixnet-contract/src/lib.rs b/common/mixnet-contract/src/lib.rs index ffecdea57c..c3162adf4a 100644 --- a/common/mixnet-contract/src/lib.rs +++ b/common/mixnet-contract/src/lib.rs @@ -2,8 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 mod delegation; +pub mod error; mod gateway; -mod mixnode; +pub mod mixnode; mod msg; mod types; @@ -15,4 +16,7 @@ pub use delegation::{ pub use gateway::{Gateway, GatewayBond, GatewayOwnershipResponse, PagedGatewayResponse}; pub use mixnode::{Layer, MixNode, MixNodeBond, MixOwnershipResponse, PagedMixnodeResponse}; pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; -pub use types::{IdentityKey, IdentityKeyRef, LayerDistribution, SphinxKey, StateParams}; +pub use types::{ + IdentityKey, IdentityKeyRef, LayerDistribution, RewardingIntervalResponse, SphinxKey, + StateParams, +}; diff --git a/common/mixnet-contract/src/mixnode.rs b/common/mixnet-contract/src/mixnode.rs index f6b3281580..287271e373 100644 --- a/common/mixnet-contract/src/mixnode.rs +++ b/common/mixnet-contract/src/mixnode.rs @@ -2,15 +2,20 @@ #![allow(clippy::field_reassign_with_default)] use crate::{IdentityKey, SphinxKey}; -use cosmwasm_std::{coin, Addr, Coin}; +use az::CheckedCast; +use cosmwasm_std::{coin, Addr, Coin, Uint128}; +use log::error; +use network_defaults::{DEFAULT_OPERATOR_EPOCH_COST, DEFAULT_PROFIT_MARGIN}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use std::cmp::Ordering; use std::fmt::Display; -use ts_rs::TS; -#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize, JsonSchema, TS)] +type U128 = fixed::types::U75F53; // u128 with 18 significant digits + +#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize, JsonSchema)] pub struct MixNode { pub host: String, pub mix_port: u16, @@ -43,6 +48,97 @@ pub enum Layer { Three = 3, } +#[derive(Debug, Clone, JsonSchema, PartialEq, Serialize, Deserialize, Copy)] +pub struct NodeRewardParams { + period_reward_pool: Uint128, + k: Uint128, + reward_blockstamp: u64, + circulating_supply: Uint128, + uptime: Uint128, + sybil_resistance_percent: u8, +} + +impl NodeRewardParams { + pub fn new( + period_reward_pool: u128, + k: u128, + reward_blockstamp: u64, + circulating_supply: u128, + uptime: u128, + sybil_resistance_percent: u8, + ) -> NodeRewardParams { + NodeRewardParams { + period_reward_pool: Uint128(period_reward_pool), + k: Uint128(k), + reward_blockstamp, + circulating_supply: Uint128(circulating_supply), + uptime: Uint128(uptime), + sybil_resistance_percent, + } + } + + pub fn performance(&self) -> U128 { + U128::from_num(self.uptime.u128()) / U128::from_num(100) + } + + pub fn operator_cost(&self) -> U128 { + U128::from_num(self.uptime.u128() / 100u128 * DEFAULT_OPERATOR_EPOCH_COST as u128) + } + + pub fn set_reward_blockstamp(&mut self, blockstamp: u64) { + self.reward_blockstamp = blockstamp; + } + + pub fn period_reward_pool(&self) -> u128 { + self.period_reward_pool.u128() + } + + pub fn k(&self) -> u128 { + self.k.u128() + } + + pub fn circulating_supply(&self) -> u128 { + self.circulating_supply.u128() + } + + pub fn reward_blockstamp(&self) -> u64 { + self.reward_blockstamp + } + + pub fn uptime(&self) -> u128 { + self.uptime.u128() + } + + pub fn one_over_k(&self) -> U128 { + U128::from_num(1) / U128::from_num(self.k.u128()) + } + + pub fn alpha(&self) -> U128 { + U128::from_num(self.sybil_resistance_percent) / U128::from_num(100) + } +} + +#[derive(Debug)] +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 + } +} + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct MixNodeBond { pub bond_amount: Coin, @@ -51,6 +147,7 @@ pub struct MixNodeBond { pub layer: Layer, pub block_height: u64, pub mix_node: MixNode, + pub profit_margin_percent: Option, } impl MixNodeBond { @@ -60,6 +157,7 @@ impl MixNodeBond { layer: Layer, block_height: u64, mix_node: MixNode, + profit_margin_percent: Option, ) -> Self { MixNodeBond { total_delegation: coin(0, &bond_amount.denom), @@ -68,9 +166,15 @@ impl MixNodeBond { layer, block_height, mix_node, + profit_margin_percent, } } + pub fn profit_margin(&self) -> U128 { + U128::from_num(self.profit_margin_percent.unwrap_or(DEFAULT_PROFIT_MARGIN)) + / U128::from_num(100) + } + pub fn identity(&self) -> &String { &self.mix_node.identity_key } @@ -86,6 +190,124 @@ impl MixNodeBond { pub fn mix_node(&self) -> &MixNode { &self.mix_node } + + pub fn total_stake(&self) -> Option { + if self.bond_amount.denom != self.total_delegation.denom { + None + } else { + Some(self.bond_amount.amount.u128() + self.total_delegation.amount.u128()) + } + } + + pub fn total_delegation(&self) -> Coin { + self.total_delegation.clone() + } + + pub fn bond_to_circulating_supply(&self, circulating_supply: u128) -> U128 { + U128::from_num(self.bond_amount().amount.u128()) / U128::from_num(circulating_supply) + } + + pub fn total_stake_to_circulating_supply(&self, circulating_supply: u128) -> U128 { + U128::from_num(self.bond_amount().amount.u128() + self.total_delegation().amount.u128()) + / U128::from_num(circulating_supply) + } + + pub fn lambda(&self, params: &NodeRewardParams) -> U128 { + // Ratio of a bond to the token circulating supply + let bond_to_circulating_supply_ratio = + self.bond_to_circulating_supply(params.circulating_supply()); + bond_to_circulating_supply_ratio.min(params.one_over_k()) + } + + pub fn sigma(&self, params: &NodeRewardParams) -> U128 { + // Ratio of a delegation to the the token circulating supply + let total_stake_to_circulating_supply_ratio = + self.total_stake_to_circulating_supply(params.circulating_supply()); + total_stake_to_circulating_supply_ratio.min(params.one_over_k()) + } + + pub fn reward(&self, params: &NodeRewardParams) -> NodeRewardResult { + // Assuming uniform work distribution across the network this is one_over_k * k + let omega_k = U128::from_num(1u128); + let lambda = self.lambda(params); + let sigma = self.sigma(params); + + let reward = params.performance() + * params.period_reward_pool() + * (sigma * omega_k + params.alpha() * lambda * sigma * params.k()) + / (U128::from_num(1) + params.alpha()); + + NodeRewardResult { + reward, + lambda, + sigma, + } + } + + pub fn node_profit(&self, params: &NodeRewardParams) -> U128 { + if self.reward(params).reward() < params.operator_cost() { + U128::from_num(0) + } else { + self.reward(params).reward() - params.operator_cost() + } + } + + pub fn operator_reward(&self, params: &NodeRewardParams) -> u128 { + let reward = self.reward(params); + let profit = if reward.reward < params.operator_cost() { + U128::from_num(0) + } else { + reward.reward - params.operator_cost() + }; + let operator_base_reward = reward.reward.min(params.operator_cost()); + let operator_reward = (self.profit_margin() + + (U128::from_num(1) - 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: &NodeRewardParams) -> U128 { + if self.total_stake_to_circulating_supply(params.circulating_supply()) < params.one_over_k() + { + self.total_stake_to_circulating_supply(params.circulating_supply()) + } else { + params.one_over_k() + } + } + + pub fn reward_delegation(&self, delegation_amount: Uint128, params: &NodeRewardParams) -> u128 { + let scaled_delegation_amount = + U128::from_num(delegation_amount.u128()) / U128::from_num(params.circulating_supply()); + + let delegator_reward = (U128::from_num(1) - self.profit_margin()) + * scaled_delegation_amount + / self.sigma(params) + * self.node_profit(params); + + let reward = delegator_reward.max(U128::from_num(0)); + if let Some(int_reward) = reward.checked_cast() { + int_reward + } else { + error!( + "Could not cast delegator reward ({}) to u128, returning 0 - mixnode {}", + reward, + self.identity() + ); + 0u128 + } + } } impl PartialOrd for MixNodeBond { @@ -217,6 +439,7 @@ mod tests { layer: Layer::One, block_height: 100, mix_node: mixnode_fixture(), + profit_margin_percent: Some(10), }; let mix2 = MixNodeBond { @@ -226,6 +449,7 @@ mod tests { layer: Layer::One, block_height: 120, mix_node: mixnode_fixture(), + profit_margin_percent: Some(10), }; let mix3 = MixNodeBond { @@ -235,6 +459,7 @@ mod tests { layer: Layer::One, block_height: 120, mix_node: mixnode_fixture(), + profit_margin_percent: Some(10), }; let mix4 = MixNodeBond { @@ -244,6 +469,7 @@ mod tests { layer: Layer::One, block_height: 120, mix_node: mixnode_fixture(), + profit_margin_percent: Some(10), }; let mix5 = MixNodeBond { @@ -253,6 +479,7 @@ mod tests { layer: Layer::One, block_height: 120, mix_node: mixnode_fixture(), + profit_margin_percent: Some(10), }; // summary: diff --git a/common/mixnet-contract/src/msg.rs b/common/mixnet-contract/src/msg.rs index dfda40befa..e7c4f4e643 100644 --- a/common/mixnet-contract/src/msg.rs +++ b/common/mixnet-contract/src/msg.rs @@ -1,6 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::mixnode::NodeRewardParams; use crate::StateParams; use crate::{Gateway, IdentityKey, MixNode}; use cosmwasm_std::Addr; @@ -31,10 +32,32 @@ pub enum ExecuteMsg { mix_identity: IdentityKey, }, + BeginMixnodeRewarding { + // nonce of the current rewarding interval + rewarding_interval_nonce: u32, + }, + RewardMixnode { identity: IdentityKey, // percentage value in range 0-100 uptime: u32, + + // nonce of the current rewarding interval + rewarding_interval_nonce: u32, + }, + + FinishMixnodeRewarding { + // nonce of the current rewarding interval + rewarding_interval_nonce: u32, + }, + + RewardMixnodeV2 { + identity: IdentityKey, + // percentage value in range 0-100 + params: NodeRewardParams, + + // nonce of the current rewarding interval + rewarding_interval_nonce: u32, }, } @@ -56,6 +79,7 @@ pub enum QueryMsg { address: Addr, }, StateParams {}, + CurrentRewardingInterval {}, GetMixDelegations { mix_identity: IdentityKey, start_after: Option, @@ -75,6 +99,10 @@ pub enum QueryMsg { address: Addr, }, LayerDistribution {}, + GetRewardPool {}, + GetCirculatingSupply {}, + GetEpochRewardPercent {}, + GetSybilResistancePercent {}, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] diff --git a/common/mixnet-contract/src/types.rs b/common/mixnet-contract/src/types.rs index b2a16515fe..ee6ad94477 100644 --- a/common/mixnet-contract/src/types.rs +++ b/common/mixnet-contract/src/types.rs @@ -26,15 +26,29 @@ impl LayerDistribution { } } +#[derive(Debug, Default, Serialize, Deserialize, Copy, Clone, Eq, PartialEq)] +pub struct RewardingIntervalResponse { + pub current_rewarding_interval_starting_block: u64, + pub current_rewarding_interval_nonce: u32, + pub rewarding_in_progress: bool, +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct StateParams { - pub epoch_length: u32, // length of an epoch, expressed in hours + pub epoch_length: u32, // length of a rewarding epoch/interval, expressed in hours pub minimum_mixnode_bond: Uint128, // minimum amount a mixnode must bond to get into the system pub minimum_gateway_bond: Uint128, // minimum amount a gateway must bond to get into the system pub mixnode_bond_reward_rate: Decimal, // annual reward rate, expressed as a decimal like 1.25 pub mixnode_delegation_reward_rate: Decimal, // annual reward rate, expressed as a decimal like 1.25 + + // 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, + + // 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, } @@ -54,6 +68,11 @@ impl Display for StateParams { "mixnode delegation reward rate: {}; ", self.mixnode_delegation_reward_rate )?; + write!( + f, + "mixnode rewarded set size: {}", + self.mixnode_rewarded_set_size + )?; write!( f, "mixnode active set size: {}", diff --git a/common/network-defaults/Cargo.toml b/common/network-defaults/Cargo.toml index 6cbde55586..ba50e9c473 100644 --- a/common/network-defaults/Cargo.toml +++ b/common/network-defaults/Cargo.toml @@ -7,6 +7,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +hex-literal = "0.3.3" serde = {version = "1.0", features = ["derive"]} url = "2.2" time = { version = "0.3", features = ["macros"] } diff --git a/common/network-defaults/src/eth_contract.rs b/common/network-defaults/src/eth_contract.rs new file mode 100644 index 0000000000..b9d4e5fddd --- /dev/null +++ b/common/network-defaults/src/eth_contract.rs @@ -0,0 +1,132 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// This should be modified whenever an updated Ethereum contract is uploaded +pub const ETH_JSON_ABI: &str = r#" +[ + { + "inputs": [ + { + "internalType": "contract ERC20Burnable", + "name": "_erc20", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "Bandwidth", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "VerificationKey", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "SignedVerificationKey", + "type": "bytes" + } + ], + "name": "Burned", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "verificationKey", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "signedVerificationKey", + "type": "bytes" + } + ], + "name": "burnTokenForAccessCode", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "erc20", + "outputs": [ + { + "internalType": "contract ERC20Burnable", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] + "#; diff --git a/common/network-defaults/src/lib.rs b/common/network-defaults/src/lib.rs index 1542c8a3ad..2e02299ce2 100644 --- a/common/network-defaults/src/lib.rs +++ b/common/network-defaults/src/lib.rs @@ -5,6 +5,8 @@ use std::time::Duration; use time::OffsetDateTime; use url::Url; +pub mod eth_contract; + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ValidatorDetails { // it is assumed those values are always valid since they're being provided in our defaults file @@ -61,7 +63,24 @@ pub fn default_api_endpoints() -> Vec { } pub const DEFAULT_MIXNET_CONTRACT_ADDRESS: &str = "punk10pyejy66429refv3g35g2t7am0was7yalwrzen"; -pub const NETWORK_MONITOR_ADDRESS: &str = "punk1v9qauwdq5terag6uvfsdytcs2d0sdmfdy7hgk3"; +pub const REWARDING_VALIDATOR_ADDRESS: &str = "punk1v9qauwdq5terag6uvfsdytcs2d0sdmfdy7hgk3"; + +/// How much bandwidth (in bytes) one token can buy +const BYTES_PER_TOKEN: u64 = 1024 * 1024 * 1024; +/// How many ERC20 tokens should be burned to buy bandwidth +pub const TOKENS_TO_BURN: u64 = 10; +/// Default bandwidth (in bytes) that we try to buy +pub const BANDWIDTH_VALUE: u64 = TOKENS_TO_BURN * BYTES_PER_TOKEN; + +// Ethereum constants used for token bridge +pub const ETH_CONTRACT_ADDRESS: [u8; 20] = + hex_literal::hex!("9fEE3e28c17dbB87310A51F13C4fbf4331A6f102"); +pub const ETH_MIN_BLOCK_DEPTH: usize = 7; +pub const COSMOS_CONTRACT_ADDRESS: &str = "punk1jld76tqw4wnpfenmay2xkv86nr3j0w426eka82"; +// Name of the event triggered by the eth contract. If the event name is changed, +// this would also need to be changed; It is currently tested against the json abi +pub const ETH_EVENT_NAME: &str = "Burned"; +pub const ETH_BURN_FUNCTION_NAME: &str = "burnTokenForAccessCode"; /// Defaults Cosmos Hub/ATOM path pub const COSMOS_DERIVATION_PATH: &str = "m/44'/118'/0'/0/0"; @@ -93,4 +112,11 @@ pub const VALIDATOR_API_VERSION: &str = "v1"; // REWARDING pub const DEFAULT_FIRST_EPOCH_START: OffsetDateTime = time::macros::datetime!(2021-08-23 12:00 UTC); -pub const DEFAULT_EPOCH_LENGTH: Duration = Duration::from_secs(24 * 60 * 60); // 24h +pub const DEFAULT_EPOCH_LENGTH: Duration = Duration::from_secs(24 * 60 * 60 * 30); // 30 days +/// We'll be assuming a few more things, profit margin and cost function. Since we don't have relialable package measurement, we'll be using uptime. We'll also set the value of 1 Nym to 1 $, to be able to translate epoch costs to Nyms. We'll also assume a cost of 40$ per epoch(month), converting that to Nym at our 1$ rate translates to 40_000_000 uNyms +pub const DEFAULT_OPERATOR_EPOCH_COST: u64 = 40_000_000; // 40$/(30 days) at 1 Nym == 1$ + +// TODO: is there a way to get this from the chain +pub const TOTAL_SUPPLY: u128 = 1_000_000_000_000_000; + +pub const DEFAULT_PROFIT_MARGIN: u8 = 10; diff --git a/contracts/erc20-bridge/Cargo.lock b/contracts/erc20-bridge/Cargo.lock new file mode 100644 index 0000000000..9e51f6ed3e --- /dev/null +++ b/contracts/erc20-bridge/Cargo.lock @@ -0,0 +1,863 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding", + "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 = [ + "generic-array 0.14.4", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", +] + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "config" +version = "0.1.0" +dependencies = [ + "handlebars", + "humantime-serde", + "network-defaults", + "serde", + "toml", + "url", +] + +[[package]] +name = "const-oid" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdab415d6744056100f40250a66bc430c1a46f7a02e20bc11c94c79a0f0464df" + +[[package]] +name = "cosmos_contract" +version = "0.1.0" +dependencies = [ + "config", + "cosmwasm-std", + "cosmwasm-storage", + "erc20-bridge-contract", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cosmwasm-crypto" +version = "0.14.1" +source = "git+https://github.com/jstuczyn/cosmwasm?branch=0.14.1-updatedk256#308781cd5f33b0e996176c6b500793d3648c0f7b" +dependencies = [ + "digest 0.9.0", + "ed25519-zebra", + "k256", + "rand_core 0.5.1", + "thiserror", +] + +[[package]] +name = "cosmwasm-derive" +version = "0.14.1" +source = "git+https://github.com/jstuczyn/cosmwasm?branch=0.14.1-updatedk256#308781cd5f33b0e996176c6b500793d3648c0f7b" +dependencies = [ + "syn", +] + +[[package]] +name = "cosmwasm-std" +version = "0.14.1" +source = "git+https://github.com/jstuczyn/cosmwasm?branch=0.14.1-updatedk256#308781cd5f33b0e996176c6b500793d3648c0f7b" +dependencies = [ + "base64", + "cosmwasm-crypto", + "cosmwasm-derive", + "schemars", + "serde", + "serde-json-wasm", + "thiserror", + "uint", +] + +[[package]] +name = "cosmwasm-storage" +version = "0.14.1" +source = "git+https://github.com/jstuczyn/cosmwasm?branch=0.14.1-updatedk256#308781cd5f33b0e996176c6b500793d3648c0f7b" +dependencies = [ + "cosmwasm-std", + "serde", +] + +[[package]] +name = "cpufeatures" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-bigint" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d12477e115c0d570c12a2dfd859f80b55b60ddb5075df210d3af06d133a69f45" +dependencies = [ + "generic-array 0.14.4", + "rand_core 0.6.3", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array 0.14.4", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "der" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28e98c534e9c8a0483aa01d6f6913bc063de254311bd267c9cf535e9b70e15b2" +dependencies = [ + "const-oid", +] + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.4", +] + +[[package]] +name = "dyn-clone" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf" + +[[package]] +name = "ecdsa" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43ee23aa5b4f68c7a092b5c3beb25f50c406adc75e2363634f242f28ab255372" +dependencies = [ + "der", + "elliptic-curve", + "hmac", + "signature", +] + +[[package]] +name = "ed25519-zebra" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a128b76af6dd4b427e34a6fd43dc78dbfe73672ec41ff615a2414c1a0ad0409" +dependencies = [ + "curve25519-dalek", + "hex", + "rand_core 0.5.1", + "serde", + "sha2", + "thiserror", +] + +[[package]] +name = "elliptic-curve" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beca177dcb8eb540133e7680baff45e7cc4d93bf22002676cec549f82343721b" +dependencies = [ + "crypto-bigint", + "ff", + "generic-array 0.14.4", + "group", + "pkcs8", + "rand_core 0.6.3", + "subtle", + "zeroize", +] + +[[package]] +name = "erc20-bridge-contract" +version = "0.1.0" +dependencies = [ + "schemars", + "serde", +] + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + +[[package]] +name = "ff" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f40b2dcd8bc322217a5f6559ae5f9e9d1de202a2ecee2e9eafcbece7562a4f" +dependencies = [ + "rand_core 0.6.3", + "subtle", +] + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", +] + +[[package]] +name = "group" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c363a5301b8f153d80747126a04b3c82073b9fe3130571a9d170cacdeaf7912" +dependencies = [ + "ff", + "rand_core 0.6.3", + "subtle", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e4590e13640f19f249fe3e4eca5113bc4289f2497710378190e7f4bd96f45b" + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "humantime-serde" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac34a56cfd4acddb469cc7fff187ed5ac36f498ba085caf8bbc725e3ff474058" +dependencies = [ + "humantime", + "serde", +] + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "k256" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903ae2481bcdfdb7b68e0a9baa4b7c9aff600b9ae2e8e5bb5833b8c91ab851ea" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "sha2", +] + +[[package]] +name = "libc" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "network-defaults" +version = "0.1.0" +dependencies = [ + "hex-literal", + "serde", + "time", + "url", +] + +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" +dependencies = [ + "maplit", + "pest", + "sha-1", +] + +[[package]] +name = "pkcs8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee3ef9b64d26bad0536099c816c6734379e45bbd5f14798def6809e5cc350447" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "proc-macro2" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" +dependencies = [ + "unicode-xid", +] + +[[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.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.3", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "schemars" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a48d098c2a7fdf5740b19deb1181b4fb8a9e68e03ae517c14cde04b5725409" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9ea2a613fe4cd7118b2bb101a25d8ae6192e1975179b67b2f17afd11e70ac8" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + +[[package]] +name = "serde" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-json-wasm" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50eef3672ec8fa45f3457fd423ba131117786784a895548021976117c1ded449" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_derive_internals" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dbab34ca63057a1f15280bdf3c39f2b1eb1b54c17e98360e511637aef7418c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[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 = "sha2" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug 0.3.0", +] + +[[package]] +name = "signature" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19772be3c4dd2ceaacf03cb41d5885f2a02c4d8804884918e3a258480803335" +dependencies = [ + "digest 0.9.0", + "rand_core 0.6.3", +] + +[[package]] +name = "spki" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c01a0c15da1b0b0e1494112e7af814a678fec9bd157881b49beac661e9b6f32" +dependencies = [ + "der", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1d708c221c5a612956ef9f75b37e454e88d1f7b899fbd3a18d4252012d663" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "thiserror" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99beeb0daeac2bd1e86ac2c21caddecb244b39a093594da1a661ec2060c7aedd" +dependencies = [ + "libc", + "time-macros", +] + +[[package]] +name = "time-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6" + +[[package]] +name = "tinyvec" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "typenum" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" + +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + +[[package]] +name = "uint" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6470ab50f482bde894a037a57064480a246dbfdd5960bd65a44824693f08da5f" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "zeroize" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" diff --git a/contracts/erc20-bridge/Cargo.toml b/contracts/erc20-bridge/Cargo.toml new file mode 100644 index 0000000000..a487dbee61 --- /dev/null +++ b/contracts/erc20-bridge/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "cosmos_contract" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[workspace] # adding a blank workspace to keep it out of the global workspace. + +[lib] +crate-type = ["cdylib", "rlib"] + +[profile.release] +opt-level = 3 +debug = false +rpath = false +lto = true +debug-assertions = false +codegen-units = 1 +panic = 'abort' +incremental = false +overflow-checks = true + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] + +[dev-dependencies] +config = { path = "../../common/config"} + +[dependencies] +erc20-bridge-contract = { path = "../../common/erc20-bridge-contract" } + +# this branch is identical to 0.14.1 with addition of updated k256 dependency required to help poor cargo choose correct version +cosmwasm-std = { git = "https://github.com/jstuczyn/cosmwasm", branch="0.14.1-updatedk256", features = ["iterator"] } +cosmwasm-storage = { git = "https://github.com/jstuczyn/cosmwasm", branch="0.14.1-updatedk256", features = ["iterator"] } + +schemars = "0.8" +serde = { version = "1.0.103", default-features = false, features = ["derive"] } +thiserror = "1.0.23" diff --git a/contracts/erc20-bridge/src/error.rs b/contracts/erc20-bridge/src/error.rs new file mode 100644 index 0000000000..02f3523eeb --- /dev/null +++ b/contracts/erc20-bridge/src/error.rs @@ -0,0 +1,27 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{StdError, VerificationError}; +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("{0}")] + Std(#[from] StdError), + + #[error("Invalid size for signature items")] + InvalidSignatureSize, + + #[error("This payment has already been claimed by someone")] + PaymentAlreadyClaimed, + + #[error("Error while verifying ed25519 signature - {0}")] + VerificationError(#[from] VerificationError), + + #[error("The payment is not properly signed")] + BadSignature, +} diff --git a/contracts/erc20-bridge/src/lib.rs b/contracts/erc20-bridge/src/lib.rs new file mode 100644 index 0000000000..02957f9e37 --- /dev/null +++ b/contracts/erc20-bridge/src/lib.rs @@ -0,0 +1,102 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +mod error; +mod queries; +mod storage; +mod support; +mod transactions; + +use cosmwasm_std::{ + entry_point, to_binary, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, +}; + +use crate::error::ContractError; +use erc20_bridge_contract::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +/// Instantiate the contract. +/// +/// `deps` contains Storage, API and Querier +/// `env` contains block, message and contract info +/// `msg` is the contract initialization message, sort of like a constructor call. +#[entry_point] +pub fn instantiate( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: InstantiateMsg, +) -> Result { + Ok(Response::default()) +} + +/// Handle an incoming message +#[entry_point] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::LinkPayment { data } => transactions::link_payment(deps, env, info, data), + } +} + +#[entry_point] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { + let query_res = match msg { + QueryMsg::GetPayments { start_after, limit } => { + to_binary(&queries::query_payments_paged(deps, start_after, limit)?) + } + }; + + Ok(query_res?) +} + +#[entry_point] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + Ok(Default::default()) +} + +#[cfg(test)] +pub mod tests { + use super::*; + use config::defaults::DENOM; + use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; + use cosmwasm_std::{coins, from_binary}; + use erc20_bridge_contract::payment::PagedPaymentResponse; + + #[test] + fn initialize_contract() { + let mut deps = mock_dependencies(&[]); + let env = mock_env(); + let msg = InstantiateMsg {}; + let info = mock_info("creator", &[]); + + let res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + assert_eq!(0, res.messages.len()); + + // payments should be empty after initialization + let res = query( + deps.as_ref(), + env.clone(), + QueryMsg::GetPayments { + start_after: None, + limit: Option::from(2), + }, + ) + .unwrap(); + let page: PagedPaymentResponse = from_binary(&res).unwrap(); + assert_eq!(0, page.payments.len()); // there are no payments in the list when it's just been initialized + + // Contract balance should match what we initialized it as + assert_eq!( + coins(0, DENOM), + vec![deps + .as_ref() + .querier + .query_balance(env.contract.address, DENOM) + .unwrap()] + ); + } +} diff --git a/contracts/erc20-bridge/src/queries.rs b/contracts/erc20-bridge/src/queries.rs new file mode 100644 index 0000000000..66e0c28f4d --- /dev/null +++ b/contracts/erc20-bridge/src/queries.rs @@ -0,0 +1,191 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{Deps, Order, StdResult}; + +use crate::storage::payments_read; +use erc20_bridge_contract::keys::PublicKey; +use erc20_bridge_contract::payment::{PagedPaymentResponse, Payment}; + +const PAYMENT_PAGE_MAX_LIMIT: u32 = 100; +const PAYMENT_PAGE_DEFAULT_LIMIT: u32 = 50; + +/// Adds a 0 byte to terminate the `start_after` value given. This allows CosmWasm +/// to get the succeeding key as the start of the next page. +fn calculate_start_value>(start_after: Option) -> Option> { + start_after.as_ref().map(|identity| { + identity + .as_ref() + .iter() + .cloned() + .chain(std::iter::once(0)) + .collect() + }) +} + +pub fn query_payments_paged( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(PAYMENT_PAGE_DEFAULT_LIMIT) + .min(PAYMENT_PAGE_MAX_LIMIT) as usize; + let start = calculate_start_value(start_after); + + let payments = payments_read(deps.storage) + .range(start.as_deref(), None, Order::Ascending) + .take(limit) + .map(|res| res.map(|item| item.1)) + .collect::>>()?; + + let start_next_after = payments.last().map(|payment| payment.verification_key()); + + Ok(PagedPaymentResponse::new(payments, limit, start_next_after)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::storage::payments; + use crate::support::tests::helpers; + use std::convert::TryInto; + + #[test] + fn payments_empty_on_init() { + let deps = helpers::init_contract(); + let response = query_payments_paged(deps.as_ref(), None, Option::from(2)).unwrap(); + assert_eq!(0, response.payments.len()); + } + + #[test] + fn payments_paged_retrieval_obeys_limits() { + let mut deps = helpers::init_contract(); + let storage = deps.as_mut().storage; + let limit = 2; + for n in 0u32..10000 { + let bytes: Vec = std::iter::repeat(n.to_be_bytes()) + .take(8) + .flatten() + .collect(); + let verification_key = PublicKey::new(bytes.try_into().unwrap()); + let payment = helpers::payment_fixture(); + payments(storage) + .save(&verification_key.to_bytes(), &payment) + .unwrap(); + } + + let page1 = query_payments_paged(deps.as_ref(), None, Option::from(limit)).unwrap(); + assert_eq!(limit, page1.payments.len() as u32); + } + + #[test] + fn payments_paged_retrieval_has_default_limit() { + let mut deps = helpers::init_contract(); + let storage = deps.as_mut().storage; + for n in 0u32..100 { + let bytes: Vec = std::iter::repeat(n.to_be_bytes()) + .take(8) + .flatten() + .collect(); + let verification_key = PublicKey::new(bytes.try_into().unwrap()); + let payment = helpers::payment_fixture(); + payments(storage) + .save(&verification_key.to_bytes(), &payment) + .unwrap(); + } + + // query without explicitly setting a limit + let page1 = query_payments_paged(deps.as_ref(), None, None).unwrap(); + + assert_eq!(PAYMENT_PAGE_DEFAULT_LIMIT, page1.payments.len() as u32); + } + + #[test] + fn payments_paged_retrieval_has_max_limit() { + let mut deps = helpers::init_contract(); + let storage = deps.as_mut().storage; + for n in 0u32..10000 { + let bytes: Vec = std::iter::repeat(n.to_be_bytes()) + .take(8) + .flatten() + .collect(); + let verification_key = PublicKey::new(bytes.try_into().unwrap()); + let payment = helpers::payment_fixture(); + payments(storage) + .save(&verification_key.to_bytes(), &payment) + .unwrap(); + } + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000; + let page1 = query_payments_paged(deps.as_ref(), None, Option::from(crazy_limit)).unwrap(); + + // we default to a decent sized upper bound instead + assert_eq!(PAYMENT_PAGE_MAX_LIMIT, page1.payments.len() as u32); + } + + #[test] + fn payments_pagination_works() { + let key1 = PublicKey::new([1; 32]); + let key2 = PublicKey::new([2; 32]); + let key3 = PublicKey::new([3; 32]); + let key4 = PublicKey::new([4; 32]); + + let mut deps = helpers::init_contract(); + let payment = helpers::payment_fixture(); + payments(&mut deps.storage) + .save(&key1.to_bytes(), &payment) + .unwrap(); + + let per_page = 2; + let page1 = query_payments_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.payments.len()); + + // save another + payments(&mut deps.storage) + .save(&key2.to_bytes(), &payment) + .unwrap(); + + // page1 should have 2 results on it + let page1 = query_payments_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + assert_eq!(2, page1.payments.len()); + + payments(&mut deps.storage) + .save(&key3.to_bytes(), &payment) + .unwrap(); + + // page1 still has 2 results + let page1 = query_payments_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + assert_eq!(2, page1.payments.len()); + + // retrieving the next page should start after the last key on this page + let start_after = key2; + let page2 = query_payments_paged( + deps.as_ref(), + Option::from(start_after), + Option::from(per_page), + ) + .unwrap(); + + assert_eq!(1, page2.payments.len()); + + // save another one + payments(&mut deps.storage) + .save(&key4.to_bytes(), &payment) + .unwrap(); + + let start_after = key2; + let page2 = query_payments_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.payments.len()); + } +} diff --git a/contracts/erc20-bridge/src/storage.rs b/contracts/erc20-bridge/src/storage.rs new file mode 100644 index 0000000000..42682827f1 --- /dev/null +++ b/contracts/erc20-bridge/src/storage.rs @@ -0,0 +1,79 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::Storage; +use cosmwasm_storage::{bucket, bucket_read, Bucket, ReadonlyBucket}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use erc20_bridge_contract::payment::Payment; + +// buckets +const PREFIX_PAYMENTS: &[u8] = b"payments"; +const PREFIX_STATUS: &[u8] = b"status"; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub enum Status { + Unchecked, + Checked, + Spent, +} + +pub fn payments(storage: &mut dyn Storage) -> Bucket { + bucket(storage, PREFIX_PAYMENTS) +} + +pub fn payments_read(storage: &dyn Storage) -> ReadonlyBucket { + bucket_read(storage, PREFIX_PAYMENTS) +} + +pub fn status(storage: &mut dyn Storage) -> Bucket { + bucket(storage, PREFIX_STATUS) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::support::tests::helpers; + use cosmwasm_std::testing::MockStorage; + use erc20_bridge_contract::keys::PublicKey; + + #[test] + fn payments_single_read_retrieval() { + let mut storage = MockStorage::new(); + let key1 = PublicKey::new([1; 32]); + let key2 = PublicKey::new([2; 32]); + let payment1 = helpers::payment_fixture(); + let payment2 = helpers::payment_fixture(); + payments(&mut storage) + .save(key1.as_ref(), &payment1) + .unwrap(); + payments(&mut storage) + .save(key2.as_ref(), &payment2) + .unwrap(); + + let res1 = payments_read(&storage).load(key1.as_ref()).unwrap(); + let res2 = payments_read(&storage).load(key2.as_ref()).unwrap(); + assert_eq!(payment1, res1); + assert_eq!(payment2, res2); + } + + #[test] + fn status_single_read_retrieval() { + let mut storage = MockStorage::new(); + let key1 = PublicKey::new([1; 32]); + let key2 = PublicKey::new([2; 32]); + let status_value = Status::Unchecked; + status(&mut storage) + .save(key1.as_ref(), &status_value) + .unwrap(); + status(&mut storage) + .save(key2.as_ref(), &status_value) + .unwrap(); + + let res1 = status(&mut storage).load(key1.as_ref()).unwrap(); + assert_eq!(status_value, res1); + let res2 = status(&mut storage).load(key2.as_ref()).unwrap(); + assert_eq!(status_value, res2); + } +} diff --git a/contracts/erc20-bridge/src/support/mod.rs b/contracts/erc20-bridge/src/support/mod.rs new file mode 100644 index 0000000000..3e1ec563d5 --- /dev/null +++ b/contracts/erc20-bridge/src/support/mod.rs @@ -0,0 +1,3 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 +pub mod tests; diff --git a/contracts/erc20-bridge/src/support/tests.rs b/contracts/erc20-bridge/src/support/tests.rs new file mode 100644 index 0000000000..7fb16b8e05 --- /dev/null +++ b/contracts/erc20-bridge/src/support/tests.rs @@ -0,0 +1,28 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(test)] +pub mod helpers { + use crate::instantiate; + use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier}; + use cosmwasm_std::{Empty, MemoryStorage, OwnedDeps}; + use erc20_bridge_contract::keys::PublicKey; + use erc20_bridge_contract::msg::InstantiateMsg; + use erc20_bridge_contract::payment::Payment; + + pub fn init_contract() -> OwnedDeps> { + let mut deps = mock_dependencies(&[]); + let msg = InstantiateMsg {}; + let env = mock_env(); + let info = mock_info("creator", &[]); + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + return deps; + } + + pub fn payment_fixture() -> Payment { + let public_key = PublicKey::new([1; 32]); + let gateway_identity = PublicKey::new([2; 32]); + let bandwidth = 42; + Payment::new(public_key, gateway_identity, bandwidth) + } +} diff --git a/contracts/erc20-bridge/src/transactions.rs b/contracts/erc20-bridge/src/transactions.rs new file mode 100644 index 0000000000..ec953bc0fa --- /dev/null +++ b/contracts/erc20-bridge/src/transactions.rs @@ -0,0 +1,146 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; + +use crate::error::ContractError; +use crate::storage::{payments, status, Status}; +use erc20_bridge_contract::payment::{LinkPaymentData, Payment}; + +pub(crate) fn link_payment( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + data: LinkPaymentData, +) -> Result { + let mut status_bucket = status(deps.storage); + + let verification_key = data.verification_key.to_bytes(); + let gateway_identity = data.gateway_identity.to_bytes(); + let message: Vec = verification_key + .iter() + .chain(gateway_identity.iter()) + .copied() + .collect(); + let signature = data.signature.to_bytes(); + + if let Ok(Some(_)) = status_bucket.may_load(&verification_key) { + return Err(ContractError::PaymentAlreadyClaimed); + } + + if !deps + .api + .ed25519_verify(&message, &signature, &verification_key)? + { + return Err(ContractError::BadSignature); + } + + status_bucket.save(&verification_key, &Status::Unchecked)?; + payments(deps.storage).save( + &verification_key, + &Payment::new(data.verification_key, data.gateway_identity, data.bandwidth), + )?; + + Ok(Response::default()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::storage::payments_read; + use crate::support::tests::helpers; + use cosmwasm_std::testing::{mock_env, mock_info}; + use erc20_bridge_contract::keys::PublicKey; + + #[test] + fn bad_signature_payment() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let info = mock_info("owner", &[]); + + let payment_data = LinkPaymentData::new([1; 32], [2; 32], 42, [3; 64]); + + assert_eq!( + link_payment(deps.as_mut(), env, info, payment_data), + Err(ContractError::BadSignature) + ); + } + + #[test] + fn good_payment() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let info = mock_info("owner", &[]); + + let verification_key = [ + 78, 142, 213, 13, 39, 169, 76, 205, 242, 206, 129, 208, 190, 51, 139, 206, 245, 199, + 120, 151, 181, 250, 192, 153, 123, 104, 129, 139, 60, 254, 243, 98, + ]; + let gateway_identity = [ + 106, 76, 76, 238, 214, 177, 233, 112, 56, 33, 21, 201, 89, 42, 69, 196, 175, 56, 6, + 110, 184, 167, 203, 63, 1, 167, 134, 102, 165, 215, 3, 212, + ]; + let bandwidth = 42; + let signature = [ + 200, 134, 156, 198, 113, 180, 129, 90, 70, 28, 176, 201, 35, 208, 145, 28, 15, 16, 9, + 110, 148, 188, 193, 75, 157, 201, 206, 211, 128, 215, 66, 207, 175, 155, 48, 24, 171, + 254, 9, 37, 108, 205, 143, 37, 77, 189, 162, 52, 44, 130, 173, 60, 220, 22, 193, 3, + 111, 90, 123, 147, 206, 8, 137, 1, + ]; + + let payment_data = + LinkPaymentData::new(verification_key, gateway_identity, bandwidth, signature); + + assert!(link_payment(deps.as_mut(), env, info, payment_data).is_ok()); + + assert_eq!( + payments_read(&deps.storage) + .load(&verification_key) + .unwrap(), + Payment::new( + PublicKey::new(verification_key), + PublicKey::new(gateway_identity), + bandwidth + ) + ); + assert_eq!( + status(&mut deps.storage).load(&verification_key).unwrap(), + Status::Unchecked + ) + } + + #[test] + fn double_spend_protection() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let info = mock_info("owner", &[]); + + let verification_key = [ + 78, 142, 213, 13, 39, 169, 76, 205, 242, 206, 129, 208, 190, 51, 139, 206, 245, 199, + 120, 151, 181, 250, 192, 153, 123, 104, 129, 139, 60, 254, 243, 98, + ]; + let gateway_identity = [ + 106, 76, 76, 238, 214, 177, 233, 112, 56, 33, 21, 201, 89, 42, 69, 196, 175, 56, 6, + 110, 184, 167, 203, 63, 1, 167, 134, 102, 165, 215, 3, 212, + ]; + let bandwidth = 42; + let signature = [ + 200, 134, 156, 198, 113, 180, 129, 90, 70, 28, 176, 201, 35, 208, 145, 28, 15, 16, 9, + 110, 148, 188, 193, 75, 157, 201, 206, 211, 128, 215, 66, 207, 175, 155, 48, 24, 171, + 254, 9, 37, 108, 205, 143, 37, 77, 189, 162, 52, 44, 130, 173, 60, 220, 22, 193, 3, + 111, 90, 123, 147, 206, 8, 137, 1, + ]; + + let payment_data = + LinkPaymentData::new(verification_key, gateway_identity, bandwidth, signature); + + link_payment(deps.as_mut(), env.clone(), info.clone(), payment_data).unwrap(); + + // Only the verification key is used for double spending protection, the other data is irrelevant + let second_payment_data = LinkPaymentData::new(verification_key, [1; 32], 10, [2; 64]); + assert_eq!( + link_payment(deps.as_mut(), env, info, second_payment_data), + Err(ContractError::PaymentAlreadyClaimed) + ) + } +} diff --git a/contracts/mixnet/.gitignore b/contracts/mixnet/.gitignore new file mode 100644 index 0000000000..1761c01d05 --- /dev/null +++ b/contracts/mixnet/.gitignore @@ -0,0 +1 @@ +.envrc \ No newline at end of file diff --git a/contracts/mixnet/Cargo.lock b/contracts/mixnet/Cargo.lock index 90c5f59a0f..357afd9bed 100644 --- a/contracts/mixnet/Cargo.lock +++ b/contracts/mixnet/Cargo.lock @@ -23,9 +23,9 @@ dependencies = [ [[package]] name = "ast_node" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93f52ce8fac3d0e6720a92b0576d737c01b1b5db4dd786e962e5925f00bf755" +checksum = "e96d5444b02f3080edac8a144f6baf29b2fb6ff589ad4311559731a7c7529381" dependencies = [ "darling", "pmutil", @@ -41,12 +41,24 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "az" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6dff4a1892b54d70af377bf7a17064192e822865791d812957f21e3108c325" + [[package]] name = "base64" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "block-buffer" version = "0.7.3" @@ -79,9 +91,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.7.1" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538" +checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" [[package]] name = "byte-tools" @@ -89,6 +101,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" +[[package]] +name = "bytemuck" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72957246c41db82b8ef88a5486143830adeb8227ef9837740bdec67724cf2c5b" + [[package]] name = "byteorder" version = "1.4.3" @@ -121,9 +139,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c32f031ea41b4291d695026c023b95d59db2d8a2c7640800ed56bc8f510f22" +checksum = "9d6f2aa4d0537bcc1c74df8755072bd31c1ef1a3a1b85a68e8404a8c353b7b8b" [[package]] name = "cosmwasm-crypto" @@ -147,9 +165,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6a9a6a4ca3b4d7b56f943312a65857cdfc84424f9c2773889f4cd17f36fba61" +checksum = "04159eec9b583671db7923ff2b979736dfb8f0152347cab9fd02373c22e1a870" dependencies = [ "schemars", "serde_json", @@ -181,9 +199,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.1.4" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed00c67cb5d0a7d64a44f6ad2668db7e7530311dd53ea79bcd4fb022c64911c8" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" dependencies = [ "libc", ] @@ -196,9 +214,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-bigint" -version = "0.2.2" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b32a398eb1ccfbe7e4f452bc749c44d38dd732e9a253f19da224c416f00ee7f4" +checksum = "f83bd3bb4314701c568e340cd8cf78c975aa0ca79e03d3f6d1677d5b0c9c0c03" dependencies = [ "generic-array 0.14.4", "rand_core 0.6.3", @@ -218,9 +236,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "639891fde0dbea823fc3d798a0fdf9d2f9440a42d64a78ab3488b0ca025117b3" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" dependencies = [ "byteorder", "digest 0.9.0", @@ -266,9 +284,9 @@ dependencies = [ [[package]] name = "der" -version = "0.4.0" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f215f706081a44cb702c71c39a52c05da637822e9c1645a50b7202689e982d" +checksum = "28e98c534e9c8a0483aa01d6f6913bc063de254311bd267c9cf535e9b70e15b2" dependencies = [ "const-oid", ] @@ -338,9 +356,9 @@ checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf" [[package]] name = "ecdsa" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713c32426287891008edb98f8b5c6abb2130aa043c93a818728fcda78606f274" +checksum = "43ee23aa5b4f68c7a092b5c3beb25f50c406adc75e2363634f242f28ab255372" dependencies = [ "der", "elliptic-curve", @@ -370,9 +388,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "elliptic-curve" -version = "0.10.4" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e5c176479da93a0983f0a6fdc3c1b8e7d5be0d7fe3fe05a99f15b96582b9a8" +checksum = "beca177dcb8eb540133e7680baff45e7cc4d93bf22002676cec549f82343721b" dependencies = [ "crypto-bigint", "ff", @@ -404,14 +422,26 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" [[package]] name = "ff" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63eec06c61e487eecf0f7e6e6372e596a81922c28d33e645d6983ca6493a1af0" +checksum = "d0f40b2dcd8bc322217a5f6559ae5f9e9d1de202a2ecee2e9eafcbece7562a4f" dependencies = [ "rand_core 0.6.3", "subtle", ] +[[package]] +name = "fixed" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d333a26ec13a023c6dff4b7584de4d323cfee2e508f5dd2bbee6669e4f7efdf" +dependencies = [ + "az", + "bytemuck", + "half", + "typenum", +] + [[package]] name = "fnv" version = "1.0.7" @@ -501,6 +531,12 @@ dependencies = [ "subtle", ] +[[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" @@ -521,6 +557,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-literal" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e4590e13640f19f249fe3e4eca5113bc4289f2497710378190e7f4bd96f45b" + [[package]] name = "hmac" version = "0.11.0" @@ -564,6 +606,15 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "is-macro" version = "0.1.9" @@ -579,15 +630,15 @@ dependencies = [ [[package]] name = "itoa" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "k256" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b0281ca8032567c9711cd48631781c15228301860a39b32deb28d63125e46" +checksum = "903ae2481bcdfdb7b68e0a9baa4b7c9aff600b9ae2e8e5bb5833b8c91ab851ea" dependencies = [ "cfg-if 1.0.0", "ecdsa", @@ -603,9 +654,18 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1fa8cddc8fbbee11227ef194b5317ed014b8acbf15139bd716a18ad3fe99ec5" +checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013" + +[[package]] +name = "lock_api" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +dependencies = [ + "scopeguard", +] [[package]] name = "log" @@ -624,9 +684,9 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "matches" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "memchr" @@ -638,10 +698,15 @@ checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" name = "mixnet-contract" version = "0.1.0" dependencies = [ + "az", "cosmwasm-std", + "fixed", + "log", + "network-defaults", "schemars", "serde", "serde_repr", + "thiserror", "ts-rs", ] @@ -653,6 +718,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", + "fixed", "mixnet-contract", "schemars", "serde", @@ -663,6 +729,7 @@ dependencies = [ name = "network-defaults" version = "0.1.0" dependencies = [ + "hex-literal", "serde", "time", "url", @@ -732,6 +799,31 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if 1.0.0", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -802,9 +894,9 @@ dependencies = [ [[package]] name = "pkcs8" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbee84ed13e44dd82689fa18348a49934fa79cc774a344c42fc9b301c71b140a" +checksum = "ee3ef9b64d26bad0536099c816c6734379e45bbd5f14798def6809e5cc350447" dependencies = [ "der", "spki", @@ -823,9 +915,9 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.10" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" [[package]] name = "precomputed-hash" @@ -835,9 +927,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "proc-macro2" -version = "1.0.24" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" dependencies = [ "unicode-xid", ] @@ -850,9 +942,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" dependencies = [ "proc-macro2", ] @@ -917,6 +1009,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.5.4" @@ -942,9 +1043,9 @@ checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" [[package]] name = "schemars" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6ab463ae35acccb5cba66c0084c985257b797d288b6050cc2f6ac1b266cb78" +checksum = "d7a48d098c2a7fdf5740b19deb1181b4fb8a9e68e03ae517c14cde04b5725409" dependencies = [ "dyn-clone", "schemars_derive", @@ -954,9 +1055,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "902fdfbcf871ae8f653bddf4b2c05905ddaabc08f69d32a915787e3be0d31356" +checksum = "4a9ea2a613fe4cd7118b2bb101a25d8ae6192e1975179b67b2f17afd11e70ac8" dependencies = [ "proc-macro2", "quote", @@ -971,10 +1072,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" [[package]] -name = "serde" -version = "1.0.122" +name = "scopeguard" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "974ef1bd2ad8a507599b336595454081ff68a9599b4890af7643c0c0ed73a62c" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" dependencies = [ "serde_derive", ] @@ -990,9 +1097,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.122" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dee1f300f838c8ac340ecb0112b3ac472464fa67e87292bdb3dfc9c49128e17" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" dependencies = [ "proc-macro2", "quote", @@ -1012,9 +1119,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.61" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a" +checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" dependencies = [ "itoa", "ryu", @@ -1046,9 +1153,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.9.5" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" +checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa" dependencies = [ "block-buffer 0.9.0", "cfg-if 1.0.0", @@ -1059,9 +1166,9 @@ dependencies = [ [[package]] name = "signature" -version = "1.3.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19772be3c4dd2ceaacf03cb41d5885f2a02c4d8804884918e3a258480803335" +checksum = "f2807892cfa58e081aa1f1111391c7a0649d4fa127a4ffbe34bcbfb35a1171a4" dependencies = [ "digest 0.9.0", "rand_core 0.6.3", @@ -1075,15 +1182,15 @@ checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b" [[package]] name = "smallvec" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" [[package]] name = "spki" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "987637c5ae6b3121aba9d513f869bd2bff11c4cc086c22473befd6649c0bd521" +checksum = "5c01a0c15da1b0b0e1494112e7af814a678fec9bd157881b49beac661e9b6f32" dependencies = [ "der", ] @@ -1102,12 +1209,13 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "string_cache" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a" +checksum = "923f0f39b6267d37d23ce71ae7235602134b250ace715dd2c90421998ddac0c6" dependencies = [ "lazy_static", "new_debug_unreachable", + "parking_lot", "phf_shared", "precomputed-hash", "serde", @@ -1146,15 +1254,15 @@ checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" [[package]] name = "subtle" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "swc_atoms" -version = "0.2.7" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "837a3ef86c2817228e733b6f173c821fd76f9eb21a0bc9001a826be48b00b4e7" +checksum = "9f5229fe227ff0060e13baa386d6e368797700eab909523f730008d191ee53ae" dependencies = [ "string_cache", "string_cache_codegen", @@ -1254,9 +1362,9 @@ dependencies = [ [[package]] name = "swc_macros_common" -version = "0.3.3" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08ed2e930f5a1a4071fe62c90fd3a296f6030e5d94bfe13993244423caf59a78" +checksum = "bf7c68e78ffbcba3d38abe6d0b76a0e1a37888b5c9301db3426537207090ada3" dependencies = [ "pmutil", "proc-macro2", @@ -1266,9 +1374,9 @@ dependencies = [ [[package]] name = "swc_visit" -version = "0.2.6" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a423caa0b4585118164dbad8f1ad52b592a9a9370b25decc4d84c6b4309132c0" +checksum = "f8511a4788ab29daf00bee23e425aac92c9be4eec74c98fec4a45d0e710be695" dependencies = [ "either", "swc_visit_macros", @@ -1290,9 +1398,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.65" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1d708c221c5a612956ef9f75b37e454e88d1f7b899fbd3a18d4252012d663" +checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" dependencies = [ "proc-macro2", "quote", @@ -1301,18 +1409,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.23" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.23" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", @@ -1321,9 +1429,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a776787d9c5d455bec3db044586ccdd8a9c74d5da5dc319fb80f3db08808fe6" +checksum = "99beeb0daeac2bd1e86ac2c21caddecb244b39a093594da1a661ec2060c7aedd" dependencies = [ "libc", "time-macros", @@ -1331,15 +1439,15 @@ dependencies = [ [[package]] name = "time-macros" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04a153416002296880a3b51329a0e3df31c779c53ec827993e865ce427982843" +checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6" [[package]] name = "tinyvec" -version = "1.3.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" +checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" dependencies = [ "tinyvec_macros", ] @@ -1383,9 +1491,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.13.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" +checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" [[package]] name = "ucd-trie" @@ -1395,9 +1503,9 @@ checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" [[package]] name = "uint" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11fe9a9348741cf134085ad57c249508345fe16411b3d7fb4ff2da2f1d6382e" +checksum = "6470ab50f482bde894a037a57064480a246dbfdd5960bd65a44824693f08da5f" dependencies = [ "byteorder", "crunchy", @@ -1407,12 +1515,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" -dependencies = [ - "matches", -] +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" [[package]] name = "unicode-normalization" @@ -1431,9 +1536,9 @@ checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" [[package]] name = "unicode-xid" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] name = "url" @@ -1466,7 +1571,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] -name = "zeroize" -version = "1.3.0" +name = "winapi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "zeroize" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf68b08513768deaa790264a7fac27a58cbf2705cfcdc9448362229217d7e970" diff --git a/contracts/mixnet/Cargo.toml b/contracts/mixnet/Cargo.toml index 13bd3ed584..0d803ecae1 100644 --- a/contracts/mixnet/Cargo.toml +++ b/contracts/mixnet/Cargo.toml @@ -49,3 +49,4 @@ thiserror = { version = "1.0.23" } [dev-dependencies] cosmwasm-schema = { version = "0.14.0" } +fixed = "1.1" diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 4dc9dac8e4..42ad4d4244 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -1,11 +1,13 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::u128; + use crate::helpers::calculate_epoch_reward_rate; use crate::state::State; use crate::storage::{config, layer_distribution}; use crate::{error::ContractError, queries, transactions}; -use config::defaults::NETWORK_MONITOR_ADDRESS; +use config::defaults::REWARDING_VALIDATOR_ADDRESS; use cosmwasm_std::{ entry_point, to_binary, Addr, Decimal, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, Uint128, @@ -24,23 +26,35 @@ pub const INITIAL_MIXNODE_BOND: Uint128 = Uint128(100_000000); pub const INITIAL_MIXNODE_BOND_REWARD_RATE: u64 = 110; pub const INITIAL_MIXNODE_DELEGATION_REWARD_RATE: u64 = 110; +pub const INITIAL_MIXNODE_REWARDED_SET_SIZE: u32 = 200; pub const INITIAL_MIXNODE_ACTIVE_SET_SIZE: u32 = 100; -fn default_initial_state(owner: Addr) -> State { +pub const INITIAL_REWARD_POOL: u128 = 250_000_000_000_000; +pub const EPOCH_REWARD_PERCENT: u8 = 2; // Used to calculate epoch reward pool +pub const DEFAULT_SYBIL_RESISTANCE_PERCENT: u8 = 30; + +// We'll be assuming a few more things, profit margin and cost function. Since we don't have relialable package measurement, we'll be using uptime. We'll also set the value of 1 Nym to 1 $, to be able to translate epoch costs to Nyms. We'll also assume a cost of 40$ per epoch(month), converting that to Nym at our 1$ rate translates to 40_000_000 uNyms +pub const DEFAULT_COST_PER_EPOCH: u32 = 40_000_000; + +fn default_initial_state(owner: Addr, env: Env) -> State { let mixnode_bond_reward_rate = Decimal::percent(INITIAL_MIXNODE_BOND_REWARD_RATE); let mixnode_delegation_reward_rate = Decimal::percent(INITIAL_MIXNODE_DELEGATION_REWARD_RATE); State { owner, - network_monitor_address: Addr::unchecked(NETWORK_MONITOR_ADDRESS), // we trust our hardcoded value + rewarding_validator_address: Addr::unchecked(REWARDING_VALIDATOR_ADDRESS), // we trust our hardcoded value params: StateParams { epoch_length: INITIAL_DEFAULT_EPOCH_LENGTH, minimum_mixnode_bond: INITIAL_MIXNODE_BOND, minimum_gateway_bond: INITIAL_GATEWAY_BOND, mixnode_bond_reward_rate, mixnode_delegation_reward_rate, + mixnode_rewarded_set_size: INITIAL_MIXNODE_REWARDED_SET_SIZE, mixnode_active_set_size: INITIAL_MIXNODE_ACTIVE_SET_SIZE, }, + rewarding_interval_starting_block: env.block.height, + latest_rewarding_interval_nonce: 0, + rewarding_in_progress: false, mixnode_epoch_bond_reward: calculate_epoch_reward_rate( INITIAL_DEFAULT_EPOCH_LENGTH, mixnode_bond_reward_rate, @@ -60,11 +74,11 @@ fn default_initial_state(owner: Addr) -> State { #[entry_point] pub fn instantiate( deps: DepsMut, - _env: Env, + env: Env, info: MessageInfo, _msg: InstantiateMsg, ) -> Result { - let state = default_initial_state(info.sender); + let state = default_initial_state(info.sender, env); config(deps.storage).save(&state)?; layer_distribution(deps.storage).save(&Default::default())?; @@ -91,15 +105,42 @@ pub fn execute( ExecuteMsg::UpdateStateParams(params) => { transactions::try_update_state_params(deps, info, params) } - ExecuteMsg::RewardMixnode { identity, uptime } => { - transactions::try_reward_mixnode(deps, env, info, identity, uptime) - } + ExecuteMsg::RewardMixnode { + identity, + uptime, + rewarding_interval_nonce, + } => transactions::try_reward_mixnode( + deps, + env, + info, + identity, + uptime, + rewarding_interval_nonce, + ), + ExecuteMsg::RewardMixnodeV2 { + identity, + params, + rewarding_interval_nonce, + } => transactions::try_reward_mixnode_v2( + deps, + env, + info, + identity, + params, + rewarding_interval_nonce, + ), ExecuteMsg::DelegateToMixnode { mix_identity } => { transactions::try_delegate_to_mixnode(deps, env, info, mix_identity) } ExecuteMsg::UndelegateFromMixnode { mix_identity } => { transactions::try_remove_delegation_from_mixnode(deps, info, mix_identity) } + ExecuteMsg::BeginMixnodeRewarding { + rewarding_interval_nonce, + } => transactions::try_begin_mixnode_rewarding(deps, env, info, rewarding_interval_nonce), + ExecuteMsg::FinishMixnodeRewarding { + rewarding_interval_nonce, + } => transactions::try_finish_mixnode_rewarding(deps, info, rewarding_interval_nonce), } } @@ -119,6 +160,9 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result to_binary(&queries::query_state_params(deps)), + QueryMsg::CurrentRewardingInterval {} => { + to_binary(&queries::query_rewarding_interval(deps)) + } QueryMsg::LayerDistribution {} => to_binary(&queries::query_layer_distribution(deps)), QueryMsg::GetMixDelegations { mix_identity, @@ -151,11 +195,14 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result to_binary(&queries::query_reward_pool(deps)), + QueryMsg::GetCirculatingSupply {} => to_binary(&queries::query_circulating_supply(deps)), + QueryMsg::GetEpochRewardPercent {} => to_binary(&EPOCH_REWARD_PERCENT), + QueryMsg::GetSybilResistancePercent {} => to_binary(&DEFAULT_SYBIL_RESISTANCE_PERCENT), }; Ok(query_res?) } - #[entry_point] pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { Ok(Default::default()) diff --git a/contracts/mixnet/src/error.rs b/contracts/mixnet/src/error.rs index 3610edee30..027f697407 100644 --- a/contracts/mixnet/src/error.rs +++ b/contracts/mixnet/src/error.rs @@ -51,6 +51,9 @@ pub enum ContractError { #[error("The delegation reward rate for mixnode was set to be lower than 1")] DecreasingMixnodeDelegationReward, + #[error("Provided active set size is bigger than the demanded set")] + InvalidActiveSetSize, + #[error("The node had uptime larger than 100%")] UnexpectedUptime, @@ -77,4 +80,24 @@ pub enum ContractError { identity: IdentityKey, address: Addr, }, + #[error("Overflow error!")] + Overflow(#[from] cosmwasm_std::OverflowError), + + #[error("We tried to remove more funds then are available in the Reward pool. Wanted to remove {to_remove}, but have only {reward_pool}")] + OutOfFunds { to_remove: u128, reward_pool: u128 }, + + #[error("Invalid ratio")] + Ratio(#[from] mixnet_contract::error::MixnetContractError), + + #[error("Received invalid rewarding interval nonce. Expected {expected}, received {received}")] + InvalidRewardingIntervalNonce { received: u32, expected: u32 }, + + #[error("Rewarding distribution is currently in progress")] + RewardingInProgress, + + #[error("Rewarding distribution is currently not in progress")] + RewardingNotInProgress, + + #[error("Mixnode {identity} has already been rewarded during the current rewarding interval")] + MixnodeAlreadyRewarded { identity: IdentityKey }, } diff --git a/contracts/mixnet/src/helpers.rs b/contracts/mixnet/src/helpers.rs index de5636a0ac..6c86b4034e 100644 --- a/contracts/mixnet/src/helpers.rs +++ b/contracts/mixnet/src/helpers.rs @@ -19,11 +19,11 @@ const DECIMAL_FRACTIONAL: Uint128 = Uint128(1_000_000_000_000_000_000u128); // cosmwasm bucket internal value const NAMESPACE_LENGTH: usize = 2; -fn decimal_to_uint128(value: Decimal) -> Uint128 { +pub fn decimal_to_uint128(value: Decimal) -> Uint128 { value * DECIMAL_FRACTIONAL } -fn uint128_to_decimal(value: Uint128) -> Decimal { +pub fn uint128_to_decimal(value: Uint128) -> Decimal { Decimal::from_ratio(value, DECIMAL_FRACTIONAL) } diff --git a/contracts/mixnet/src/queries.rs b/contracts/mixnet/src/queries.rs index e76d62a93c..e6f0596a44 100644 --- a/contracts/mixnet/src/queries.rs +++ b/contracts/mixnet/src/queries.rs @@ -4,17 +4,17 @@ use crate::error::ContractError; use crate::helpers::get_all_delegations_paged; use crate::storage::{ - all_mix_delegations_read, gateways_owners_read, gateways_read, mix_delegations_read, - mixnodes_owners_read, mixnodes_read, read_layer_distribution, read_state_params, - reverse_mix_delegations_read, + all_mix_delegations_read, circulating_supply, config_read, gateways_owners_read, gateways_read, + mix_delegations_read, mixnodes_owners_read, mixnodes_read, read_layer_distribution, + read_state_params, reverse_mix_delegations_read, reward_pool_value, }; use config::defaults::DENOM; -use cosmwasm_std::{coin, Addr, Deps, Order, StdResult}; +use cosmwasm_std::{coin, Addr, Deps, Order, StdResult, Uint128}; use mixnet_contract::{ Delegation, GatewayBond, GatewayOwnershipResponse, IdentityKey, LayerDistribution, MixNodeBond, MixOwnershipResponse, PagedAllDelegationsResponse, PagedGatewayResponse, PagedMixDelegationsResponse, PagedMixnodeResponse, PagedReverseMixDelegationsResponse, - RawDelegationData, StateParams, + RawDelegationData, RewardingIntervalResponse, StateParams, }; const BOND_PAGE_MAX_LIMIT: u32 = 100; @@ -87,10 +87,27 @@ pub(crate) fn query_state_params(deps: Deps) -> StateParams { read_state_params(deps.storage) } +pub(crate) fn query_rewarding_interval(deps: Deps) -> RewardingIntervalResponse { + let state = config_read(deps.storage).load().unwrap(); + RewardingIntervalResponse { + current_rewarding_interval_starting_block: state.rewarding_interval_starting_block, + current_rewarding_interval_nonce: state.latest_rewarding_interval_nonce, + rewarding_in_progress: state.rewarding_in_progress, + } +} + pub(crate) fn query_layer_distribution(deps: Deps) -> LayerDistribution { read_layer_distribution(deps.storage) } +pub(crate) fn query_reward_pool(deps: Deps) -> Uint128 { + reward_pool_value(deps.storage) +} + +pub(crate) fn query_circulating_supply(deps: Deps) -> Uint128 { + circulating_supply(deps.storage) +} + /// Adds a 0 byte to terminate the `start_after` value given. This allows CosmWasm /// to get the succeeding key as the start of the next page. // S works for both `String` and `Addr` and that's what we wanted @@ -562,15 +579,19 @@ pub(crate) mod tests { let dummy_state = State { owner: Addr::unchecked("someowner"), - network_monitor_address: Addr::unchecked("monitor"), + rewarding_validator_address: Addr::unchecked("monitor"), params: StateParams { epoch_length: 1, minimum_mixnode_bond: 123u128.into(), minimum_gateway_bond: 456u128.into(), mixnode_bond_reward_rate: "1.23".parse().unwrap(), mixnode_delegation_reward_rate: "7.89".parse().unwrap(), - mixnode_active_set_size: 1000, + mixnode_rewarded_set_size: 1000, + mixnode_active_set_size: 500, }, + rewarding_interval_starting_block: 123, + latest_rewarding_interval_nonce: 0, + rewarding_in_progress: false, mixnode_epoch_bond_reward: "1.23".parse().unwrap(), mixnode_epoch_delegation_reward: "7.89".parse().unwrap(), }; diff --git a/contracts/mixnet/src/state.rs b/contracts/mixnet/src/state.rs index 6371e6529f..4440c776eb 100644 --- a/contracts/mixnet/src/state.rs +++ b/contracts/mixnet/src/state.rs @@ -9,9 +9,16 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct State { pub owner: Addr, // only the owner account can update state - pub network_monitor_address: Addr, + pub rewarding_validator_address: Addr, pub params: StateParams, + // keep track of the changes to the current rewarding interval, + // i.e. at which block has the latest rewarding occurred + // and whether another run is already in progress + pub rewarding_interval_starting_block: u64, + pub latest_rewarding_interval_nonce: u32, + pub rewarding_in_progress: bool, + // helper values to avoid having to recalculate them on every single payment operation pub mixnode_epoch_bond_reward: Decimal, // reward per epoch expressed as a decimal like 0.05 pub mixnode_epoch_delegation_reward: Decimal, // reward per epoch expressed as a decimal like 0.05 diff --git a/contracts/mixnet/src/storage.rs b/contracts/mixnet/src/storage.rs index 80aaddecf3..ec690dce16 100644 --- a/contracts/mixnet/src/storage.rs +++ b/contracts/mixnet/src/storage.rs @@ -1,14 +1,16 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 - -use crate::queries; +use crate::contract::INITIAL_REWARD_POOL; use crate::state::State; use crate::transactions::MINIMUM_BLOCK_AGE_FOR_REWARDING; +use crate::{error::ContractError, queries}; +use config::defaults::TOTAL_SUPPLY; use cosmwasm_std::{Decimal, Order, StdResult, Storage, Uint128}; use cosmwasm_storage::{ bucket, bucket_read, singleton, singleton_read, Bucket, ReadonlyBucket, ReadonlySingleton, Singleton, }; +use mixnet_contract::mixnode::NodeRewardParams; use mixnet_contract::{ Addr, GatewayBond, IdentityKey, IdentityKeyRef, Layer, LayerDistribution, MixNodeBond, RawDelegationData, StateParams, @@ -24,9 +26,10 @@ use serde::Serialize; // singletons const CONFIG_KEY: &[u8] = b"config"; const LAYER_DISTRIBUTION_KEY: &[u8] = b"layers"; +const REWARD_POOL_PREFIX: &[u8] = b"pool"; // buckets -const PREFIX_MIXNODES: &[u8] = b"mn"; +pub const PREFIX_MIXNODES: &[u8] = b"mn"; const PREFIX_MIXNODES_OWNERS: &[u8] = b"mo"; const PREFIX_GATEWAYS: &[u8] = b"gt"; const PREFIX_GATEWAYS_OWNERS: &[u8] = b"go"; @@ -34,6 +37,8 @@ const PREFIX_GATEWAYS_OWNERS: &[u8] = b"go"; const PREFIX_MIX_DELEGATION: &[u8] = b"md"; const PREFIX_REVERSE_MIX_DELEGATION: &[u8] = b"dm"; +const PREFIX_REWARDED_MIXNODES: &[u8] = b"rm"; + // Contract-level stuff // TODO Unify bucket and mixnode storage functions @@ -46,6 +51,53 @@ pub fn config_read(storage: &dyn Storage) -> ReadonlySingleton { singleton_read(storage, CONFIG_KEY) } +fn reward_pool(storage: &dyn Storage) -> ReadonlySingleton { + singleton_read(storage, REWARD_POOL_PREFIX) +} + +pub fn mut_reward_pool(storage: &mut dyn Storage) -> Singleton { + singleton(storage, REWARD_POOL_PREFIX) +} + +pub fn reward_pool_value(storage: &dyn Storage) -> Uint128 { + match reward_pool(storage).load() { + Ok(value) => value, + Err(_e) => Uint128(INITIAL_REWARD_POOL), + } +} + +#[allow(dead_code)] +pub fn incr_reward_pool( + amount: Uint128, + storage: &mut dyn Storage, +) -> Result { + let stake = reward_pool_value(storage).saturating_add(amount); + mut_reward_pool(storage).save(&stake)?; + Ok(stake) +} + +pub fn decr_reward_pool( + amount: Uint128, + storage: &mut dyn Storage, +) -> Result { + let stake = match reward_pool_value(storage).checked_sub(amount) { + Ok(stake) => stake, + Err(_e) => { + return Err(ContractError::OutOfFunds { + to_remove: amount.u128(), + reward_pool: reward_pool_value(storage).u128(), + }) + } + }; + mut_reward_pool(storage).save(&stake)?; + Ok(stake) +} + +pub fn circulating_supply(storage: &dyn Storage) -> Uint128 { + let reward_pool = reward_pool_value(storage).u128(); + Uint128(TOTAL_SUPPLY - reward_pool) +} + pub(crate) fn read_state_params(storage: &dyn Storage) -> StateParams { // note: In any other case, I wouldn't have attempted to unwrap this result, but in here // if we fail to load the stored state we would already be in the undefined behaviour land, @@ -53,22 +105,6 @@ pub(crate) fn read_state_params(storage: &dyn Storage) -> StateParams { config_read(storage).load().unwrap().params } -pub(crate) fn read_mixnode_epoch_bond_reward_rate(storage: &dyn Storage) -> Decimal { - // same justification as in `read_state_params` for the unwrap - config_read(storage) - .load() - .unwrap() - .mixnode_epoch_bond_reward -} - -pub(crate) fn read_mixnode_epoch_delegation_reward_rate(storage: &dyn Storage) -> Decimal { - // same justification as in `read_state_params` for the unwrap - config_read(storage) - .load() - .unwrap() - .mixnode_epoch_delegation_reward -} - pub fn layer_distribution(storage: &mut dyn Storage) -> Singleton { singleton(storage, LAYER_DISTRIBUTION_KEY) } @@ -146,6 +182,35 @@ pub fn mixnodes_owners_read(storage: &dyn Storage) -> ReadonlyBucket Bucket { + Bucket::multilevel( + storage, + &[ + rewarding_interval_nonce.to_be_bytes().as_ref(), + PREFIX_REWARDED_MIXNODES, + ], + ) +} + +// we want to treat this bucket as a set so we don't really care about what type of data is being stored. +// I went with u8 as after serialization it takes only a single byte of space, while if a `()` was used, +// it would have taken 4 bytes (representation of 'null') +pub fn rewarded_mixnodes_read( + storage: &dyn Storage, + rewarding_interval_nonce: u32, +) -> ReadonlyBucket { + ReadonlyBucket::multilevel( + storage, + &[ + rewarding_interval_nonce.to_be_bytes().as_ref(), + PREFIX_REWARDED_MIXNODES, + ], + ) +} + // helpers pub(crate) fn increase_mix_delegated_stakes( storage: &mut dyn Storage, @@ -196,6 +261,55 @@ pub(crate) fn increase_mix_delegated_stakes( Ok(total_rewarded) } +pub(crate) fn increase_mix_delegated_stakes_v2( + storage: &mut dyn Storage, + bond: &MixNodeBond, + params: &NodeRewardParams, +) -> Result { + let chunk_size = queries::DELEGATION_PAGE_MAX_LIMIT as usize; + + let mut total_rewarded = Uint128::zero(); + let mut chunk_start: Option> = None; + loop { + // get `chunk_size` of delegations + let delegations_chunk = mix_delegations_read(storage, bond.identity()) + .range(chunk_start.as_deref(), None, Order::Ascending) + .take(chunk_size) + .collect::>>()?; + + if delegations_chunk.is_empty() { + break; + } + + // append 0 byte to the last value to start with whatever is the next succeeding key + chunk_start = Some( + delegations_chunk + .last() + .unwrap() + .0 + .iter() + .cloned() + .chain(std::iter::once(0u8)) + .collect(), + ); + + // and for each of them increase the stake proportionally to the reward + // if at least `MINIMUM_BLOCK_AGE_FOR_REWARDING` blocks have been created + // since they delegated + for (delegator_address, mut delegation) in delegations_chunk.into_iter() { + if delegation.block_height + MINIMUM_BLOCK_AGE_FOR_REWARDING + <= params.reward_blockstamp() + { + let reward = bond.reward_delegation(delegation.amount, params); + delegation.amount += Uint128(reward); + total_rewarded += Uint128(reward); + mix_delegations(storage, bond.identity()).save(&delegator_address, &delegation)?; + } + } + } + + Ok(total_rewarded) +} // currently not used outside tests #[cfg(test)] pub(crate) fn read_mixnode_bond( @@ -345,6 +459,7 @@ mod tests { identity_key: node_identity.clone(), ..mix_node_fixture() }, + profit_margin_percent: Some(10), }; mixnodes(&mut storage) diff --git a/contracts/mixnet/src/support/tests.rs b/contracts/mixnet/src/support/tests.rs index 70b761e0fd..0b36814fb7 100644 --- a/contracts/mixnet/src/support/tests.rs +++ b/contracts/mixnet/src/support/tests.rs @@ -133,6 +133,7 @@ pub mod helpers { Layer::One, 12_345, mix_node, + None, ) } diff --git a/contracts/mixnet/src/transactions.rs b/contracts/mixnet/src/transactions.rs index 8173243bbc..de5e3f5f07 100644 --- a/contracts/mixnet/src/transactions.rs +++ b/contracts/mixnet/src/transactions.rs @@ -1,6 +1,5 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 - use crate::error::ContractError; use crate::helpers::{calculate_epoch_reward_rate, scale_reward_by_uptime, Delegations}; use crate::queries; @@ -10,13 +9,19 @@ use cosmwasm_std::{ attr, coins, BankMsg, Coin, Decimal, DepsMut, Env, MessageInfo, Response, StdResult, Uint128, }; use cosmwasm_storage::ReadonlyBucket; +use mixnet_contract::mixnode::NodeRewardParams; use mixnet_contract::{ Gateway, GatewayBond, IdentityKey, Layer, MixNode, MixNodeBond, RawDelegationData, StateParams, }; pub(crate) const OLD_DELEGATIONS_CHUNK_SIZE: usize = 500; + +// approximately 1 day (assuming 5s per block) pub(crate) const MINIMUM_BLOCK_AGE_FOR_REWARDING: u64 = 17280; +// approximately 30min (assuming 5s per block) +pub(crate) const MAX_REWARDING_DURATION_IN_BLOCKS: u64 = 360; + fn total_delegations(delegations_bucket: ReadonlyBucket) -> StdResult { Ok(Coin::new( Delegations::new(delegations_bucket) @@ -99,6 +104,7 @@ pub(crate) fn try_add_mixnode( layer, env.block.height, mix_node, + None, ); // this might potentially require more gas if a significant number of delegations was there @@ -299,7 +305,7 @@ pub(crate) fn try_update_state_params( // note: In any other case, I wouldn't have attempted to unwrap this result, but in here // if we fail to load the stored state we would already be in the undefined behaviour land, // so we better just blow up immediately. - let mut state = config_read(deps.storage).load().unwrap(); + let mut state = config_read(deps.storage).load()?; // check if this is executed by the owner, if not reject the transaction if info.sender != state.owner { @@ -314,6 +320,12 @@ pub(crate) fn try_update_state_params( return Err(ContractError::DecreasingMixnodeDelegationReward); } + // 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); + } + // if we're updating epoch length, recalculate rewards for mixnodes if state.params.epoch_length != params.epoch_length { state.mixnode_epoch_bond_reward = @@ -341,6 +353,57 @@ pub(crate) fn try_update_state_params( Ok(Response::default()) } +// Note: this function is designed to work with only a single validator entity distributing rewards +// The main purpose of this function is to update `latest_rewarding_interval_nonce` which +// will trigger a different seed selection for the pseudorandom generation of the "demanded" set of mixnodes. +pub(crate) fn try_begin_mixnode_rewarding( + deps: DepsMut, + env: Env, + info: MessageInfo, + rewarding_interval_nonce: u32, +) -> Result { + let mut state = config_read(deps.storage).load()?; + + // 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); + } + + // check whether sufficient number of blocks already elapsed since the previous rewarding happened + // (this implies the validator responsible for rewarding in the previous interval did not call + // `try_finish_mixnode_rewarding` - perhaps they crashed or something. Regardless of the reason + // it shouldn't prevent anyone from distributing rewards in the following interval) + // Do note, however, that calling `try_finish_mixnode_rewarding` is crucial as otherwise the + // "demanded" set won't get updated on the validator API side + if state.rewarding_in_progress + && state.rewarding_interval_starting_block + MAX_REWARDING_DURATION_IN_BLOCKS + > env.block.height + { + return Err(ContractError::RewardingInProgress); + } + + // make sure the validator is in sync with the contract state + if rewarding_interval_nonce != state.latest_rewarding_interval_nonce + 1 { + return Err(ContractError::InvalidRewardingIntervalNonce { + received: rewarding_interval_nonce, + expected: state.latest_rewarding_interval_nonce + 1, + }); + } + + state.rewarding_interval_starting_block = env.block.height; + state.latest_rewarding_interval_nonce = rewarding_interval_nonce; + state.rewarding_in_progress = true; + + config(deps.storage).save(&state)?; + + let mut response = Response::new(); + response.add_attribute( + "rewarding interval nonce", + rewarding_interval_nonce.to_string(), + ); + Ok(response) +} + // Note: if any changes are made to this function or anything it is calling down the stack, // for example delegation reward distribution, the gas limits must be retested and both // validator-api/src/rewarding/mod.rs::{MIXNODE_REWARD_OP_BASE_GAS_LIMIT, PER_MIXNODE_DELEGATION_GAS_INCREASE} @@ -351,16 +414,41 @@ pub(crate) fn try_reward_mixnode( info: MessageInfo, mix_identity: IdentityKey, uptime: u32, + rewarding_interval_nonce: u32, ) -> Result { - let state = config_read(deps.storage).load().unwrap(); + let state = config_read(deps.storage).load()?; - // check if this is executed by the monitor, if not reject the transaction - if info.sender != state.network_monitor_address { + // 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); } + if !state.rewarding_in_progress { + return Err(ContractError::RewardingNotInProgress); + } + + // make sure the transaction is sent for the correct rewarding interval + if rewarding_interval_nonce != state.latest_rewarding_interval_nonce { + return Err(ContractError::InvalidRewardingIntervalNonce { + received: rewarding_interval_nonce, + expected: state.latest_rewarding_interval_nonce, + }); + } + + // check if the mixnode hasn't been rewarded in this rewarding interval already + if rewarded_mixnodes_read(deps.storage, rewarding_interval_nonce) + .may_load(mix_identity.as_bytes())? + .is_some() + { + return Err(ContractError::MixnodeAlreadyRewarded { + identity: mix_identity, + }); + } + // optimisation for uptime being 0. No rewards will be given so just terminate here if uptime == 0 { + rewarded_mixnodes(deps.storage, rewarding_interval_nonce) + .save(mix_identity.as_bytes(), &Default::default())?; return Ok(Response { submessages: vec![], messages: vec![], @@ -383,28 +471,33 @@ pub(crate) fn try_reward_mixnode( } }; - let bond_reward_rate = read_mixnode_epoch_bond_reward_rate(deps.storage); - let delegation_reward_rate = read_mixnode_epoch_delegation_reward_rate(deps.storage); - let bond_scaled_reward_rate = scale_reward_by_uptime(bond_reward_rate, uptime)?; - let delegation_scaled_reward_rate = scale_reward_by_uptime(delegation_reward_rate, uptime)?; - let mut node_reward = Uint128(0); - let total_delegation_reward = increase_mix_delegated_stakes( - deps.storage, - &mix_identity, - delegation_scaled_reward_rate, - env.block.height, - )?; + let mut total_delegation_reward = Uint128(0); // update current bond with the reward given to the node and the delegators // if it has been bonded for long enough if current_bond.block_height + MINIMUM_BLOCK_AGE_FOR_REWARDING <= env.block.height { + let bond_reward_rate = state.mixnode_epoch_bond_reward; + let delegation_reward_rate = state.mixnode_epoch_delegation_reward; + let bond_scaled_reward_rate = scale_reward_by_uptime(bond_reward_rate, uptime)?; + let delegation_scaled_reward_rate = scale_reward_by_uptime(delegation_reward_rate, uptime)?; + + total_delegation_reward = increase_mix_delegated_stakes( + deps.storage, + &mix_identity, + delegation_scaled_reward_rate, + env.block.height, + )?; + node_reward = current_bond.bond_amount.amount * bond_scaled_reward_rate; current_bond.bond_amount.amount += node_reward; current_bond.total_delegation.amount += total_delegation_reward; mixnodes(deps.storage).save(mix_identity.as_bytes(), ¤t_bond)?; } + rewarded_mixnodes(deps.storage, rewarding_interval_nonce) + .save(mix_identity.as_bytes(), &Default::default())?; + Ok(Response { submessages: vec![], messages: vec![], @@ -416,6 +509,121 @@ pub(crate) fn try_reward_mixnode( }) } +pub(crate) fn try_reward_mixnode_v2( + deps: DepsMut, + env: Env, + info: MessageInfo, + mix_identity: IdentityKey, + params: NodeRewardParams, + rewarding_interval_nonce: u32, +) -> Result { + let state = config_read(deps.storage).load()?; + + // 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); + } + + if !state.rewarding_in_progress { + return Err(ContractError::RewardingNotInProgress); + } + + // make sure the transaction is sent for the correct rewarding interval + if rewarding_interval_nonce != state.latest_rewarding_interval_nonce { + return Err(ContractError::InvalidRewardingIntervalNonce { + received: rewarding_interval_nonce, + expected: state.latest_rewarding_interval_nonce, + }); + } + + // check if the mixnode hasn't been rewarded in this rewarding interval already + if rewarded_mixnodes_read(deps.storage, rewarding_interval_nonce) + .may_load(mix_identity.as_bytes())? + .is_some() + { + return Err(ContractError::MixnodeAlreadyRewarded { + identity: mix_identity, + }); + } + + // check if the bond even exists + let mut current_bond = match mixnodes_read(deps.storage).load(mix_identity.as_bytes()) { + Ok(bond) => bond, + Err(_) => { + return Ok(Response { + attributes: vec![attr("result", "bond not found")], + ..Default::default() + }); + } + }; + + let mut reward_params = params; + + reward_params.set_reward_blockstamp(env.block.height); + + let reward_result = current_bond.reward(&reward_params); + + // Omitting the price per packet function now, it follows that base operator reward is the node_reward + let operator_reward = current_bond.operator_reward(&reward_params); + + let total_delegation_reward = + increase_mix_delegated_stakes_v2(deps.storage, ¤t_bond, &reward_params)?; + + // update current bond with the reward given to the node and the delegators + // if it has been bonded for long enough + if current_bond.block_height + MINIMUM_BLOCK_AGE_FOR_REWARDING + <= reward_params.reward_blockstamp() + { + current_bond.bond_amount.amount += Uint128(operator_reward); + current_bond.total_delegation.amount += total_delegation_reward; + mixnodes(deps.storage).save(mix_identity.as_bytes(), ¤t_bond)?; + decr_reward_pool(Uint128(operator_reward), deps.storage)?; + decr_reward_pool(total_delegation_reward, deps.storage)?; + } + + rewarded_mixnodes(deps.storage, rewarding_interval_nonce) + .save(mix_identity.as_bytes(), &Default::default())?; + + Ok(Response { + submessages: vec![], + messages: vec![], + attributes: vec![ + attr("bond increase", reward_result.reward()), + attr("total delegation increase", total_delegation_reward), + ], + data: None, + }) +} +pub(crate) fn try_finish_mixnode_rewarding( + deps: DepsMut, + info: MessageInfo, + rewarding_interval_nonce: u32, +) -> Result { + let mut state = config_read(deps.storage).load()?; + + // 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); + } + + if !state.rewarding_in_progress { + return Err(ContractError::RewardingNotInProgress); + } + + // make sure the validator is in sync with the contract state + if rewarding_interval_nonce != state.latest_rewarding_interval_nonce { + return Err(ContractError::InvalidRewardingIntervalNonce { + received: rewarding_interval_nonce, + expected: state.latest_rewarding_interval_nonce, + }); + } + + state.rewarding_in_progress = false; + config(deps.storage).save(&state)?; + + Ok(Response::new()) +} + fn validate_delegation_stake(delegation: &[Coin]) -> Result<(), ContractError> { // check if anything was put as delegation if delegation.is_empty() { @@ -449,8 +657,7 @@ pub(crate) fn try_delegate_to_mixnode( validate_delegation_stake(&info.funds)?; // check if the target node actually exists - let mut mixnodes_bucket = mixnodes(deps.storage); - let mut current_bond = match mixnodes_bucket.load(mix_identity.as_bytes()) { + let mut current_bond = match mixnodes_read(deps.storage).load(mix_identity.as_bytes()) { Ok(bond) => bond, Err(_) => { return Err(ContractError::MixNodeBondNotFound { @@ -459,17 +666,19 @@ pub(crate) fn try_delegate_to_mixnode( } }; + let amount = info.funds[0].amount; + // update total_delegation of this node current_bond.total_delegation.amount += info.funds[0].amount; - mixnodes_bucket.save(mix_identity.as_bytes(), ¤t_bond)?; + mixnodes(deps.storage).save(mix_identity.as_bytes(), ¤t_bond)?; let mut delegation_bucket = mix_delegations(deps.storage, &mix_identity); let sender_bytes = info.sender.as_bytes(); // write the delegation let new_amount = match delegation_bucket.may_load(sender_bytes)? { - Some(existing_delegation) => existing_delegation.amount + info.funds[0].amount, - None => info.funds[0].amount, + Some(existing_delegation) => existing_delegation.amount + amount, + None => amount, }; // the block height is reset, if it existed let new_delegation = RawDelegationData::new(new_amount, env.block.height); @@ -532,8 +741,9 @@ pub(crate) fn try_remove_delegation_from_mixnode( pub mod tests { use super::*; use crate::contract::{ - execute, query, INITIAL_DEFAULT_EPOCH_LENGTH, INITIAL_GATEWAY_BOND, INITIAL_MIXNODE_BOND, - INITIAL_MIXNODE_BOND_REWARD_RATE, INITIAL_MIXNODE_DELEGATION_REWARD_RATE, + execute, query, DEFAULT_SYBIL_RESISTANCE_PERCENT, INITIAL_DEFAULT_EPOCH_LENGTH, + INITIAL_GATEWAY_BOND, INITIAL_MIXNODE_BOND, INITIAL_MIXNODE_BOND_REWARD_RATE, + INITIAL_MIXNODE_DELEGATION_REWARD_RATE, }; use crate::helpers::calculate_epoch_reward_rate; use crate::queries::DELEGATION_PAGE_DEFAULT_LIMIT; @@ -1397,7 +1607,8 @@ pub mod tests { mixnode_delegation_reward_rate: Decimal::percent( INITIAL_MIXNODE_DELEGATION_REWARD_RATE, ), - mixnode_active_set_size: 42, // change something + mixnode_rewarded_set_size: 100, + mixnode_active_set_size: 50, }; // cannot be updated from non-owner account @@ -1415,10 +1626,8 @@ pub mod tests { assert_eq!(current_state.params, new_params); // mixnode_epoch_rewards are recalculated if annual reward is changed - let current_mix_bond_reward_rate = - read_mixnode_epoch_bond_reward_rate(deps.as_ref().storage); - let current_mix_delegation_reward_rate = - read_mixnode_epoch_delegation_reward_rate(deps.as_ref().storage); + let current_mix_bond_reward_rate = current_state.mixnode_epoch_bond_reward; + let current_mix_delegation_reward_rate = current_state.mixnode_epoch_delegation_reward; let new_mixnode_bond_reward_rate = Decimal::percent(120); let new_mixnode_delegation_reward_rate = Decimal::percent(120); @@ -1471,27 +1680,359 @@ pub mod tests { expected_mixnode_delegation, new_state.mixnode_epoch_delegation_reward ); + + // 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_state_params(deps.as_mut(), info, new_params.clone()); + assert_eq!(Err(ContractError::InvalidActiveSetSize), res) + } + + #[cfg(test)] + mod beginning_mixnode_rewarding { + use super::*; + + #[test] + fn can_only_be_called_by_specified_validator_address() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + let res = try_begin_mixnode_rewarding( + deps.as_mut(), + env.clone(), + mock_info("not-the-approved-validator", &[]), + 1, + ); + assert_eq!(Err(ContractError::Unauthorized), res); + + let res = try_begin_mixnode_rewarding( + deps.as_mut(), + env.clone(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 1, + ); + assert!(res.is_ok()) + } + + #[test] + fn cannot_be_called_if_rewarding_is_already_in_progress_with_little_day() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + try_begin_mixnode_rewarding( + deps.as_mut(), + env.clone(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 1, + ) + .unwrap(); + + let res = try_begin_mixnode_rewarding( + deps.as_mut(), + env.clone(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 2, + ); + assert_eq!(Err(ContractError::RewardingInProgress), res); + } + + #[test] + fn can_be_called_if_rewarding_is_in_progress_if_sufficient_number_of_blocks_elapsed() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + try_begin_mixnode_rewarding( + deps.as_mut(), + env.clone(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 1, + ) + .unwrap(); + + let mut new_env = env.clone(); + + new_env.block.height = env.block.height + MAX_REWARDING_DURATION_IN_BLOCKS; + + let res = try_begin_mixnode_rewarding( + deps.as_mut(), + new_env, + mock_info(rewarding_validator_address.as_ref(), &[]), + 2, + ); + assert!(res.is_ok()); + } + + #[test] + fn provided_nonce_must_be_equal_the_current_plus_one() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let mut current_state = config_read(deps.as_mut().storage).load().unwrap(); + current_state.latest_rewarding_interval_nonce = 42; + config(deps.as_mut().storage).save(¤t_state).unwrap(); + + let rewarding_validator_address = current_state.rewarding_validator_address; + + let res = try_begin_mixnode_rewarding( + deps.as_mut(), + env.clone(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 11, + ); + assert_eq!( + Err(ContractError::InvalidRewardingIntervalNonce { + received: 11, + expected: 43 + }), + res + ); + + let res = try_begin_mixnode_rewarding( + deps.as_mut(), + env.clone(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 44, + ); + assert_eq!( + Err(ContractError::InvalidRewardingIntervalNonce { + received: 44, + expected: 43 + }), + res + ); + + let res = try_begin_mixnode_rewarding( + deps.as_mut(), + env.clone(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 42, + ); + assert_eq!( + Err(ContractError::InvalidRewardingIntervalNonce { + received: 42, + expected: 43 + }), + res + ); + + let res = try_begin_mixnode_rewarding( + deps.as_mut(), + env.clone(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 43, + ); + assert!(res.is_ok()) + } + + #[test] + fn updates_contract_state() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let start_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = start_state.rewarding_validator_address; + + try_begin_mixnode_rewarding( + deps.as_mut(), + env.clone(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 1, + ) + .unwrap(); + + let new_state = config_read(deps.as_mut().storage).load().unwrap(); + assert!(new_state.rewarding_in_progress); + assert_eq!( + new_state.rewarding_interval_starting_block, + env.block.height + ); + assert_eq!( + start_state.latest_rewarding_interval_nonce + 1, + new_state.latest_rewarding_interval_nonce + ); + } + } + + #[cfg(test)] + mod finishing_mixnode_rewarding { + use super::*; + + #[test] + fn can_only_be_called_by_specified_validator_address() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + try_begin_mixnode_rewarding( + deps.as_mut(), + env, + mock_info(rewarding_validator_address.as_ref(), &[]), + 1, + ) + .unwrap(); + + let res = try_finish_mixnode_rewarding( + deps.as_mut(), + mock_info("not-the-approved-validator", &[]), + 1, + ); + assert_eq!(Err(ContractError::Unauthorized), res); + + let res = try_finish_mixnode_rewarding( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 1, + ); + assert!(res.is_ok()) + } + + #[test] + fn cannot_be_called_if_rewarding_is_not_in_progress() { + let mut deps = helpers::init_contract(); + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + let res = try_finish_mixnode_rewarding( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 0, + ); + assert_eq!(Err(ContractError::RewardingNotInProgress), res); + } + + #[test] + fn provided_nonce_must_be_equal_the_current_one() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let mut current_state = config_read(deps.as_mut().storage).load().unwrap(); + current_state.latest_rewarding_interval_nonce = 42; + config(deps.as_mut().storage).save(¤t_state).unwrap(); + + let rewarding_validator_address = current_state.rewarding_validator_address; + + try_begin_mixnode_rewarding( + deps.as_mut(), + env, + mock_info(rewarding_validator_address.as_ref(), &[]), + 43, + ) + .unwrap(); + + let res = try_finish_mixnode_rewarding( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 11, + ); + assert_eq!( + Err(ContractError::InvalidRewardingIntervalNonce { + received: 11, + expected: 43 + }), + res + ); + + let res = try_finish_mixnode_rewarding( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 44, + ); + assert_eq!( + Err(ContractError::InvalidRewardingIntervalNonce { + received: 44, + expected: 43 + }), + res + ); + + let res = try_finish_mixnode_rewarding( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 42, + ); + assert_eq!( + Err(ContractError::InvalidRewardingIntervalNonce { + received: 42, + expected: 43 + }), + res + ); + + let res = try_finish_mixnode_rewarding( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 43, + ); + assert!(res.is_ok()) + } + + #[test] + fn updates_contract_state() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + try_begin_mixnode_rewarding( + deps.as_mut(), + env, + mock_info(rewarding_validator_address.as_ref(), &[]), + 1, + ) + .unwrap(); + + try_finish_mixnode_rewarding( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 1, + ) + .unwrap(); + + let new_state = config_read(deps.as_mut().storage).load().unwrap(); + assert!(!new_state.rewarding_in_progress); + } } #[test] fn rewarding_mixnode() { let mut deps = helpers::init_contract(); let mut env = mock_env(); - let current_state = config(deps.as_mut().storage).load().unwrap(); - let network_monitor_address = current_state.network_monitor_address; + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; let node_owner: Addr = Addr::unchecked("node-owner"); let node_identity: IdentityKey = "nodeidentity".into(); // errors out if executed by somebody else than network monitor let info = mock_info("not-the-monitor", &[]); - let res = try_reward_mixnode(deps.as_mut(), env.clone(), info, node_identity.clone(), 100); + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info, + node_identity.clone(), + 100, + 1, + ); assert_eq!(res, Err(ContractError::Unauthorized)); + // begin rewarding period + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap(); // returns bond not found attribute if the target owner hasn't bonded any mixnodes - let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = try_reward_mixnode(deps.as_mut(), env.clone(), info, node_identity.clone(), 100) - .unwrap(); + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info, + node_identity.clone(), + 100, + 1, + ) + .unwrap(); assert_eq!(vec![attr("result", "bond not found")], res.attributes); let initial_bond = 100_000000; @@ -1506,6 +2047,7 @@ pub mod tests { identity_key: node_identity.clone(), ..mix_node_fixture() }, + profit_margin_percent: Some(10), }; mixnodes(deps.as_mut().storage) @@ -1521,9 +2063,8 @@ pub mod tests { env.block.height += 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING; - let bond_reward_rate = read_mixnode_epoch_bond_reward_rate(deps.as_ref().storage); - let delegation_reward_rate = - read_mixnode_epoch_delegation_reward_rate(deps.as_ref().storage); + let bond_reward_rate = current_state.mixnode_epoch_bond_reward; + let delegation_reward_rate = current_state.mixnode_epoch_delegation_reward; let expected_bond_reward = Uint128(initial_bond) * bond_reward_rate; let expected_delegation_reward = Uint128(initial_delegation) * delegation_reward_rate; @@ -1532,9 +2073,17 @@ pub mod tests { let expected_bond = expected_bond_reward + Uint128(initial_bond); let expected_delegation = expected_delegation_reward + Uint128(initial_delegation); - let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = try_reward_mixnode(deps.as_mut(), env.clone(), info, node_identity.clone(), 100) - .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(), + 100, + 1, + ) + .unwrap(); + try_finish_mixnode_rewarding(deps.as_mut(), info, 1).unwrap(); assert_eq!( expected_bond, @@ -1561,9 +2110,18 @@ pub mod tests { let expected_bond = expected_bond_reward + expected_bond; let expected_delegation = expected_delegation_reward + expected_delegation; - let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = try_reward_mixnode(deps.as_mut(), env.clone(), info, node_identity.clone(), 20) - .unwrap(); + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 2).unwrap(); + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info, + node_identity.clone(), + 20, + 2, + ) + .unwrap(); assert_eq!( expected_bond, @@ -1583,12 +2141,185 @@ pub mod tests { ); } + #[test] + fn rewarding_mixnodes_outside_rewarding_period() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + // bond the node + let node_owner: Addr = Addr::unchecked("node-owner"); + let node_identity: IdentityKey = "nodeidentity".into(); + + try_add_mixnode( + deps.as_mut(), + env.clone(), + mock_info(node_owner.as_ref(), &good_mixnode_bond()), + MixNode { + identity_key: node_identity.to_string(), + ..helpers::mix_node_fixture() + }, + ) + .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(), + 100, + 1, + ); + assert_eq!(Err(ContractError::RewardingNotInProgress), res); + + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap(); + + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info, + node_identity.clone(), + 100, + 1, + ); + assert!(res.is_ok()) + } + + #[test] + fn rewarding_mixnodes_with_incorrect_rewarding_nonce() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + // bond the node + let node_owner: Addr = Addr::unchecked("node-owner"); + let node_identity: IdentityKey = "nodeidentity".into(); + + try_add_mixnode( + deps.as_mut(), + env.clone(), + mock_info(node_owner.as_ref(), &good_mixnode_bond()), + MixNode { + identity_key: node_identity.to_string(), + ..helpers::mix_node_fixture() + }, + ) + .unwrap(); + + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap(); + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + 100, + 0, + ); + assert_eq!( + Err(ContractError::InvalidRewardingIntervalNonce { + received: 0, + expected: 1 + }), + res + ); + + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + 100, + 2, + ); + assert_eq!( + Err(ContractError::InvalidRewardingIntervalNonce { + received: 2, + expected: 1 + }), + res + ); + + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info, + node_identity.clone(), + 100, + 1, + ); + assert!(res.is_ok()) + } + + #[test] + fn attempting_rewarding_mixnode_multiple_times_per_interval() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + // bond the node + let node_owner: Addr = Addr::unchecked("node-owner"); + let node_identity: IdentityKey = "nodeidentity".into(); + + try_add_mixnode( + deps.as_mut(), + env.clone(), + mock_info(node_owner.as_ref(), &good_mixnode_bond()), + MixNode { + identity_key: node_identity.to_string(), + ..helpers::mix_node_fixture() + }, + ) + .unwrap(); + + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap(); + + // first reward goes through just fine + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + 100, + 1, + ); + assert!(res.is_ok()); + + // but the other one fails + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + 100, + 1, + ); + assert_eq!( + Err(ContractError::MixnodeAlreadyRewarded { + identity: node_identity.clone() + }), + res + ); + + // but rewarding the same node in the following interval is fine again + try_finish_mixnode_rewarding(deps.as_mut(), info.clone(), 1).unwrap(); + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 2).unwrap(); + + let res = try_reward_mixnode(deps.as_mut(), env, info, node_identity.clone(), 100, 2); + assert!(res.is_ok()); + } + #[test] fn rewarding_mixnode_blockstamp_based() { let mut deps = helpers::init_contract(); let mut env = mock_env(); - let current_state = config(deps.as_mut().storage).load().unwrap(); - let network_monitor_address = current_state.network_monitor_address; + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; let node_owner: Addr = Addr::unchecked("node-owner"); let node_identity: IdentityKey = "nodeidentity".into(); @@ -1605,6 +2336,7 @@ pub mod tests { identity_key: node_identity.clone(), ..mix_node_fixture() }, + profit_margin_percent: Some(10), }; mixnodes(deps.as_mut().storage) @@ -1621,9 +2353,8 @@ pub mod tests { ) .unwrap(); - let bond_reward_rate = read_mixnode_epoch_bond_reward_rate(deps.as_ref().storage); - let delegation_reward_rate = - read_mixnode_epoch_delegation_reward_rate(deps.as_ref().storage); + let bond_reward_rate = current_state.mixnode_epoch_bond_reward; + let delegation_reward_rate = current_state.mixnode_epoch_delegation_reward; let scaled_bond_reward = scale_reward_by_uptime(bond_reward_rate, 100).unwrap(); let scaled_delegation_reward = scale_reward_by_uptime(delegation_reward_rate, 100).unwrap(); @@ -1633,9 +2364,18 @@ pub mod tests { let expected_bond = expected_bond_reward + Uint128(initial_bond); let expected_delegation = expected_delegation_reward + Uint128(initial_delegation); - let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = try_reward_mixnode(deps.as_mut(), env.clone(), info, node_identity.clone(), 100) - .unwrap(); + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap(); + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + 100, + 1, + ) + .unwrap(); + try_finish_mixnode_rewarding(deps.as_mut(), info, 1).unwrap(); assert_eq!( expected_bond, @@ -1661,9 +2401,18 @@ pub mod tests { let expected_bond = expected_bond_reward + expected_bond; let expected_delegation = expected_delegation_reward + expected_delegation; - let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = try_reward_mixnode(deps.as_mut(), env.clone(), info, node_identity.clone(), 100) - .unwrap(); + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 2).unwrap(); + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + 100, + 2, + ) + .unwrap(); + try_finish_mixnode_rewarding(deps.as_mut(), info, 2).unwrap(); assert_eq!( expected_bond, @@ -1689,9 +2438,18 @@ pub mod tests { let expected_bond = expected_bond_reward + expected_bond; let expected_delegation = expected_delegation_reward + expected_delegation; - let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = try_reward_mixnode(deps.as_mut(), env.clone(), info, node_identity.clone(), 100) - .unwrap(); + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 3).unwrap(); + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + 100, + 3, + ) + .unwrap(); + try_finish_mixnode_rewarding(deps.as_mut(), info, 3).unwrap(); assert_eq!( expected_bond, @@ -2364,8 +3122,8 @@ pub mod tests { fn delegators_on_mix_node_reward_rate() { let mut deps = helpers::init_contract(); let mut env = mock_env(); - let current_state = config(deps.as_mut().storage).load().unwrap(); - let network_monitor_address = current_state.network_monitor_address; + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; let initial_mix_bond = 100_000000; let initial_delegation1 = 50000; // will see single digits rewards @@ -2396,8 +3154,8 @@ pub mod tests { env.block.height += 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING; - let bond_reward = read_mixnode_epoch_bond_reward_rate(deps.as_ref().storage); - let delegation_reward = read_mixnode_epoch_delegation_reward_rate(deps.as_ref().storage); + let bond_reward = current_state.mixnode_epoch_bond_reward; + let delegation_reward = current_state.mixnode_epoch_delegation_reward; // the node's bond and delegations are correctly increased and scaled by uptime // if node was 100% up, it will get full epoch reward @@ -2411,9 +3169,18 @@ pub mod tests { let expected_delegation2 = expected_delegation2_reward + Uint128(initial_delegation2); let expected_delegation3 = expected_delegation3_reward + Uint128(initial_delegation3); - let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = - try_reward_mixnode(deps.as_mut(), env.clone(), info, identity.clone(), 100).unwrap(); + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap(); + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + identity.clone(), + 100, + 1, + ) + .unwrap(); + try_finish_mixnode_rewarding(deps.as_mut(), info, 1).unwrap(); assert_eq!( expected_bond, @@ -2471,9 +3238,18 @@ pub mod tests { let expected_delegation2 = expected_delegation2_reward + expected_delegation2; let expected_delegation3 = expected_delegation3_reward + expected_delegation3; - let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = - try_reward_mixnode(deps.as_mut(), env.clone(), info, identity.clone(), 20).unwrap(); + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 2).unwrap(); + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + identity.clone(), + 20, + 2, + ) + .unwrap(); + try_finish_mixnode_rewarding(deps.as_mut(), info, 2).unwrap(); assert_eq!( expected_bond, @@ -2518,9 +3294,18 @@ pub mod tests { ); // if the node was 0% up, nobody will get any rewards - let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = - try_reward_mixnode(deps.as_mut(), env.clone(), info, identity.clone(), 0).unwrap(); + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 3).unwrap(); + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + identity.clone(), + 0, + 3, + ) + .unwrap(); + try_finish_mixnode_rewarding(deps.as_mut(), info, 3).unwrap(); assert_eq!( expected_bond, @@ -2654,4 +3439,135 @@ pub mod tests { assert_eq!(bob_node.mix_node.identity_key, "bob"); assert_eq!(bob_node.layer, Layer::Two); } + + #[test] + fn test_tokenomics_rewarding() { + use crate::contract::{EPOCH_REWARD_PERCENT, INITIAL_REWARD_POOL}; + + type U128 = fixed::types::U75F53; + + let mut deps = helpers::init_contract(); + let mut env = mock_env(); + let current_state = config(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + let period_reward_pool = (INITIAL_REWARD_POOL / 100) * EPOCH_REWARD_PERCENT as u128; + assert_eq!(period_reward_pool, 5_000_000_000_000); + let k = 200; // Imagining our active set size is 200 + let circulating_supply = circulating_supply(&deps.storage).u128(); + assert_eq!(circulating_supply, 750_000_000_000_000u128); + // mut_reward_pool(deps.as_mut().storage) + // .save(&Uint128(period_reward_pool)) + // .unwrap(); + + try_add_mixnode( + deps.as_mut(), + mock_env(), + mock_info( + "alice", + &vec![Coin { + denom: DENOM.to_string(), + amount: Uint128(10000_000_000), + }], + ), + MixNode { + identity_key: "alice".to_string(), + ..helpers::mix_node_fixture() + }, + ) + .unwrap(); + + try_delegate_to_mixnode( + deps.as_mut(), + mock_env(), + mock_info("d1", &vec![coin(8000_000000, DENOM)]), + "alice".to_string(), + ) + .unwrap(); + + try_delegate_to_mixnode( + deps.as_mut(), + mock_env(), + mock_info("d2", &vec![coin(2000_000000, DENOM)]), + "alice".to_string(), + ) + .unwrap(); + + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + try_begin_mixnode_rewarding( + deps.as_mut(), + env.clone(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 1, + ) + .unwrap(); + + env.block.height += 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING; + + let mix_1 = mixnodes_read(&deps.storage).load(b"alice").unwrap(); + let mix_1_uptime = 100; + + let mut params = NodeRewardParams::new( + period_reward_pool, + k, + 0, + circulating_supply, + mix_1_uptime, + DEFAULT_SYBIL_RESISTANCE_PERCENT, + ); + + params.set_reward_blockstamp(env.block.height); + + assert_eq!(params.performance(), 1); + + let mix_1_reward_result = mix_1.reward(¶ms); + + assert_eq!( + mix_1_reward_result.sigma(), + U128::from_num(0.0000266666666666) + ); + assert_eq!( + mix_1_reward_result.lambda(), + U128::from_num(0.0000133333333333) + ); + assert_eq!(mix_1_reward_result.reward().int(), 102646153); + + let mix1_operator_profit = mix_1.operator_reward(¶ms); + + let mix1_delegator1_reward = mix_1.reward_delegation(Uint128(8000_000000), ¶ms); + + let mix1_delegator2_reward = mix_1.reward_delegation(Uint128(2000_000000), ¶ms); + + assert_eq!(mix1_operator_profit, U128::from_num(74455384)); + assert_eq!(mix1_delegator1_reward, U128::from_num(22552615)); + assert_eq!(mix1_delegator2_reward, U128::from_num(5638153)); + + let pre_reward_bond = read_mixnode_bond(&deps.storage, b"alice").unwrap().u128(); + assert_eq!(pre_reward_bond, 10000_000_000); + + let pre_reward_delegation = read_mixnode_delegation(&deps.storage, b"alice") + .unwrap() + .u128(); + assert_eq!(pre_reward_delegation, 10000_000_000); + + try_reward_mixnode_v2(deps.as_mut(), env, info, "alice".to_string(), params, 1).unwrap(); + + assert_eq!( + read_mixnode_bond(&deps.storage, b"alice").unwrap().u128(), + U128::from_num(pre_reward_bond) + U128::from_num(mix1_operator_profit) + ); + assert_eq!( + read_mixnode_delegation(&deps.storage, b"alice") + .unwrap() + .u128(), + pre_reward_delegation + mix1_delegator1_reward + mix1_delegator2_reward + ); + + assert_eq!( + reward_pool_value(&deps.storage).u128(), + U128::from_num(INITIAL_REWARD_POOL) + - (U128::from_num(mix1_operator_profit) + + U128::from_num(mix1_delegator1_reward) + + U128::from_num(mix1_delegator2_reward)) + ) + } } diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 272ee47925..0e9b875b2e 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -11,6 +11,7 @@ rust-version = "1.56" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +bip39 = "1.0.1" clap = "2.33.0" dirs = "3.0" dashmap = "4.0" @@ -28,22 +29,26 @@ tokio-stream = { version = "0.1", features = [ "fs" ] } tokio-tungstenite = "0.14" url = { version = "2.2", features = [ "serde" ] } sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } +web3 = "0.17.0" # internal coconut-interface = { path = "../common/coconut-interface" , optional = true} -credentials = { path = "../common/credentials" , optional = true} +credentials = { path = "../common/credentials" } config = { path = "../common/config" } crypto = { path = "../common/crypto" } +erc20-bridge-contract = { path = "../common/erc20-bridge-contract" } gateway-requests = { path = "gateway-requests" } +gateway-client = { path = "../common/client-libs/gateway-client" } mixnet-client = { path = "../common/client-libs/mixnet-client" } mixnode-common = { path = "../common/mixnode-common" } +network-defaults = { path = "../common/network-defaults" } nymsphinx = { path = "../common/nymsphinx" } pemstore = { path = "../common/pemstore" } -validator-client = { path = "../common/client-libs/validator-client" } +validator-client = { path = "../common/client-libs/validator-client", features = ["nymd-client"] } version-checker = { path = "../common/version-checker" } [features] -coconut = ["coconut-interface", "credentials", "gateway-requests/coconut"] +coconut = ["coconut-interface", "gateway-requests/coconut", "gateway-client/coconut"] [build-dependencies] tokio = { version = "1.4", features = ["rt-multi-thread", "macros"] } diff --git a/gateway/gateway-requests/Cargo.toml b/gateway/gateway-requests/Cargo.toml index 99a8b6cd15..de5bf95356 100644 --- a/gateway/gateway-requests/Cargo.toml +++ b/gateway/gateway-requests/Cargo.toml @@ -24,10 +24,10 @@ crypto = { path = "../../common/crypto" } pemstore = { path = "../../common/pemstore" } coconut-interface = { path = "../../common/coconut-interface", optional = true } -credentials = { path = "../../common/credentials", optional = true } +credentials = { path = "../../common/credentials" } [features] -coconut = ["coconut-interface", "credentials"] +coconut = ["coconut-interface"] [dependencies.tungstenite] version = "0.13.0" diff --git a/gateway/gateway-requests/src/registration/handshake/gateway.rs b/gateway/gateway-requests/src/registration/handshake/gateway.rs index 8663f9af5f..ace136d290 100644 --- a/gateway/gateway-requests/src/registration/handshake/gateway.rs +++ b/gateway/gateway-requests/src/registration/handshake/gateway.rs @@ -49,14 +49,11 @@ impl<'a> GatewayHandshake<'a> { } // init: <- pub_key || g^x - let init_message = check_processing_error( + let (remote_identity, remote_ephemeral_key) = check_processing_error( State::::parse_init_message(received_init_payload), &mut state, ) .await?; - - let remote_identity = init_message.local_id_pubkey(); - let remote_ephemeral_key = init_message.ephemeral_key(); state.update_remote_identity(remote_identity); // hkdf::::(g^xy) diff --git a/gateway/gateway-requests/src/registration/handshake/state.rs b/gateway/gateway-requests/src/registration/handshake/state.rs index 9b45b89292..10c0ae9729 100644 --- a/gateway/gateway-requests/src/registration/handshake/state.rs +++ b/gateway/gateway-requests/src/registration/handshake/state.rs @@ -15,46 +15,9 @@ use futures::{Sink, SinkExt, Stream, StreamExt}; use log::*; use nymsphinx::params::{GatewayEncryptionAlgorithm, GatewaySharedKeyHkdfAlgorithm}; use rand::{CryptoRng, RngCore}; -use serde::{Deserialize, Serialize}; use std::convert::{TryFrom, TryInto}; use tungstenite::Message as WsMessage; -#[derive(Serialize, Deserialize)] -pub struct InitMessage { - local_id_pubkey: [u8; identity::PUBLIC_KEY_LENGTH], - ephemeral_key: [u8; identity::PUBLIC_KEY_LENGTH], -} - -impl InitMessage { - fn new(local_id_pubkey: &identity::PublicKey, ephemeral_key: &encryption::PublicKey) -> Self { - InitMessage { - local_id_pubkey: local_id_pubkey.to_bytes(), - ephemeral_key: ephemeral_key.to_bytes(), - } - } - - #[cfg(not(target_arch = "wasm32"))] - pub fn local_id_pubkey(&self) -> identity::PublicKey { - identity::PublicKey::from_bytes(&self.local_id_pubkey).unwrap() - } - - #[cfg(not(target_arch = "wasm32"))] - pub fn ephemeral_key(&self) -> encryption::PublicKey { - encryption::PublicKey::from_bytes(&self.ephemeral_key).unwrap() - } - - fn to_bytes(&self) -> Vec { - bincode::serialize(self).unwrap() - } -} - -impl TryFrom<&[u8]> for InitMessage { - type Error = HandshakeError; - - fn try_from(value: &[u8]) -> Result { - bincode::deserialize(value).map_err(|e| e.into()) - } -} /// Handshake state. pub(crate) struct State<'a, S> { /// The underlying WebSocket stream. @@ -101,17 +64,41 @@ impl<'a, S> State<'a, S> { // Eventually the ID_PUBKEY prefix will get removed and recipient will know // initializer's identity from another source. pub(crate) fn init_message(&self) -> Vec { - InitMessage::new( - self.identity.public_key(), - self.ephemeral_keypair.public_key(), - ) - .to_bytes() + self.identity + .public_key() + .to_bytes() + .iter() + .cloned() + .chain( + self.ephemeral_keypair + .public_key() + .to_bytes() + .iter() + .cloned(), + ) + .collect() } // this will need to be adjusted when REMOTE_ID_PUBKEY is removed #[cfg(not(target_arch = "wasm32"))] - pub(crate) fn parse_init_message(init_message: Vec) -> Result { - InitMessage::try_from(init_message.as_slice()).map_err(|_| HandshakeError::MalformedRequest) + pub(crate) fn parse_init_message( + mut init_message: Vec, + ) -> Result<(identity::PublicKey, encryption::PublicKey), HandshakeError> { + if init_message.len() != identity::PUBLIC_KEY_LENGTH + encryption::PUBLIC_KEY_SIZE { + return Err(HandshakeError::MalformedRequest); + } + + let remote_ephemeral_key_bytes = init_message.split_off(identity::PUBLIC_KEY_LENGTH); + // this can only fail if the provided bytes have len different from encryption::PUBLIC_KEY_SIZE + // which is impossible + let remote_ephemeral_key = + encryption::PublicKey::from_bytes(&remote_ephemeral_key_bytes).unwrap(); + + // this could actually fail if the curve point fails to get decompressed + let remote_identity = identity::PublicKey::from_bytes(&init_message) + .map_err(|_| HandshakeError::MalformedRequest)?; + + Ok((remote_identity, remote_ephemeral_key)) } pub(crate) fn derive_shared_key(&mut self, remote_ephemeral_key: &encryption::PublicKey) { diff --git a/gateway/gateway-requests/src/types.rs b/gateway/gateway-requests/src/types.rs index 015053445e..249a4d066e 100644 --- a/gateway/gateway-requests/src/types.rs +++ b/gateway/gateway-requests/src/types.rs @@ -22,6 +22,8 @@ use tungstenite::protocol::Message; #[cfg(feature = "coconut")] use coconut_interface::Credential; +#[cfg(not(feature = "coconut"))] +use credentials::token::bandwidth::TokenCredential; #[derive(Serialize, Deserialize, Debug)] #[serde(tag = "type", rename_all = "camelCase")] @@ -117,8 +119,7 @@ pub enum ClientControlRequest { }, #[serde(alias = "handshakePayload")] RegisterHandshakeInitRequest { data: Vec }, - #[cfg(feature = "coconut")] - CoconutBandwidthCredential { + BandwidthCredential { enc_credential: Vec, iv: Vec, }, @@ -148,7 +149,7 @@ impl ClientControlRequest { let enc_credential = shared_key.encrypt_and_tag(&serialized_credential, Some(iv.inner())); - Some(ClientControlRequest::CoconutBandwidthCredential { + Some(ClientControlRequest::BandwidthCredential { enc_credential, iv: iv.to_bytes(), }) @@ -166,6 +167,30 @@ impl ClientControlRequest { let credential = shared_key.decrypt_tagged(&enc_credential, Some(iv.inner()))?; bincode::deserialize(&credential).map_err(|_| GatewayRequestsError::MalformedEncryption) } + + #[cfg(not(feature = "coconut"))] + pub fn new_enc_token_bandwidth_credential( + credential: &TokenCredential, + shared_key: &SharedKeys, + iv: IV, + ) -> Self { + let enc_credential = shared_key.encrypt_and_tag(&credential.to_bytes(), Some(iv.inner())); + ClientControlRequest::BandwidthCredential { + enc_credential, + iv: iv.to_bytes(), + } + } + + #[cfg(not(feature = "coconut"))] + pub fn try_from_enc_token_bandwidth_credential( + enc_credential: Vec, + shared_key: &SharedKeys, + iv: IV, + ) -> Result { + let credential = shared_key.decrypt_tagged(&enc_credential, Some(iv.inner()))?; + TokenCredential::from_bytes(&credential) + .map_err(|_| GatewayRequestsError::MalformedEncryption) + } } impl From for Message { @@ -196,11 +221,22 @@ impl TryInto for ClientControlRequest { #[derive(Serialize, Deserialize, Debug)] #[serde(tag = "type", rename_all = "camelCase")] pub enum ServerResponse { - Authenticate { status: bool }, - Register { status: bool }, - Bandwidth { available_total: i64 }, - Send { remaining_bandwidth: i64 }, - Error { message: String }, + Authenticate { + status: bool, + bandwidth_remaining: i64, + }, + Register { + status: bool, + }, + Bandwidth { + available_total: i64, + }, + Send { + remaining_bandwidth: i64, + }, + Error { + message: String, + }, } impl ServerResponse { diff --git a/gateway/src/commands/init.rs b/gateway/src/commands/init.rs index 0cd5a84076..a2bed3186d 100644 --- a/gateway/src/commands/init.rs +++ b/gateway/src/commands/init.rs @@ -9,7 +9,7 @@ use config::NymConfig; use crypto::asymmetric::{encryption, identity}; pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { - App::new("init") + let app = App::new("init") .about("Initialise the gateway") .arg( Arg::with_name(ID_ARG_NAME) @@ -50,11 +50,30 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { .takes_value(true) ) .arg( - Arg::with_name(VALIDATORS_ARG_NAME) - .long(VALIDATORS_ARG_NAME) - .help("Comma separated list of rest endpoints of the validators") + Arg::with_name(VALIDATOR_APIS_ARG_NAME) + .long(VALIDATOR_APIS_ARG_NAME) + .help("Comma separated list of endpoints of the validators APIs") .takes_value(true), - ) + ); + + #[cfg(not(feature = "coconut"))] + let app = app + .arg(Arg::with_name(ETH_ENDPOINT) + .long(ETH_ENDPOINT) + .help("URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20 tokens") + .takes_value(true) + .required(true)) + .arg(Arg::with_name(VALIDATORS_ARG_NAME) + .long(VALIDATORS_ARG_NAME) + .help("Comma separated list of endpoints of the validator") + .takes_value(true)) + .arg(Arg::with_name(COSMOS_MNEMONIC) + .long(COSMOS_MNEMONIC) + .help("Cosmos wallet mnemonic") + .takes_value(true) + .required(true)); + + app } fn show_bonding_info(config: &Config) { diff --git a/gateway/src/commands/mod.rs b/gateway/src/commands/mod.rs index 574602f2c5..3838329caa 100644 --- a/gateway/src/commands/mod.rs +++ b/gateway/src/commands/mod.rs @@ -13,7 +13,13 @@ pub(crate) const ID_ARG_NAME: &str = "id"; pub(crate) const HOST_ARG_NAME: &str = "host"; pub(crate) const MIX_PORT_ARG_NAME: &str = "mix-port"; pub(crate) const CLIENTS_PORT_ARG_NAME: &str = "clients-port"; +pub(crate) const VALIDATOR_APIS_ARG_NAME: &str = "validator-apis"; +#[cfg(not(feature = "coconut"))] pub(crate) const VALIDATORS_ARG_NAME: &str = "validators"; +#[cfg(not(feature = "coconut"))] +pub(crate) const COSMOS_MNEMONIC: &str = "mnemonic"; +#[cfg(not(feature = "coconut"))] +pub(crate) const ETH_ENDPOINT: &str = "eth_endpoint"; pub(crate) const ANNOUNCE_HOST_ARG_NAME: &str = "announce-host"; pub(crate) const DATASTORE_PATH: &str = "datastore"; @@ -64,13 +70,28 @@ pub(crate) fn override_config(mut config: Config, matches: &ArgMatches) -> Confi config = config.announce_host_from_listening_host() } - if let Some(raw_validators) = matches.value_of(VALIDATORS_ARG_NAME) { + if let Some(raw_validators) = matches.value_of(VALIDATOR_APIS_ARG_NAME) { config = config.with_custom_validator_apis(parse_validators(raw_validators)); } + #[cfg(not(feature = "coconut"))] + if let Some(raw_validators) = matches.value_of(VALIDATORS_ARG_NAME) { + config = config.with_custom_validator_nymd(parse_validators(raw_validators)); + } + + #[cfg(not(feature = "coconut"))] + if let Some(cosmos_mnemonic) = matches.value_of(COSMOS_MNEMONIC) { + config = config.with_cosmos_mnemonic(String::from(cosmos_mnemonic)); + } + if let Some(datastore_path) = matches.value_of(DATASTORE_PATH) { config = config.with_custom_persistent_store(datastore_path); } + #[cfg(not(feature = "coconut"))] + if let Some(eth_endpoint) = matches.value_of(ETH_ENDPOINT) { + config = config.with_eth_endpoint(String::from(eth_endpoint)); + } + config } diff --git a/gateway/src/commands/run.rs b/gateway/src/commands/run.rs index 83519820c0..2e881f9d09 100644 --- a/gateway/src/commands/run.rs +++ b/gateway/src/commands/run.rs @@ -12,7 +12,7 @@ use log::*; use version_checker::is_minor_version_compatible; pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { - App::new("run") + let app = App::new("run") .about("Starts the gateway") .arg( Arg::with_name(ID_ARG_NAME) @@ -53,11 +53,28 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { .takes_value(true) ) .arg( - Arg::with_name(VALIDATORS_ARG_NAME) - .long(VALIDATORS_ARG_NAME) - .help("Comma separated list of rest endpoints of the validators") + Arg::with_name(VALIDATOR_APIS_ARG_NAME) + .long(VALIDATOR_APIS_ARG_NAME) + .help("Comma separated list of endpoints of the validators APIs") .takes_value(true), - ) + ); + + #[cfg(not(feature = "coconut"))] + let app = app + .arg(Arg::with_name(ETH_ENDPOINT) + .long(ETH_ENDPOINT) + .help("URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20 tokens") + .takes_value(true)) + .arg(Arg::with_name(VALIDATORS_ARG_NAME) + .long(VALIDATORS_ARG_NAME) + .help("Comma separated list of endpoints of the validator") + .takes_value(true)) + .arg(Arg::with_name(COSMOS_MNEMONIC) + .long(COSMOS_MNEMONIC) + .help("Cosmos wallet mnemonic") + .takes_value(true)); + + app } fn show_binding_warning(address: String) { @@ -149,7 +166,7 @@ pub async fn execute(matches: ArgMatches<'static>) { } println!( - "Validator servers: {:?}", + "Validator API servers: {:?}", config.get_validator_api_endpoints() ); diff --git a/gateway/src/config/mod.rs b/gateway/src/config/mod.rs index dd952ce882..fdbcaaac14 100644 --- a/gateway/src/config/mod.rs +++ b/gateway/src/config/mod.rs @@ -127,6 +127,24 @@ impl Config { self } + #[cfg(not(feature = "coconut"))] + pub fn with_custom_validator_nymd(mut self, validator_nymd_urls: Vec) -> Self { + self.gateway.validator_nymd_urls = validator_nymd_urls; + self + } + + #[cfg(not(feature = "coconut"))] + pub fn with_cosmos_mnemonic(mut self, cosmos_mnemonic: String) -> Self { + self.gateway.cosmos_mnemonic = cosmos_mnemonic; + self + } + + #[cfg(not(feature = "coconut"))] + pub fn with_eth_endpoint(mut self, eth_endpoint: String) -> Self { + self.gateway.eth_endpoint = eth_endpoint; + self + } + pub fn with_listening_address>(mut self, listening_address: S) -> Self { let listening_address_string = listening_address.into(); if let Ok(ip_addr) = listening_address_string.parse() { @@ -191,10 +209,25 @@ impl Config { self.gateway.public_sphinx_key_file.clone() } + #[cfg(not(feature = "coconut"))] + pub fn get_eth_endpoint(&self) -> String { + self.gateway.eth_endpoint.clone() + } + pub fn get_validator_api_endpoints(&self) -> Vec { self.gateway.validator_api_urls.clone() } + #[cfg(not(feature = "coconut"))] + pub fn get_validator_nymd_endpoints(&self) -> Vec { + self.gateway.validator_nymd_urls.clone() + } + + #[cfg(not(feature = "coconut"))] + pub fn get_cosmos_mnemonic(&self) -> String { + self.gateway.cosmos_mnemonic.clone() + } + pub fn get_listening_address(&self) -> IpAddr { self.gateway.listening_address } @@ -281,9 +314,21 @@ pub struct Gateway { /// Path to file containing public sphinx key. public_sphinx_key_file: PathBuf, + /// Address to an Ethereum full node. + #[cfg(not(feature = "coconut"))] + eth_endpoint: String, + /// Addresses to APIs running on validator from which the node gets the view of the network. validator_api_urls: Vec, + /// Addresses to validators which the node uses to check for double spending of ERC20 tokens. + #[cfg(not(feature = "coconut"))] + validator_nymd_urls: Vec, + + /// Mnemonic of a cosmos wallet used for checking for double spending. + #[cfg(not(feature = "coconut"))] + cosmos_mnemonic: String, + /// nym_home_directory specifies absolute path to the home nym gateways directory. /// It is expected to use default value and hence .toml file should not redefine this field. nym_root_directory: PathBuf, @@ -328,7 +373,13 @@ impl Default for Gateway { public_identity_key_file: Default::default(), private_sphinx_key_file: Default::default(), public_sphinx_key_file: Default::default(), + #[cfg(not(feature = "coconut"))] + eth_endpoint: "".to_string(), validator_api_urls: default_api_endpoints(), + #[cfg(not(feature = "coconut"))] + validator_nymd_urls: default_nymd_endpoints(), + #[cfg(not(feature = "coconut"))] + cosmos_mnemonic: "".to_string(), nym_root_directory: Config::default_root_directory(), persistent_storage: Default::default(), } diff --git a/gateway/src/config/template.rs b/gateway/src/config/template.rs index 81051f9cb5..3205bae00c 100644 --- a/gateway/src/config/template.rs +++ b/gateway/src/config/template.rs @@ -34,6 +34,9 @@ private_sphinx_key_file = '{{ gateway.private_sphinx_key_file }}' # Path to file containing public sphinx key. public_sphinx_key_file = '{{ gateway.public_sphinx_key_file }}' +# Addess to an Ethereum full node. +eth_endpoint = '{{ gateway.eth_endpoint }}' + ##### additional gateway config options ##### # Optional address announced to the directory server for the clients to connect to. @@ -56,6 +59,15 @@ validator_api_urls = [ {{/each}} ] +# Addresses to validators which the node uses to check for double spending of ERC20 tokens. +validator_nymd_urls = [ + {{#each gateway.validator_nymd_urls }} + '{{this}}', + {{/each}} +] + +cosmos_mnemonic = "{{ gateway.cosmos_mnemonic }}" + ##### advanced configuration options ##### # nym_home_directory specifies absolute path to the home nym gateway directory. diff --git a/gateway/src/node/client_handling/bandwidth.rs b/gateway/src/node/client_handling/bandwidth.rs index e0de947a75..d8d1472b0d 100644 --- a/gateway/src/node/client_handling/bandwidth.rs +++ b/gateway/src/node/client_handling/bandwidth.rs @@ -1,11 +1,17 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +#[cfg(feature = "coconut")] use std::convert::TryFrom; +#[cfg(feature = "coconut")] use coconut_interface::Credential; +#[cfg(feature = "coconut")] use credentials::error::Error; +#[cfg(not(feature = "coconut"))] +use credentials::token::bandwidth::TokenCredential; +#[cfg(feature = "coconut")] const BANDWIDTH_INDEX: usize = 0; pub struct Bandwidth { @@ -18,6 +24,7 @@ impl Bandwidth { } } +#[cfg(feature = "coconut")] impl TryFrom for Bandwidth { type Error = Error; @@ -34,3 +41,12 @@ impl TryFrom for Bandwidth { } } } + +#[cfg(not(feature = "coconut"))] +impl From for Bandwidth { + fn from(credential: TokenCredential) -> Self { + Bandwidth { + value: credential.bandwidth(), + } + } +} diff --git a/gateway/src/node/client_handling/mod.rs b/gateway/src/node/client_handling/mod.rs index 118a2ad4c4..9d50814da9 100644 --- a/gateway/src/node/client_handling/mod.rs +++ b/gateway/src/node/client_handling/mod.rs @@ -2,7 +2,5 @@ // SPDX-License-Identifier: Apache-2.0 pub(crate) mod active_clients; -pub(crate) mod websocket; - -#[cfg(feature = "coconut")] mod bandwidth; +pub(crate) mod websocket; diff --git a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs index f623503bff..b242ebf610 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -17,13 +17,11 @@ use thiserror::Error; use tokio::io::{AsyncRead, AsyncWrite}; use tokio_tungstenite::tungstenite::protocol::Message; -#[cfg(feature = "coconut")] use crate::node::client_handling::bandwidth::Bandwidth; -#[cfg(feature = "coconut")] use gateway_requests::iv::IV; #[derive(Debug, Error)] -enum RequestHandlingError { +pub(crate) enum RequestHandlingError { #[error("Internal gateway storage error")] StorageError(#[from] StorageError), @@ -39,13 +37,27 @@ enum RequestHandlingError { #[error("The received request is not valid in the current context")] IllegalRequest, - #[cfg(feature = "coconut")] #[error("Provided bandwidth credential asks for more bandwidth than it is supported to add at once (credential value: {0}, supported: {}). Try to split it before attempting again", i64::MAX)] UnsupportedBandwidthValue(u64), - #[cfg(feature = "coconut")] #[error("Provided bandwidth credential did not verify correctly")] - InvalidCoconutBandwidthCredential, + InvalidBandwidthCredential, + + #[cfg(not(feature = "coconut"))] + #[error("Ethereum web3 error")] + Web3Error(#[from] web3::Error), + + #[cfg(not(feature = "coconut"))] + #[error("Ethereum ABI error")] + EthAbiError(#[from] web3::ethabi::Error), + + #[cfg(not(feature = "coconut"))] + #[error("Ethereum contract error")] + EthContractError(#[from] web3::contract::Error), + + #[cfg(not(feature = "coconut"))] + #[error("Nymd Error - {0}")] + NymdError(#[from] validator_client::nymd::error::NymdError), #[cfg(feature = "coconut")] #[error("Provided coconut bandwidth credential did not have expected structure - {0}")] @@ -117,10 +129,6 @@ where Ok(bandwidth) } - // note: this is not technically a "coconut" thing, but currently we have no non-coconut - // bandwidth handling and hence clippy complains about dead and unreachable code - // so whenever we introduce another form of bandwidth claim, this feature flag should get removed - #[cfg(feature = "coconut")] /// Increases the amount of available bandwidth of the connected client by the specified value. /// /// # Arguments @@ -180,7 +188,7 @@ where )?; if !credential.verify(&self.inner.aggregated_verification_key) { - return Err(RequestHandlingError::InvalidCoconutBandwidthCredential); + return Err(RequestHandlingError::InvalidBandwidthCredential); } let bandwidth = Bandwidth::try_from(credential)?; @@ -202,6 +210,69 @@ where Ok(ServerResponse::Bandwidth { available_total }) } + #[cfg(not(feature = "coconut"))] + /// Tries to handle the received bandwidth request by checking correctness of the received data + /// and if successful, increases client's bandwidth by an appropriate amount. + /// + /// # Arguments + /// + /// * `enc_credential`: raw encrypted bandwidth credential to verify. + /// * `iv`: fresh iv used for the credential. + async fn handle_token_bandwidth( + &mut self, + enc_credential: Vec, + iv: Vec, + ) -> Result { + let iv = IV::try_from_bytes(&iv)?; + let credential = ClientControlRequest::try_from_enc_token_bandwidth_credential( + enc_credential, + &self.client.shared_keys, + iv, + )?; + + debug!("Received bandwidth increase request. Verifying signature"); + if !credential.verify_signature() { + return Err(RequestHandlingError::InvalidBandwidthCredential); + } + debug!("Verifying Ethereum for token burn..."); + self.inner + .erc20_bridge + .verify_eth_events(credential.verification_key()) + .await?; + debug!("Claim the token on Cosmos, to make sure it's not spent twice..."); + self.inner.erc20_bridge.claim_token(&credential).await?; + + let bandwidth = Bandwidth::from(credential); + let bandwidth_value = bandwidth.value(); + + if bandwidth_value > i64::MAX as u64 { + // note that this would have represented more than 1 exabyte, + // which is like 125,000 worth of hard drives so I don't think we have + // to worry about it for now... + warn!("Somehow we received bandwidth value higher than 9223372036854775807. We don't really want to deal with this now"); + return Err(RequestHandlingError::UnsupportedBandwidthValue( + bandwidth_value, + )); + } + + self.increase_bandwidth(bandwidth_value as i64).await?; + let available_total = self.get_available_bandwidth().await?; + debug!("Increased bandwidth for client: {:?}", self.client.address); + + Ok(ServerResponse::Bandwidth { available_total }) + } + + async fn handle_bandwidth( + &mut self, + enc_credential: Vec, + iv: Vec, + ) -> Result { + #[cfg(feature = "coconut")] + return self.handle_coconut_bandwidth(enc_credential, iv).await; + #[cfg(not(feature = "coconut"))] + return self.handle_token_bandwidth(enc_credential, iv).await; + } + /// Tries to handle request to forward sphinx packet into the network. The request can only succeed /// if the client has enough available bandwidth. /// @@ -214,17 +285,8 @@ where &self, mix_packet: MixPacket, ) -> Result { - // currently we have no way for increasing bandwidth hence we shouldn't be performing - // any meaningful metering. Once we have another way of claiming bandwidth, this - // feature lock should go away - #[cfg(feature = "coconut")] - // for now let's just use actual size of the sphinx packet. there's a tiny bit of overhead - // we're not including (but it's literally like 2 bytes) when the packet is framed let consumed_bandwidth = mix_packet.sphinx_packet().len() as i64; - #[cfg(not(feature = "coconut"))] - let consumed_bandwidth = 0; - let available_bandwidth = self.get_available_bandwidth().await?; if available_bandwidth < consumed_bandwidth { @@ -273,9 +335,8 @@ where match ClientControlRequest::try_from(raw_request) { Err(e) => RequestHandlingError::InvalidTextRequest(e).into_error_message(), Ok(request) => match request { - #[cfg(feature = "coconut")] - ClientControlRequest::CoconutBandwidthCredential { enc_credential, iv } => { - match self.handle_coconut_bandwidth(enc_credential, iv).await { + ClientControlRequest::BandwidthCredential { enc_credential, iv } => { + match self.handle_bandwidth(enc_credential, iv).await { Ok(response) => response.into(), Err(err) => err.into_error_message(), } 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 new file mode 100644 index 0000000000..1998e9baed --- /dev/null +++ b/gateway/src/node/client_handling/websocket/connection_handler/eth_events.rs @@ -0,0 +1,204 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bip39::core::str::FromStr; +use bip39::Mnemonic; +use rand::seq::SliceRandom; +use rand::thread_rng; +use url::Url; +use web3::contract::tokens::Detokenize; +use web3::contract::{Contract, Error}; +use web3::ethabi::Token; +use web3::transports::Http; +use web3::types::{BlockNumber, FilterBuilder, H256}; +use web3::Web3; + +use crate::node::client_handling::websocket::connection_handler::authenticated::RequestHandlingError; +use credentials::token::bandwidth::TokenCredential; +use crypto::asymmetric::identity::{PublicKey, Signature}; +use erc20_bridge_contract::msg::ExecuteMsg; +use erc20_bridge_contract::payment::LinkPaymentData; +use gateway_client::bandwidth::eth_contract; +use network_defaults::{COSMOS_CONTRACT_ADDRESS, DENOM, ETH_EVENT_NAME, ETH_MIN_BLOCK_DEPTH}; +use validator_client::nymd::{ + AccountId, CosmosCoin, Decimal, Denom, Fee, Gas, NymdClient, SigningNymdClient, +}; + +pub(crate) struct ERC20Bridge { + // This is needed because web3's Contract doesn't sufficiently expose it's eth interface + web3: Web3, + contract: Contract, + nymd_client: NymdClient, +} + +impl ERC20Bridge { + pub fn new(eth_endpoint: String, nymd_urls: Vec, cosmos_mnemonic: String) -> Self { + let transport = Http::new(ð_endpoint).expect("Invalid Ethereum endpoint"); + let web3 = Web3::new(transport); + let nymd_url = nymd_urls + .choose(&mut thread_rng()) + .expect("The list of validators is empty"); + let mnemonic = + Mnemonic::from_str(&cosmos_mnemonic).expect("Invalid Cosmos mnemonic provided"); + let nymd_client = NymdClient::connect_with_mnemonic( + nymd_url.as_ref(), + AccountId::from_str(COSMOS_CONTRACT_ADDRESS).ok(), + mnemonic, + ) + .expect("Could not create nymd client"); + + ERC20Bridge { + contract: eth_contract(web3.clone()), + web3, + nymd_client, + } + } + + pub(crate) async fn verify_eth_events( + &self, + verification_key: PublicKey, + ) -> Result<(), RequestHandlingError> { + // It's safe to unwrap here, as we are guarded by a unit test that checks the event + // name constant against the contract abi + let event = self.contract.abi().event(ETH_EVENT_NAME).unwrap(); + let latest_block = self.web3.eth().block_number().await?; + let check_until = if cfg!(debug_assertions) { + latest_block + } else { + latest_block - ETH_MIN_BLOCK_DEPTH + }; + let filter = FilterBuilder::default() + .address(vec![self.contract.address()]) + .topics( + Some(vec![event.signature()]), + Some(vec![H256::from(verification_key.to_bytes())]), + None, + None, + ) + .from_block(BlockNumber::Earliest) + .to_block(BlockNumber::Number(check_until)) + .build(); + // Get only the first event that checks out. If the client burns more tokens with the + // same verification key, those token would be lost + for l in self.web3.eth().logs(filter).await? { + let log = event.parse_log(web3::ethabi::RawLog { + topics: l.topics, + data: l.data.0, + })?; + let burned_event = + Burned::from_tokens(log.params.into_iter().map(|x| x.value).collect::>())?; + if burned_event.verify(verification_key) { + return Ok(()); + } + } + + Err(RequestHandlingError::InvalidBandwidthCredential) + } + + pub(crate) async fn claim_token( + &self, + credential: &TokenCredential, + ) -> Result<(), RequestHandlingError> { + // It's ok to unwrap here, as the cosmos contract and denom are set correctly + let contract_address = self.nymd_client.contract_address().unwrap(); + let coin = CosmosCoin { + denom: Denom::from_str(DENOM).unwrap(), + amount: Decimal::from(100000u64), + }; + let fee = Fee::from_amount_and_gas(coin.clone(), Gas::from(500000)); + let req = ExecuteMsg::LinkPayment { + data: LinkPaymentData::new( + credential.verification_key().to_bytes(), + credential.gateway_identity().to_bytes(), + credential.bandwidth(), + credential.signature_bytes(), + ), + }; + self.nymd_client + .execute( + // it's ok to unwrap here, as the address is + contract_address, + &req, + fee, + "Linking payment", + vec![coin], + ) + .await?; + Ok(()) + } +} + +#[derive(Debug)] +pub struct Burned { + /// The bandwidth bought by the client + pub bandwidth: u64, + /// Client public verification key + pub verification_key: PublicKey, + /// Signed verification key + pub signed_verification_key: Signature, +} + +impl Burned { + pub fn verify(&self, verification_key: PublicKey) -> bool { + self.verification_key == verification_key + && verification_key + .verify( + &self.verification_key.to_bytes(), + &self.signed_verification_key, + ) + .is_ok() + } +} + +impl Detokenize for Burned { + fn from_tokens(tokens: Vec) -> Result + where + Self: Sized, + { + if tokens.len() != 3 { + return Err(Error::InvalidOutputType(format!( + "Expected three elements, got: {:?}", + tokens + ))); + } + let bandwidth = tokens + .get(0) + .unwrap() + .clone() + .into_uint() + .ok_or_else(|| Error::InvalidOutputType(String::from("Expected Uint for bandwidth")))? + .as_u64(); + let verification_key: [u8; 32] = tokens + .get(1) + .unwrap() + .clone() + .into_uint() + .ok_or_else(|| { + Error::InvalidOutputType(String::from("Expected Uint for verification key")) + })? + .into(); + let verification_key = PublicKey::from_bytes(&verification_key).map_err(|_| { + Error::InvalidOutputType(format!( + "Expected verification key of 32 bytes, got: {}", + verification_key.len() + )) + })?; + let signed_verification_key = + tokens.get(2).unwrap().clone().into_bytes().ok_or_else(|| { + Error::InvalidOutputType(String::from("Expected Bytes for signed_verification_key")) + })?; + let signed_verification_key = + Signature::from_bytes(&signed_verification_key).map_err(|_| { + Error::InvalidOutputType(format!( + "Expected signature of 64 bytes, got: {}", + signed_verification_key.len() + )) + })?; + + Ok(Burned { + bandwidth, + verification_key, + signed_verification_key, + }) + } +} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index 5c413d7022..3ae10063af 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -30,6 +30,9 @@ use tokio_tungstenite::tungstenite::{protocol::Message, Error as WsError}; #[cfg(feature = "coconut")] use coconut_interface::VerificationKey; +#[cfg(not(feature = "coconut"))] +use crate::node::client_handling::websocket::connection_handler::eth_events::ERC20Bridge; + #[derive(Debug, Error)] enum InitialAuthenticationError { #[error("Internal gateway storage error")] @@ -75,6 +78,9 @@ pub(crate) struct FreshHandler { #[cfg(feature = "coconut")] pub(crate) aggregated_verification_key: VerificationKey, + + #[cfg(not(feature = "coconut"))] + pub(crate) erc20_bridge: Arc, } impl FreshHandler @@ -91,6 +97,7 @@ where storage: PersistentStorage, active_clients_store: ActiveClientsStore, #[cfg(feature = "coconut")] aggregated_verification_key: VerificationKey, + #[cfg(not(feature = "coconut"))] erc20_bridge: Arc, ) -> Self { FreshHandler { rng, @@ -101,6 +108,8 @@ where storage, #[cfg(feature = "coconut")] aggregated_verification_key, + #[cfg(not(feature = "coconut"))] + erc20_bridge, } } @@ -394,12 +403,20 @@ where .authenticate_client(address, encrypted_address, iv) .await?; let status = shared_keys.is_some(); + let bandwidth_remaining = self + .storage + .get_available_bandwidth(address) + .await? + .unwrap_or(0); let client_details = shared_keys.map(|shared_keys| ClientDetails::new(address, shared_keys)); Ok(InitialAuthResult::new( client_details, - ServerResponse::Authenticate { status }, + ServerResponse::Authenticate { + status, + bandwidth_remaining, + }, )) } @@ -497,11 +514,6 @@ where ClientControlRequest::RegisterHandshakeInitRequest { data } => { self.handle_register(data).await } - - // note: this is not technically a "coconut" thing, but currently we have no non-coconut - // bandwidth handling and hence clippy complains about dead and unreachable code - // so whenever we introduce another form of bandwidth claim, this feature flag should get removed - #[cfg(feature = "coconut")] // won't accept anything else (like bandwidth) without prior authentication _ => Err(InitialAuthenticationError::InvalidRequest), } diff --git a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs index 435163cc63..0dda785d38 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs @@ -13,6 +13,8 @@ pub(crate) use self::authenticated::AuthenticatedHandler; pub(crate) use self::fresh::FreshHandler; mod authenticated; +#[cfg(not(feature = "coconut"))] +pub(crate) mod eth_events; mod fresh; //// TODO: note for my future self to consider the following idea: diff --git a/gateway/src/node/client_handling/websocket/listener.rs b/gateway/src/node/client_handling/websocket/listener.rs index c55da12c3f..2d6950ffd5 100644 --- a/gateway/src/node/client_handling/websocket/listener.rs +++ b/gateway/src/node/client_handling/websocket/listener.rs @@ -16,12 +16,18 @@ use tokio::task::JoinHandle; #[cfg(feature = "coconut")] use coconut_interface::VerificationKey; +#[cfg(not(feature = "coconut"))] +use crate::node::client_handling::websocket::connection_handler::eth_events::ERC20Bridge; + pub(crate) struct Listener { address: SocketAddr, local_identity: Arc, #[cfg(feature = "coconut")] aggregated_verification_key: VerificationKey, + + #[cfg(not(feature = "coconut"))] + erc20_bridge: Arc, } impl Listener { @@ -29,12 +35,15 @@ impl Listener { address: SocketAddr, local_identity: Arc, #[cfg(feature = "coconut")] aggregated_verification_key: VerificationKey, + #[cfg(not(feature = "coconut"))] erc20_bridge: ERC20Bridge, ) -> Self { Listener { address, local_identity, #[cfg(feature = "coconut")] aggregated_verification_key, + #[cfg(not(feature = "coconut"))] + erc20_bridge: Arc::new(erc20_bridge), } } @@ -70,6 +79,8 @@ impl Listener { active_clients_store.clone(), #[cfg(feature = "coconut")] self.aggregated_verification_key.clone(), + #[cfg(not(feature = "coconut"))] + Arc::clone(&self.erc20_bridge), ); tokio::spawn(async move { handle.start_handling().await }); } diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 201a4c8e2e..0a9bb50578 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -15,6 +15,8 @@ use std::net::SocketAddr; use std::process; use std::sync::Arc; +#[cfg(not(feature = "coconut"))] +use crate::node::client_handling::websocket::connection_handler::eth_events::ERC20Bridge; #[cfg(feature = "coconut")] use coconut_interface::VerificationKey; #[cfg(feature = "coconut")] @@ -88,6 +90,7 @@ impl Gateway { forwarding_channel: MixForwardingSender, active_clients_store: ActiveClientsStore, #[cfg(feature = "coconut")] verification_key: VerificationKey, + #[cfg(not(feature = "coconut"))] erc20_bridge: ERC20Bridge, ) { info!("Starting client [web]socket listener..."); @@ -101,6 +104,8 @@ impl Gateway { Arc::clone(&self.identity), #[cfg(feature = "coconut")] verification_key, + #[cfg(not(feature = "coconut"))] + erc20_bridge, ) .start( forwarding_channel, @@ -180,6 +185,13 @@ impl Gateway { .await .expect("failed to contact validators to obtain their verification keys"); + #[cfg(not(feature = "coconut"))] + let erc20_bridge = ERC20Bridge::new( + self.config.get_eth_endpoint(), + self.config.get_validator_nymd_endpoints(), + self.config.get_cosmos_mnemonic(), + ); + let mix_forwarding_channel = self.start_packet_forwarder(); let active_clients_store = ActiveClientsStore::new(); @@ -193,6 +205,8 @@ impl Gateway { active_clients_store, #[cfg(feature = "coconut")] validators_verification_key, + #[cfg(not(feature = "coconut"))] + erc20_bridge, ); info!("Finished nym gateway startup procedure - it should now be able to receive mix and client traffic!"); diff --git a/gateway/src/node/storage/bandwidth.rs b/gateway/src/node/storage/bandwidth.rs index 5299f959d3..e2367b2ee1 100644 --- a/gateway/src/node/storage/bandwidth.rs +++ b/gateway/src/node/storage/bandwidth.rs @@ -54,10 +54,6 @@ impl BandwidthManager { .await } - // note: this is not technically a "coconut" thing, but currently we have no non-coconut - // bandwidth handling and hence clippy complains about dead and unreachable code - // so whenever we introduce another form of bandwidth claim, this feature flag should get removed - #[cfg(feature = "coconut")] /// Increases available bandwidth of the particular client by the specified amount. /// /// # Arguments diff --git a/gateway/src/node/storage/mod.rs b/gateway/src/node/storage/mod.rs index 2af35291e1..a74d53a389 100644 --- a/gateway/src/node/storage/mod.rs +++ b/gateway/src/node/storage/mod.rs @@ -211,10 +211,6 @@ impl PersistentStorage { Ok(res) } - // note: this is not technically a "coconut" thing, but currently we have no non-coconut - // bandwidth handling and hence clippy complains about dead and unreachable code - // so whenever we introduce another form of bandwidth claim, this feature flag should get removed - #[cfg(feature = "coconut")] /// Increases available bandwidth of the particular client by the specified amount. /// /// # Arguments diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 869006a638..0360d27c08 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -24,6 +24,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if 1.0.0", + "cipher", + "cpufeatures", + "ctr", + "opaque-debug 0.3.0", +] + [[package]] name = "aho-corasick" version = "0.7.18" @@ -130,6 +143,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "az" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6dff4a1892b54d70af377bf7a17064192e822865791d812957f21e3108c325" + [[package]] name = "base64" version = "0.13.0" @@ -157,7 +176,7 @@ dependencies = [ "k256", "ripemd160", "sha2", - "subtle", + "subtle 2.4.1", "zeroize", ] @@ -186,6 +205,18 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "blake2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94cb07b0da6a73955f8fb85d24c466778e70cda767a568229b104f0264089330" +dependencies = [ + "byte-tools", + "crypto-mac 0.7.0", + "digest 0.8.1", + "opaque-debug 0.2.3", +] + [[package]] name = "blake3" version = "1.0.0" @@ -197,6 +228,7 @@ dependencies = [ "cc", "cfg-if 1.0.0", "constant_time_eq", + "crypto-mac 0.11.1", "digest 0.9.0", "rayon", ] @@ -255,7 +287,7 @@ dependencies = [ "group", "pairing", "rand_core 0.6.3", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -288,6 +320,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" +[[package]] +name = "bytemuck" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72957246c41db82b8ef88a5486143830adeb8227ef9837740bdec67724cf2c5b" + [[package]] name = "byteorder" version = "1.4.3" @@ -391,6 +429,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "chacha" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf3c081b5fba1e5615640aae998e0fbd10c24cbd897ee39ed754a77601a4862" +dependencies = [ + "byteorder", + "keystream", +] + [[package]] name = "chrono" version = "0.4.19" @@ -402,6 +450,15 @@ dependencies = [ "serde", ] +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array 0.14.4", +] + [[package]] name = "cloudabi" version = "0.0.3" @@ -705,6 +762,8 @@ name = "credentials" version = "0.1.0" dependencies = [ "coconut-interface", + "crypto", + "network-defaults", "thiserror", "url", "validator-client", @@ -760,6 +819,26 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto" +version = "0.1.0" +dependencies = [ + "aes", + "blake3", + "bs58", + "cipher", + "digest 0.9.0", + "ed25519-dalek", + "generic-array 0.14.4", + "hkdf", + "hmac", + "log", + "nymsphinx-types", + "pemstore", + "rand 0.7.3", + "x25519-dalek", +] + [[package]] name = "crypto-bigint" version = "0.2.6" @@ -768,10 +847,20 @@ checksum = "e49339137316df1914fdb54a5eae75a73f45068fd0d2178fe235b11d93238a6e" dependencies = [ "generic-array 0.14.4", "rand_core 0.6.3", - "subtle", + "subtle 2.4.1", "zeroize", ] +[[package]] +name = "crypto-mac" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" +dependencies = [ + "generic-array 0.12.4", + "subtle 1.0.0", +] + [[package]] name = "crypto-mac" version = "0.11.1" @@ -779,7 +868,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" dependencies = [ "generic-array 0.14.4", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -818,6 +907,15 @@ dependencies = [ "sct", ] +[[package]] +name = "ctr" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" +dependencies = [ + "cipher", +] + [[package]] name = "curve25519-dalek" version = "3.2.0" @@ -827,7 +925,7 @@ dependencies = [ "byteorder", "digest 0.9.0", "rand_core 0.5.1", - "subtle", + "subtle 2.4.1", "zeroize", ] @@ -1073,6 +1171,8 @@ checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ "curve25519-dalek", "ed25519", + "rand 0.7.3", + "serde", "sha2", "zeroize", ] @@ -1109,7 +1209,7 @@ dependencies = [ "group", "pkcs8", "rand_core 0.6.3", - "subtle", + "subtle 2.4.1", "zeroize", ] @@ -1172,7 +1272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0f40b2dcd8bc322217a5f6559ae5f9e9d1de202a2ecee2e9eafcbece7562a4f" dependencies = [ "rand_core 0.6.3", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -1197,6 +1297,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "fixed" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d333a26ec13a023c6dff4b7584de4d323cfee2e508f5dd2bbee6669e4f7efdf" +dependencies = [ + "az", + "bytemuck", + "half", + "typenum", +] + [[package]] name = "flate2" version = "1.0.21" @@ -1493,8 +1605,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ "cfg-if 1.0.0", + "js-sys", "libc", "wasi 0.9.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1663,7 +1777,7 @@ dependencies = [ "byteorder", "ff", "rand_core 0.6.3", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -1741,6 +1855,12 @@ 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" @@ -1810,6 +1930,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-literal" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e4590e13640f19f249fe3e4eca5113bc4289f2497710378190e7f4bd96f45b" + [[package]] name = "hkd32" version = "0.6.0" @@ -1824,13 +1950,23 @@ dependencies = [ "zeroize", ] +[[package]] +name = "hkdf" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b" +dependencies = [ + "digest 0.9.0", + "hmac", +] + [[package]] name = "hmac" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" dependencies = [ - "crypto-mac", + "crypto-mac 0.11.1", "digest 0.9.0", ] @@ -2161,6 +2297,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" +[[package]] +name = "keystream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28" + [[package]] name = "kuchiki" version = "0.8.1" @@ -2185,6 +2327,24 @@ version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21" +[[package]] +name = "libm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" + +[[package]] +name = "lioness" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae926706ba42c425c9457121178330d75e273df2e82e28b758faf3de3a9acb9" +dependencies = [ + "arrayref", + "blake2", + "chacha", + "keystream", +] + [[package]] name = "lock_api" version = "0.4.5" @@ -2329,10 +2489,15 @@ dependencies = [ name = "mixnet-contract" version = "0.1.0" dependencies = [ + "az", "cosmwasm-std", + "fixed", + "log", + "network-defaults", "schemars", "serde", "serde_repr", + "thiserror", "ts-rs", ] @@ -2404,6 +2569,7 @@ checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d" name = "network-defaults" version = "0.1.0" dependencies = [ + "hex-literal", "serde", "time 0.3.3", "url", @@ -2481,6 +2647,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ "autocfg 1.0.1", + "libm", ] [[package]] @@ -2540,6 +2707,13 @@ dependencies = [ "validator-client", ] +[[package]] +name = "nymsphinx-types" +version = "0.1.0" +dependencies = [ + "sphinx", +] + [[package]] name = "objc" version = "0.2.7" @@ -2706,7 +2880,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d95f5254224e617595d2cc3cc73ff0a5eaf2637519e25f03388154e9378b6ffa" dependencies = [ - "crypto-mac", + "crypto-mac 0.11.1", ] [[package]] @@ -2736,6 +2910,24 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c719dcf55f09a3a7e764c6649ab594c18a177e3599c467983cdf644bfc0a4088" +[[package]] +name = "pem" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb" +dependencies = [ + "base64", + "once_cell", + "regex", +] + +[[package]] +name = "pemstore" +version = "0.1.0" +dependencies = [ + "pem", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -3198,6 +3390,16 @@ dependencies = [ "getrandom 0.2.3", ] +[[package]] +name = "rand_distr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9532ada3929fb8b2e9dbe28d1e06c9b2cc65813f074fcb6bd5fbefeff9d56" +dependencies = [ + "num-traits", + "rand 0.7.3", +] + [[package]] name = "rand_hc" version = "0.1.0" @@ -3854,6 +4056,29 @@ dependencies = [ "system-deps 1.3.2", ] +[[package]] +name = "sphinx" +version = "0.1.0" +source = "git+https://github.com/nymtech/sphinx?rev=c494250f2a78bed33a618d470792418eee932859#c494250f2a78bed33a618d470792418eee932859" +dependencies = [ + "aes", + "arrayref", + "blake2", + "bs58", + "byteorder", + "chacha", + "curve25519-dalek", + "digest 0.9.0", + "hkdf", + "hmac", + "lioness", + "log", + "rand 0.7.3", + "rand_distr", + "sha2", + "subtle 2.4.1", +] + [[package]] name = "spin" version = "0.5.2" @@ -3970,6 +4195,12 @@ dependencies = [ "syn", ] +[[package]] +name = "subtle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" + [[package]] name = "subtle" version = "2.4.1" @@ -4408,7 +4639,7 @@ dependencies = [ "serde_repr", "sha2", "signature", - "subtle", + "subtle 2.4.1", "subtle-encoding", "tendermint-proto", "zeroize", @@ -5136,6 +5367,17 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x25519-dalek" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2392b6b94a576b4e2bf3c5b2757d63f10ada8020a2e4d08ac849ebcf6ea8e077" +dependencies = [ + "curve25519-dalek", + "rand_core 0.5.1", + "zeroize", +] + [[package]] name = "xattr" version = "0.2.2" @@ -5147,9 +5389,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.4.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "377db0846015f7ae377174787dd452e1c5f5a9050bc6f954911d01f116daa0cd" +checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" dependencies = [ "zeroize_derive", ] diff --git a/nym-wallet/src-tauri/src/operations/admin.rs b/nym-wallet/src-tauri/src/operations/admin.rs index 71ac69af8a..0c9db7a115 100644 --- a/nym-wallet/src-tauri/src/operations/admin.rs +++ b/nym-wallet/src-tauri/src/operations/admin.rs @@ -17,6 +17,7 @@ pub struct TauriStateParams { minimum_gateway_bond: String, mixnode_bond_reward_rate: String, mixnode_delegation_reward_rate: String, + mixnode_rewarded_set_size: u32, mixnode_active_set_size: u32, } @@ -28,6 +29,7 @@ impl From for TauriStateParams { minimum_gateway_bond: p.minimum_gateway_bond.to_string(), mixnode_bond_reward_rate: p.mixnode_bond_reward_rate.to_string(), mixnode_delegation_reward_rate: p.mixnode_delegation_reward_rate.to_string(), + mixnode_rewarded_set_size: p.mixnode_rewarded_set_size, mixnode_active_set_size: p.mixnode_active_set_size, } } @@ -43,6 +45,7 @@ impl TryFrom for StateParams { minimum_gateway_bond: Uint128::try_from(p.minimum_gateway_bond.as_str())?, mixnode_bond_reward_rate: Decimal::from_str(p.mixnode_bond_reward_rate.as_str())?, mixnode_delegation_reward_rate: Decimal::from_str(p.mixnode_delegation_reward_rate.as_str())?, + mixnode_rewarded_set_size: p.mixnode_rewarded_set_size, mixnode_active_set_size: p.mixnode_active_set_size, }) } diff --git a/nym-wallet/src/types/rust/operation.ts b/nym-wallet/src/types/rust/operation.ts index f22f63b183..44c6d684c4 100644 --- a/nym-wallet/src/types/rust/operation.ts +++ b/nym-wallet/src/types/rust/operation.ts @@ -10,6 +10,6 @@ export type Operation = | "UndelegateFromMixnode" | "BondGateway" | "UnbondGateway" - | "DelegateToGateway" - | "UndelegateFromGateway" - | "UpdateStateParams"; \ No newline at end of file + | "UpdateStateParams" + | "BeginMixnodeRewarding" + | "FinishMixnodeRewarding"; \ No newline at end of file diff --git a/nym-wallet/src/types/rust/stateparams.ts b/nym-wallet/src/types/rust/stateparams.ts index 4f6a8c6eea..ebe12a6bf1 100644 --- a/nym-wallet/src/types/rust/stateparams.ts +++ b/nym-wallet/src/types/rust/stateparams.ts @@ -4,5 +4,6 @@ export interface TauriStateParams { minimum_gateway_bond: string; mixnode_bond_reward_rate: string; mixnode_delegation_reward_rate: string; + mixnode_rewarded_set_size: number; mixnode_active_set_size: number; } \ No newline at end of file diff --git a/validator-api/Cargo.toml b/validator-api/Cargo.toml index 089c8d2529..f55a71a6bc 100644 --- a/validator-api/Cargo.toml +++ b/validator-api/Cargo.toml @@ -23,7 +23,9 @@ humantime-serde = "1.0" log = "0.4" pin-project = "1.0" pretty_env_logger = "0.4" -rand = "0.7" +rand-07 = { package = "rand", version = "0.7" } # required for compatibility +rand = "0.8" +rand_chacha = "0.3.1" reqwest = { version = "0.11", features = ["json"] } rocket = { version = "0.5.0-rc.1", features = ["json"] } serde = "1.0" @@ -40,6 +42,7 @@ getset = "0.1.1" rocket_sync_db_pools = {version = "0.1.0-rc.1", default-features = false} sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]} + ## internal config = { path = "../common/config" } crypto = { path="../common/crypto" } @@ -54,6 +57,8 @@ credentials = { path = "../common/credentials", optional = true } [features] coconut = ["coconut-interface", "credentials", "gateway-client/coconut"] +default = ["tokenomics"] +tokenomics = [] [build-dependencies] tokio = { version = "1.4", features = ["rt-multi-thread", "macros"] } diff --git a/validator-api/src/cache/mod.rs b/validator-api/src/cache/mod.rs index 7a1cf9864b..5bdce32952 100644 --- a/validator-api/src/cache/mod.rs +++ b/validator-api/src/cache/mod.rs @@ -4,14 +4,18 @@ use crate::nymd_client::Client; use anyhow::Result; use config::defaults::VALIDATOR_API_VERSION; -use mixnet_contract::{GatewayBond, MixNodeBond, StateParams}; +use mixnet_contract::{GatewayBond, MixNodeBond, RewardingIntervalResponse, StateParams}; +use rand::prelude::SliceRandom; +use rand_chacha::rand_core::SeedableRng; +use rand_chacha::ChaCha20Rng; use rocket::fairing::AdHoc; use serde::Serialize; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tokio::sync::RwLock; use tokio::time; +use validator_client::nymd::hash::SHA256_HASH_SIZE; use validator_client::nymd::CosmWasmClient; pub(crate) mod routes; @@ -29,11 +33,15 @@ pub struct ValidatorCache { struct ValidatorCacheInner { initialised: AtomicBool, + latest_known_rewarding_block: AtomicU64, + mixnodes: RwLock>>, gateways: RwLock>>, - active_mixnodes_available: AtomicBool, - current_mixnode_active_set_size: AtomicUsize, + rewarded_mixnodes: RwLock>>, + + current_mixnode_rewarded_set_size: AtomicU32, + current_mixnode_active_set_size: AtomicU32, } #[derive(Default, Serialize, Clone)] @@ -51,6 +59,13 @@ impl Cache { .as_secs() } + fn renew(&mut self) { + self.as_at = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + } + pub fn into_inner(self) -> T { self.value } @@ -75,19 +90,32 @@ impl ValidatorCacheRefresher { { let (mixnodes, gateways) = tokio::try_join!( self.nymd_client.get_mixnodes(), - self.nymd_client.get_gateways() + self.nymd_client.get_gateways(), )?; let state_params = self.nymd_client.get_state_params().await?; + let current_rewarding_interval = self.nymd_client.get_current_rewarding_interval().await?; + let rewarding_block_hash = self + .nymd_client + .get_block_hash( + current_rewarding_interval.current_rewarding_interval_starting_block as u32, + ) + .await?; info!( "Updating validator cache. There are {} mixnodes and {} gateways", mixnodes.len(), - gateways.len() + gateways.len(), ); self.cache - .update_cache(mixnodes, gateways, state_params) + .update_cache( + mixnodes, + gateways, + state_params, + current_rewarding_interval, + rewarding_block_hash, + ) .await; Ok(()) @@ -128,53 +156,116 @@ impl ValidatorCache { routes::get_mixnodes, routes::get_gateways, routes::get_active_mixnodes, + routes::get_rewarded_mixnodes, ], ) }) } - // TODO: check if all nodes can be compared together, - // i.e. they all have the same denom for bonds and delegations - fn verify_mixnodes(&self, mixnodes: &[MixNodeBond]) -> bool { + // NOTE: this does not guarantee consistent results between multiple validator APIs, because + // currently we do not guarantee the list of mixnodes (i.e. `mixnodes: &[MixNodeBond]`) will be the same - + // somebody might bond/unbond a node or change delegation between different cache refreshes. + // + // I guess that's not a problem right now and we can resolve it later. My idea for that would be as follows: + // since the demanded set changes only monthly, just write the identities of those nodes to the smart + // contract upon finished rewarding (this works under assumption of rewards being distributed by a single validator) + // + // alternatively we could have some state locking mechanism for the duration of determining the demanded set + // this could work with multiple validators via some multisig mechanism + fn determine_rewarded_set( + &self, + mixnodes: &[MixNodeBond], + nodes_to_select: u32, + block_hash: Option<[u8; SHA256_HASH_SIZE]>, + ) -> Vec { if mixnodes.is_empty() { - return true; - } - let expected_denom = &mixnodes[0].bond_amount.denom; - for mixnode in mixnodes { - if &mixnode.bond_amount.denom != expected_denom - || &mixnode.total_delegation.denom != expected_denom - { - return false; - } + return Vec::new(); } - true + if block_hash.is_none() { + // I'm not entirely sure under what condition can hash of a block be empty + // (note that we know the block exists otherwise we would have gotten an error + // when attempting to retrieve the hash) + error!("The hash of the block of the rewarding interval is None - we're not going to update the set"); + return Vec::new(); + } + + // seed our rng with the hash of the block of the most recent rewarding interval + let mut rng = ChaCha20Rng::from_seed(block_hash.unwrap()); + + // generate list of mixnodes and their relatively weight (by total stake) + let choices = mixnodes + .iter() + .map(|mix| { + // note that the theoretical maximum possible stake is equal to the total + // supply of all tokens, i.e. 1B (which is 1 quadrillion of native tokens, i.e. 10^15 ~ 2^50) + // which is way below maximum value of f64, so the cast is fine + let total_stake = mix.total_stake().unwrap_or_default() as f64; + (mix, total_stake) + }) // if for some reason node is invalid, treat it as 0 stake/weight + .collect::>(); + + // the unwrap here is fine as an error can only be thrown under one of the following conditions: + // - our mixnode list is empty - we have already checked for that + // - we have invalid weights, i.e. less than zero or NaNs - it shouldn't happen in our case as we safely cast down from u128 + // - all weights are zero - it's impossible in our case as the list of nodes is not empty and weight is proportional to stake. You must have non-zero stake in order to bond + // - we have more than u32::MAX values (which is incredibly unrealistic to have 4B mixnodes bonded... literally every other person on the planet would need one) + choices + .choose_multiple_weighted(&mut rng, nodes_to_select as usize, |item| item.1) + .unwrap() + .map(|(bond, _weight)| *bond) + .cloned() + .collect() } async fn update_cache( &self, - mut mixnodes: Vec, + mixnodes: Vec, gateways: Vec, state: StateParams, + rewarding_interval: RewardingIntervalResponse, + rewarding_block_hash: Option<[u8; SHA256_HASH_SIZE]>, ) { + // if the rewarding is currently in progress, don't mess with the rewarded/active sets + // as most likely will be changed next time this function is called + // // if our data is valid, it means the active sets are available, // otherwise we must explicitly indicate nobody can read this data + if !rewarding_interval.rewarding_in_progress { + // if we're still in the same rewarding interval, i.e. the latest block is the same, + // there's nothing we have to do + if rewarding_interval.current_rewarding_interval_starting_block + > self + .inner + .latest_known_rewarding_block + .load(Ordering::SeqCst) + { + let rewarded_nodes = self.determine_rewarded_set( + &mixnodes, + state.mixnode_rewarded_set_size, + rewarding_block_hash, + ); - if self.verify_mixnodes(&mixnodes) { - // partial_cmp can only fail if the nodes have different denomination, - // but we just checked for that hence the unwraps are fine here - // Note the reverse order of comparison so that the "highest" node would be first - mixnodes.sort_by(|a, b| b.partial_cmp(a).unwrap()); - self.inner - .active_mixnodes_available - .store(true, Ordering::SeqCst); - self.inner - .current_mixnode_active_set_size - .store(state.mixnode_active_set_size as usize, Ordering::SeqCst); - } else { - self.inner - .active_mixnodes_available - .store(false, Ordering::SeqCst); + self.inner + .current_mixnode_rewarded_set_size + .store(state.mixnode_rewarded_set_size, Ordering::SeqCst); + self.inner + .current_mixnode_active_set_size + .store(state.mixnode_active_set_size, Ordering::SeqCst); + self.inner.latest_known_rewarding_block.store( + rewarding_interval.current_rewarding_interval_starting_block, + Ordering::SeqCst, + ); + + self.inner + .rewarded_mixnodes + .write() + .await + .set(rewarded_nodes); + } else { + // however, update the timestamp on the cache + self.inner.rewarded_mixnodes.write().await.renew() + } } self.inner.mixnodes.write().await.set(mixnodes); @@ -189,27 +280,28 @@ impl ValidatorCache { self.inner.gateways.read().await.clone() } - pub async fn active_mixnodes(&self) -> Option>> { - // if active set is available, it means it is already sorted - if self.inner.active_mixnodes_available.load(Ordering::SeqCst) { - let cache = self.inner.mixnodes.read().await; - let timestamp = cache.as_at; - let nodes = cache - .value - .iter() - .take( - self.inner - .current_mixnode_active_set_size - .load(Ordering::SeqCst), - ) - .cloned() - .collect(); - Some(Cache { - value: nodes, - as_at: timestamp, - }) - } else { - None + pub async fn rewarded_mixnodes(&self) -> Cache> { + self.inner.rewarded_mixnodes.read().await.clone() + } + + pub async fn active_mixnodes(&self) -> Cache> { + // rewarded set is already "sorted" by pseudo-randomly choosing mixnodes from + // all bonded nodes, weighted by stake. For the active set choose first k nodes. + let cache = self.inner.rewarded_mixnodes.read().await; + let timestamp = cache.as_at; + let nodes = cache + .value + .iter() + .take( + self.inner + .current_mixnode_active_set_size + .load(Ordering::SeqCst) as usize, + ) + .cloned() + .collect(); + Cache { + value: nodes, + as_at: timestamp, } } @@ -234,9 +326,11 @@ impl ValidatorCacheInner { fn new() -> Self { ValidatorCacheInner { initialised: AtomicBool::new(false), + latest_known_rewarding_block: Default::default(), mixnodes: RwLock::new(Cache::default()), gateways: RwLock::new(Cache::default()), - active_mixnodes_available: AtomicBool::new(false), + rewarded_mixnodes: RwLock::new(Cache::default()), + current_mixnode_rewarded_set_size: Default::default(), current_mixnode_active_set_size: Default::default(), } } diff --git a/validator-api/src/cache/routes.rs b/validator-api/src/cache/routes.rs index c3e3c858aa..cf59be469e 100644 --- a/validator-api/src/cache/routes.rs +++ b/validator-api/src/cache/routes.rs @@ -16,9 +16,12 @@ pub(crate) async fn get_gateways(cache: &State) -> Json, -) -> Option>> { - cache.active_mixnodes().await.map(|cache| Json(cache.value)) +#[get("/mixnodes/rewarded")] +pub(crate) async fn get_rewarded_mixnodes(cache: &State) -> Json> { + Json(cache.rewarded_mixnodes().await.value) +} + +#[get("/mixnodes/active")] +pub(crate) async fn get_active_mixnodes(cache: &State) -> Json> { + Json(cache.active_mixnodes().await.value) } diff --git a/validator-api/src/config/mod.rs b/validator-api/src/config/mod.rs index 5b8be3ac00..94301a7dab 100644 --- a/validator-api/src/config/mod.rs +++ b/validator-api/src/config/mod.rs @@ -25,7 +25,10 @@ const DEFAULT_MAX_CONCURRENT_GATEWAY_CLIENTS: usize = 50; const DEFAULT_PACKET_DELIVERY_TIMEOUT: Duration = Duration::from_secs(20); const DEFAULT_MONITOR_RUN_INTERVAL: Duration = Duration::from_secs(15 * 60); const DEFAULT_GATEWAY_PING_INTERVAL: Duration = Duration::from_secs(60); -const DEFAULT_GATEWAY_RESPONSE_TIMEOUT: Duration = Duration::from_millis(1_500); +// Set this to a high value for now, so that we don't risk sporadic timeouts that might cause +// bought bandwidth tokens to not have time to be spent; Once we remove the gateway from the +// bandwidth bridging protocol, we can come back to a smaller timeout value +const DEFAULT_GATEWAY_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5 * 60); const DEFAULT_GATEWAY_CONNECTION_TIMEOUT: Duration = Duration::from_millis(2_500); const DEFAULT_TEST_ROUTES: usize = 3; @@ -33,7 +36,7 @@ const DEFAULT_MINIMUM_TEST_ROUTES: usize = 1; const DEFAULT_ROUTE_TEST_PACKETS: usize = 1000; const DEFAULT_PER_NODE_TEST_PACKETS: usize = 3; -const DEFAULT_CACHE_INTERVAL: Duration = Duration::from_secs(60); +const DEFAULT_CACHE_INTERVAL: Duration = Duration::from_secs(10 * 60); const DEFAULT_MONITOR_THRESHOLD: u8 = 60; #[derive(Debug, Default, Deserialize, PartialEq, Serialize)] @@ -145,6 +148,20 @@ pub struct NetworkMonitor { #[serde(with = "humantime_serde")] packet_delivery_timeout: Duration, + /// Path to directory containing public/private keys used for bandwidth token purchase. + /// Those are saved in case of emergency, to be able to reclaim bandwidth tokens. + /// The public key is the name of the file, while the private key is the content. + #[cfg(not(feature = "coconut"))] + backup_bandwidth_token_keys_dir: PathBuf, + + /// Ethereum private key. + #[cfg(not(feature = "coconut"))] + eth_private_key: String, + + /// Addess to an Ethereum full node. + #[cfg(not(feature = "coconut"))] + eth_endpoint: String, + /// Desired number of test routes to be constructed (and working) during a monitor test run. test_routes: usize, @@ -160,6 +177,13 @@ pub struct NetworkMonitor { per_node_test_packets: usize, } +impl NetworkMonitor { + #[cfg(not(feature = "coconut"))] + fn default_backup_bandwidth_token_keys_dir() -> PathBuf { + Config::default_data_directory(None).join("backup_bandwidth_token_keys_dir") + } +} + impl Default for NetworkMonitor { fn default() -> Self { NetworkMonitor { @@ -172,6 +196,12 @@ impl Default for NetworkMonitor { gateway_response_timeout: DEFAULT_GATEWAY_RESPONSE_TIMEOUT, gateway_connection_timeout: DEFAULT_GATEWAY_CONNECTION_TIMEOUT, packet_delivery_timeout: DEFAULT_PACKET_DELIVERY_TIMEOUT, + #[cfg(not(feature = "coconut"))] + backup_bandwidth_token_keys_dir: Self::default_backup_bandwidth_token_keys_dir(), + #[cfg(not(feature = "coconut"))] + eth_private_key: "".to_string(), + #[cfg(not(feature = "coconut"))] + eth_endpoint: "".to_string(), test_routes: DEFAULT_TEST_ROUTES, minimum_test_routes: DEFAULT_MINIMUM_TEST_ROUTES, route_test_packets: DEFAULT_ROUTE_TEST_PACKETS, @@ -312,10 +342,37 @@ impl Config { self } + #[cfg(not(feature = "coconut"))] + pub fn with_eth_private_key(mut self, eth_private_key: String) -> Self { + self.network_monitor.eth_private_key = eth_private_key; + self + } + + #[cfg(not(feature = "coconut"))] + pub fn with_eth_endpoint(mut self, eth_endpoint: String) -> Self { + self.network_monitor.eth_endpoint = eth_endpoint; + self + } + pub fn get_network_monitor_enabled(&self) -> bool { self.network_monitor.enabled } + #[cfg(not(feature = "coconut"))] + pub fn get_backup_bandwidth_token_keys_dir(&self) -> PathBuf { + self.network_monitor.backup_bandwidth_token_keys_dir.clone() + } + + #[cfg(not(feature = "coconut"))] + pub fn get_network_monitor_eth_private_key(&self) -> String { + self.network_monitor.eth_private_key.clone() + } + + #[cfg(not(feature = "coconut"))] + pub fn get_network_monitor_eth_endpoint(&self) -> String { + self.network_monitor.eth_endpoint.clone() + } + pub fn get_rewarding_enabled(&self) -> bool { self.rewarding.enabled } diff --git a/validator-api/src/config/template.rs b/validator-api/src/config/template.rs index 59d61c1e90..96cae891bc 100644 --- a/validator-api/src/config/template.rs +++ b/validator-api/src/config/template.rs @@ -58,6 +58,17 @@ gateway_connection_timeout = '{{ network_monitor.gateway_connection_timeout }}' # packets before declaring nodes unreachable. packet_delivery_timeout = '{{ network_monitor.packet_delivery_timeout }}' +# Path to directory containing public/private keys used for bandwidth token purchase. +# Those are saved in case of emergency, to be able to reclaim bandwidth tokens. +# The public key is the name of the file, while the private key is the content. +backup_bandwidth_token_keys_dir = '{{ client.backup_bandwidth_token_keys_dir }}' + +# Ethereum private key. +eth_private_key = '{{ network_monitor.eth_private_key }}' + +# Addess to an Ethereum full node. +eth_endpoint = '{{ network_monitor.eth_endpoint }}' + # Desired number of test routes to be constructed (and working) during a monitor test run. test_routes = {{ network_monitor.test_routes }} diff --git a/validator-api/src/main.rs b/validator-api/src/main.rs index e31d8ff115..a4812a8d6f 100644 --- a/validator-api/src/main.rs +++ b/validator-api/src/main.rs @@ -58,6 +58,11 @@ const KEYPAIR_ARG: &str = "keypair"; #[cfg(feature = "coconut")] const COCONUT_ONLY_FLAG: &str = "coconut-only"; +#[cfg(not(feature = "coconut"))] +const ETH_ENDPOINT: &str = "eth_endpoint"; +#[cfg(not(feature = "coconut"))] +const ETH_PRIVATE_KEY: &str = "eth_private_key"; + const EPOCH_LENGTH_ARG: &str = "epoch-length"; const FIRST_REWARDING_EPOCH_ARG: &str = "first-epoch"; const REWARDING_MONITOR_THRESHOLD_ARG: &str = "monitor-threshold"; @@ -74,6 +79,11 @@ fn parse_validators(raw: &str) -> Vec { } fn parse_args<'a>() -> ArgMatches<'a> { + #[cfg(feature = "coconut")] + let monitor_reqs = &[]; + #[cfg(not(feature = "coconut"))] + let monitor_reqs = &[ETH_ENDPOINT, ETH_PRIVATE_KEY]; + let base_app = App::new("Nym Validator API") .author("Nymtech") .arg( @@ -81,6 +91,7 @@ fn parse_args<'a>() -> ArgMatches<'a> { .help("specifies whether a network monitoring is enabled on this API") .long(MONITORING_ENABLED) .short("m") + .requires_all(monitor_reqs) ) .arg( Arg::with_name(REWARDING_ENABLED) @@ -152,6 +163,19 @@ fn parse_args<'a>() -> ArgMatches<'a> { .long(COCONUT_ONLY_FLAG), ); + #[cfg(not(feature = "coconut"))] + let base_app = base_app.arg( + Arg::with_name(ETH_ENDPOINT) + .help("URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20 tokens") + .takes_value(true) + .long(ETH_ENDPOINT), + ).arg( + Arg::with_name(ETH_PRIVATE_KEY) + .help("Ethereum private key used for obtaining bandwidth tokens from ERC20 tokens") + .takes_value(true) + .long(ETH_PRIVATE_KEY), + ); + base_app.get_matches() } @@ -254,6 +278,16 @@ fn override_config(mut config: Config, matches: &ArgMatches) -> Config { config = config.with_keypair(keypair_bs58) } + #[cfg(not(feature = "coconut"))] + if let Some(eth_private_key) = matches.value_of("eth_private_key") { + config = config.with_eth_private_key(String::from(eth_private_key)); + } + + #[cfg(not(feature = "coconut"))] + if let Some(eth_endpoint) = matches.value_of("eth_endpoint") { + config = config.with_eth_endpoint(String::from(eth_endpoint)); + } + if matches.is_present(WRITE_CONFIG_ARG) { info!("Saving the configuration to a file"); if let Err(err) = config.save_to_file(None) { diff --git a/validator-api/src/network_monitor/chunker.rs b/validator-api/src/network_monitor/chunker.rs index a5732600f0..c95f8421f5 100644 --- a/validator-api/src/network_monitor/chunker.rs +++ b/validator-api/src/network_monitor/chunker.rs @@ -5,7 +5,7 @@ use nymsphinx::forwarding::packet::MixPacket; use nymsphinx::{ acknowledgements::AckKey, addressing::clients::Recipient, preparer::MessagePreparer, }; -use rand::rngs::OsRng; +use rand_07::rngs::OsRng; use std::time::Duration; use topology::NymTopology; diff --git a/validator-api/src/network_monitor/mod.rs b/validator-api/src/network_monitor/mod.rs index e8d0437058..730af4ee6f 100644 --- a/validator-api/src/network_monitor/mod.rs +++ b/validator-api/src/network_monitor/mod.rs @@ -18,10 +18,7 @@ use crypto::asymmetric::{encryption, identity}; use futures::channel::mpsc; use std::sync::Arc; -#[cfg(feature = "coconut")] -use coconut_interface::Credential; -#[cfg(feature = "coconut")] -use credentials::{bandwidth::prepare_for_spending, obtain_aggregate_verification_key}; +use gateway_client::bandwidth::BandwidthController; pub(crate) mod chunker; pub(crate) mod gateways_reader; @@ -57,7 +54,7 @@ impl<'a> NetworkMonitorBuilder<'a> { // TODO: those keys change constant throughout the whole execution of the monitor. // and on top of that, they are used with ALL the gateways -> presumably this should change // in the future - let mut rng = rand::rngs::OsRng; + let mut rng = rand_07::rngs::OsRng; let identity_keypair = Arc::new(identity::KeyPair::new(&mut rng)); let encryption_keypair = Arc::new(encryption::KeyPair::new(&mut rng)); @@ -75,16 +72,24 @@ impl<'a> NetworkMonitorBuilder<'a> { ); #[cfg(feature = "coconut")] - let bandwidth_credential = - TEMPORARY_obtain_bandwidth_credential(self.config, identity_keypair.public_key()).await; + let bandwidth_controller = BandwidthController::new( + self.config.get_all_validator_api_endpoints(), + *identity_keypair.public_key(), + ); + #[cfg(not(feature = "coconut"))] + let bandwidth_controller = BandwidthController::new( + self.config.get_network_monitor_eth_endpoint(), + self.config.get_network_monitor_eth_private_key(), + self.config.get_backup_bandwidth_token_keys_dir(), + ) + .expect("Could not create bandwidth controller"); let packet_sender = new_packet_sender( self.config, gateway_status_update_sender, Arc::clone(&identity_keypair), self.config.get_gateway_sending_rate(), - #[cfg(feature = "coconut")] - bandwidth_credential, + bandwidth_controller, ); let received_processor = new_received_processor( @@ -146,40 +151,12 @@ fn new_packet_preparer( ) } -// SECURITY: -// this implies we are re-using the same credential for all gateways all the time (which unfortunately is true!) -#[cfg(feature = "coconut")] -#[allow(non_snake_case)] -async fn TEMPORARY_obtain_bandwidth_credential( - config: &Config, - identity: &identity::PublicKey, -) -> Credential { - info!("Trying to obtain bandwidth credential..."); - let validators = config.get_all_validator_api_endpoints(); - - let verification_key = obtain_aggregate_verification_key(&validators) - .await - .expect("could not obtain aggregate verification key of ALL validators"); - - let bandwidth_credential = - credentials::bandwidth::obtain_signature(&identity.to_bytes(), &validators) - .await - .expect("failed to obtain bandwidth credential!"); - - prepare_for_spending( - &identity.to_bytes(), - &bandwidth_credential, - &verification_key, - ) - .expect("failed to prepare bandwidth credential for spending!") -} - fn new_packet_sender( config: &Config, gateways_status_updater: GatewayClientUpdateSender, local_identity: Arc, max_sending_rate: usize, - #[cfg(feature = "coconut")] bandwidth_credential: Credential, + bandwidth_controller: BandwidthController, ) -> PacketSender { PacketSender::new( gateways_status_updater, @@ -188,8 +165,7 @@ fn new_packet_sender( config.get_gateway_connection_timeout(), config.get_max_concurrent_gateway_clients(), max_sending_rate, - #[cfg(feature = "coconut")] - bandwidth_credential, + bandwidth_controller, ) } diff --git a/validator-api/src/network_monitor/monitor/preparer.rs b/validator-api/src/network_monitor/monitor/preparer.rs index 7a2f838055..53c1eabe0e 100644 --- a/validator-api/src/network_monitor/monitor/preparer.rs +++ b/validator-api/src/network_monitor/monitor/preparer.rs @@ -185,32 +185,30 @@ impl PacketPreparer { let initialisation_backoff = Duration::from_secs(30); loop { let gateways = self.validator_cache.gateways().await; - let mixnodes = self.validator_cache.active_mixnodes().await; + let mixnodes = self.validator_cache.rewarded_mixnodes().await; - if let Some(mixnodes) = mixnodes { - if gateways.into_inner().len() < minimum_full_routes { - continue; - } + if gateways.into_inner().len() < minimum_full_routes { + continue; + } - let mut layered_mixes = HashMap::new(); - for mix in mixnodes.into_inner() { - let layer = mix.layer; - let mixes = layered_mixes.entry(layer).or_insert_with(Vec::new); - mixes.push(mix) - } + let mut layered_mixes = HashMap::new(); + for mix in mixnodes.into_inner() { + let layer = mix.layer; + let mixes = layered_mixes.entry(layer).or_insert_with(Vec::new); + mixes.push(mix) + } - // we remove the entries as this gives us the ownership and thus we can unwrap to default value - // which makes the code slightly nicer without having to deal with options - let layer1 = layered_mixes.remove(&Layer::One).unwrap_or_default(); - let layer2 = layered_mixes.remove(&Layer::Two).unwrap_or_default(); - let layer3 = layered_mixes.remove(&Layer::Three).unwrap_or_default(); + // we remove the entries as this gives us the ownership and thus we can unwrap to default value + // which makes the code slightly nicer without having to deal with options + let layer1 = layered_mixes.remove(&Layer::One).unwrap_or_default(); + let layer2 = layered_mixes.remove(&Layer::Two).unwrap_or_default(); + let layer3 = layered_mixes.remove(&Layer::Three).unwrap_or_default(); - if layer1.len() >= minimum_full_routes - && layer2.len() >= minimum_full_routes - && layer3.len() >= minimum_full_routes - { - break; - } + if layer1.len() >= minimum_full_routes + && layer2.len() >= minimum_full_routes + && layer3.len() >= minimum_full_routes + { + break; } info!( @@ -221,8 +219,10 @@ impl PacketPreparer { } } - async fn get_network_nodes(&self) -> (Vec, Vec) { - let mixnodes = self.validator_cache.mixnodes().await.into_inner(); + async fn get_rewarded_nodes(&self) -> (Vec, Vec) { + info!(target: "Monitor", "Obtaining network topology..."); + + let mixnodes = self.validator_cache.rewarded_mixnodes().await.into_inner(); let gateways = self.validator_cache.gateways().await.into_inner(); (mixnodes, gateways) @@ -247,7 +247,7 @@ impl PacketPreparer { gateway.try_into().map_err(|_| identity) } - // gets active nodes + // gets rewarded nodes // chooses n random nodes from each layer (and gateway) such that they are not on the blacklist // if failed to parsed => onto the blacklist they go // if generated fewer than n, blacklist will be updated by external function with correctly generated @@ -257,19 +257,19 @@ impl PacketPreparer { n: usize, blacklist: &mut HashSet, ) -> Option> { - let active_mixnodes = self.validator_cache.active_mixnodes().await?.into_inner(); + let rewarded_mixnodes = self.validator_cache.rewarded_mixnodes().await.into_inner(); let gateways = self.validator_cache.gateways().await.into_inner(); // separate mixes into layers for easier selection let mut layered_mixes = HashMap::new(); - for active_mix in active_mixnodes { + for rewarded_mix in rewarded_mixnodes { // filter out mixes on the blacklist - if blacklist.contains(&active_mix.mix_node.identity_key) { + if blacklist.contains(&rewarded_mix.mix_node.identity_key) { continue; } - let layer = active_mix.layer; + let layer = rewarded_mix.layer; let mixes = layered_mixes.entry(layer).or_insert_with(Vec::new); - mixes.push(active_mix) + mixes.push(rewarded_mix) } // filter out gateways on the blacklist let gateways = gateways @@ -445,11 +445,16 @@ impl PacketPreparer { test_nonce: u64, test_routes: &[TestRoute], ) -> PreparedPackets { - let (mixnode_bonds, gateway_bonds) = self.get_network_nodes().await; + // only test mixnodes that are rewarded, i.e. that will be rewarded in this epoch. + // (remember that "idle" nodes are still part of that set) + // we don't care about other nodes, i.e. nodes that are bonded but will not get + // any reward during the current rewarding interval + let (rewarded_mixnodes, all_gateways) = self.get_rewarded_nodes().await; - let (mixes, invalid_mixnodes) = self.filter_outdated_and_malformed_mixnodes(mixnode_bonds); + let (mixes, invalid_mixnodes) = + self.filter_outdated_and_malformed_mixnodes(rewarded_mixnodes); let (gateways, invalid_gateways) = - self.filter_outdated_and_malformed_gateways(gateway_bonds); + self.filter_outdated_and_malformed_gateways(all_gateways); let tested_mixnodes = mixes.iter().map(|node| node.into()).collect::>(); let tested_gateways = gateways.iter().map(|node| node.into()).collect::>(); diff --git a/validator-api/src/network_monitor/monitor/sender.rs b/validator-api/src/network_monitor/monitor/sender.rs index 0bdd116898..1024f29845 100644 --- a/validator-api/src/network_monitor/monitor/sender.rs +++ b/validator-api/src/network_monitor/monitor/sender.rs @@ -23,13 +23,11 @@ use std::sync::Arc; use std::task::Poll; use std::time::Duration; -#[cfg(feature = "coconut")] -use coconut_interface::Credential; +use gateway_client::bandwidth::BandwidthController; const TIME_CHUNK_SIZE: Duration = Duration::from_millis(50); // If we're below 10MB of bandwidth, claim some more -#[cfg(feature = "coconut")] const REMAINING_BANDWIDTH_THRESHOLD: i64 = 10 * 1000 * 1000; pub(crate) struct GatewayPackets { @@ -88,10 +86,10 @@ struct FreshGatewayClientData { // TODO: // SECURITY: - // since currently we have no double spending protection, just to get things running - // we're re-using the same credential for all gateways all the time. THIS IS VERY BAD!! - #[cfg(feature = "coconut")] - coconut_bandwidth_credential: Credential, + // for coconut bandwidth credentials we currently have no double spending protection, just to + // get things running we're re-using the same credential for all gateways all the time. + // THIS IS VERY BAD!! + bandwidth_controller: BandwidthController, } impl FreshGatewayClientData { @@ -147,7 +145,7 @@ impl PacketSender { gateway_connection_timeout: Duration, max_concurrent_clients: usize, max_sending_rate: usize, - #[cfg(feature = "coconut")] coconut_bandwidth_credential: Credential, + bandwidth_controller: BandwidthController, ) -> Self { PacketSender { active_gateway_clients: ActiveGatewayClients::new(), @@ -155,8 +153,7 @@ impl PacketSender { gateways_status_updater, local_identity, gateway_response_timeout, - #[cfg(feature = "coconut")] - coconut_bandwidth_credential, + bandwidth_controller, }), gateway_connection_timeout, max_concurrent_clients, @@ -200,6 +197,7 @@ impl PacketSender { message_sender, ack_sender, fresh_gateway_client_data.gateway_response_timeout, + Some(fresh_gateway_client_data.bandwidth_controller.clone()), )), (message_receiver, ack_receiver), ) @@ -288,14 +286,7 @@ impl PacketSender { let mut unlocked_client = new_client.lock_client_unchecked(); match tokio::time::timeout( gateway_connection_timeout, - unlocked_client.get_mut_unchecked().authenticate_and_start( - #[cfg(feature = "coconut")] - Some( - fresh_gateway_client_data - .coconut_bandwidth_credential - .clone(), - ), - ), + unlocked_client.get_mut_unchecked().authenticate_and_start(), ) .await { @@ -322,26 +313,15 @@ impl PacketSender { } } - #[cfg(feature = "coconut")] async fn check_remaining_bandwidth( client: &mut GatewayClient, - fresh_gateway_client_data: &FreshGatewayClientData, ) -> Result<(), GatewayClientError> { if client.remaining_bandwidth() < REMAINING_BANDWIDTH_THRESHOLD { - // TODO: SECURITY: - // We're using exactly the same credential again because we have no double - // spending protection... info!( "Client to gateway {} is running out of bandwidth... Claiming some more...", client.gateway_identity().to_base58_string() ); - client - .claim_coconut_bandwidth( - fresh_gateway_client_data - .coconut_bandwidth_credential - .clone(), - ) - .await + client.claim_bandwidth().await } else { Ok(()) } @@ -387,10 +367,7 @@ impl PacketSender { let mut guard = client.lock_client().await; let unwrapped_client = guard.get_mut_unchecked(); - #[cfg(feature = "coconut")] - if let Err(err) = - Self::check_remaining_bandwidth(unwrapped_client, &fresh_gateway_client_data).await - { + if let Err(err) = Self::check_remaining_bandwidth(unwrapped_client).await { warn!( "Failed to claim additional bandwidth for {} - {}", unwrapped_client.gateway_identity().to_base58_string(), diff --git a/validator-api/src/network_monitor/test_packet.rs b/validator-api/src/network_monitor/test_packet.rs index 4852c2f2e7..79c2971674 100644 --- a/validator-api/src/network_monitor/test_packet.rs +++ b/validator-api/src/network_monitor/test_packet.rs @@ -188,11 +188,10 @@ impl From for TestedNode { #[cfg(test)] mod tests { use super::*; - use rand::thread_rng; #[test] fn test_packet_roundtrip() { - let mut rng = thread_rng(); + let mut rng = rand_07::thread_rng(); let dummy_keypair = identity::KeyPair::new(&mut rng); let owner = "some owner".to_string(); let packet = TestPacket::new( diff --git a/validator-api/src/nymd_client.rs b/validator-api/src/nymd_client.rs index 38dbd1e488..3ada44491a 100644 --- a/validator-api/src/nymd_client.rs +++ b/validator-api/src/nymd_client.rs @@ -7,12 +7,16 @@ use crate::rewarding::{ PER_MIXNODE_DELEGATION_GAS_INCREASE, REWARDING_GAS_LIMIT_MULTIPLIER, }; use config::defaults::DEFAULT_VALIDATOR_API_PORT; -use mixnet_contract::{Delegation, ExecuteMsg, GatewayBond, IdentityKey, MixNodeBond, StateParams}; +use mixnet_contract::{ + Delegation, ExecuteMsg, GatewayBond, IdentityKey, MixNodeBond, RewardingIntervalResponse, + StateParams, +}; use std::sync::Arc; use std::time::Duration; use tokio::sync::RwLock; use tokio::time::sleep; use validator_client::nymd::{ + hash::{Hash, SHA256_HASH_SIZE}, CosmWasmClient, Fee, QueryNymdClient, SigningCosmWasmClient, SigningNymdClient, TendermintTime, }; use validator_client::ValidatorClientError; @@ -93,6 +97,34 @@ impl Client { Ok(time) } + pub(crate) async fn get_reward_pool(&self) -> Result + where + C: CosmWasmClient + Sync, + { + Ok(self.0.read().await.get_reward_pool().await?) + } + + pub(crate) async fn get_circulating_supply(&self) -> Result + where + C: CosmWasmClient + Sync, + { + Ok(self.0.read().await.get_circulating_supply().await?) + } + + pub(crate) async fn get_sybil_resistance_percent(&self) -> Result + where + C: CosmWasmClient + Sync, + { + Ok(self.0.read().await.get_sybil_resistance_percent().await?) + } + + pub(crate) async fn get_epoch_reward_percent(&self) -> Result + where + C: CosmWasmClient + Sync, + { + Ok(self.0.read().await.get_epoch_reward_percent().await?) + } + pub(crate) async fn get_mixnodes(&self) -> Result, ValidatorClientError> where C: CosmWasmClient + Sync, @@ -114,6 +146,36 @@ impl Client { self.0.read().await.get_state_params().await } + pub(crate) async fn get_current_rewarding_interval( + &self, + ) -> Result + where + C: CosmWasmClient + Sync, + { + self.0.read().await.get_current_rewarding_interval().await + } + + /// Obtains the hash of a block specified by the provided height. + /// If the resulting digest is empty, a `None` is returned instead. + /// + /// # Arguments + /// + /// * `height`: height of the block for which we want to obtain the hash. + pub async fn get_block_hash( + &self, + height: u32, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync, + { + let hash = match self.0.read().await.nymd.get_block_hash(height).await? { + Hash::Sha256(hash) => Some(hash), + Hash::None => None, + }; + + Ok(hash) + } + pub(crate) async fn get_mixnode_delegations( &self, identity: IdentityKey, @@ -141,9 +203,42 @@ impl Client { .calculate_custom_fee(total_gas_limit) } + pub(crate) async fn begin_mixnode_rewarding( + &self, + rewarding_interval_nonce: u32, + ) -> Result<(), RewardingError> + where + C: SigningCosmWasmClient + Sync, + { + self.0 + .write() + .await + .nymd + .begin_mixnode_rewarding(rewarding_interval_nonce) + .await?; + Ok(()) + } + + pub(crate) async fn finish_mixnode_rewarding( + &self, + rewarding_interval_nonce: u32, + ) -> Result<(), RewardingError> + where + C: SigningCosmWasmClient + Sync, + { + self.0 + .write() + .await + .nymd + .finish_mixnode_rewarding(rewarding_interval_nonce) + .await?; + Ok(()) + } + pub(crate) async fn reward_mixnodes( &self, nodes: &[MixnodeToReward], + rewarding_interval_nonce: u32, ) -> Result<(), RewardingError> where C: SigningCosmWasmClient + Sync, @@ -154,7 +249,7 @@ impl Client { .await; let msgs: Vec<(ExecuteMsg, _)> = nodes .iter() - .map(Into::into) + .map(|node| node.to_execute_msg(rewarding_interval_nonce)) .zip(std::iter::repeat(Vec::new())) .collect(); diff --git a/validator-api/src/rewarding/mod.rs b/validator-api/src/rewarding/mod.rs index e1bac6fe73..f75bbb8ac5 100644 --- a/validator-api/src/rewarding/mod.rs +++ b/validator-api/src/rewarding/mod.rs @@ -12,6 +12,7 @@ use crate::storage::models::{ }; use crate::storage::ValidatorApiStorage; use log::{error, info}; +use mixnet_contract::mixnode::NodeRewardParams; use mixnet_contract::{ExecuteMsg, IdentityKey}; use std::collections::HashMap; use std::convert::TryInto; @@ -48,6 +49,41 @@ pub(crate) struct MixnodeToReward { /// Total number of individual addresses that have delegated to this particular node pub(crate) total_delegations: usize, + /// Node absolute uptime over total active set uptime + params: Option, +} + +impl MixnodeToReward { + /// Somewhat clumsy way of feature gatting tokenomics payments. In a tokenomics scenario this will never be None at reward time. We levarage that to Into a different ExecuteMsg variant + // TODO: to re-integrate in another PR that combines rewarded/active sets with tokenomics + #[allow(dead_code)] + fn params(&self) -> Option { + if cfg!(feature = "tokenomics") { + self.params + } else { + None + } + } +} + +impl MixnodeToReward { + pub(crate) fn to_execute_msg(&self, rewarding_interval_nonce: u32) -> ExecuteMsg { + ExecuteMsg::RewardMixnode { + identity: self.identity.clone(), + uptime: self.uptime.u8() as u32, + rewarding_interval_nonce, + } + } + + // TODO: to re-integrate in another PR that combines rewarded/active sets with tokenomics + #[allow(dead_code)] + pub(crate) fn to_execute_msg_v2(&self, rewarding_interval_nonce: u32) -> ExecuteMsg { + ExecuteMsg::RewardMixnodeV2 { + identity: self.identity.clone(), + params: self.params().unwrap(), + rewarding_interval_nonce, + } + } } pub(crate) struct FailedMixnodeRewardChunkDetails { @@ -60,15 +96,6 @@ pub(crate) struct FailureData { mixnodes: Option>, } -impl<'a> From<&'a MixnodeToReward> for ExecuteMsg { - fn from(node: &MixnodeToReward) -> Self { - ExecuteMsg::RewardMixnode { - identity: node.identity.clone(), - uptime: node.uptime.u8() as u32, - } - } -} - pub(crate) struct Rewarder { nymd_client: Client, validator_cache: ValidatorCache, @@ -143,12 +170,7 @@ impl Rewarder { // instantaneous. let mut map = HashMap::new(); - let active_bonded_mixnodes = self - .validator_cache - .active_mixnodes() - .await - .ok_or(RewardingError::NoMixnodesToReward)? - .into_inner(); + let active_bonded_mixnodes = self.validator_cache.active_mixnodes().await.into_inner(); for mix in active_bonded_mixnodes.into_iter() { let delegator_count = self .get_mixnode_delegators_count(mix.mix_node.identity_key.clone()) @@ -185,12 +207,13 @@ impl Rewarder { // by people hesitating to delegate to nodes without them and thus those nodes disappearing // from the active set (once introduced) let mixnode_delegators = self.produce_active_mixnode_delegators_map().await?; + let state = self.nymd_client.get_state_params().await?; // 1. go through all active mixnodes // 2. filter out nodes that are currently not in the active set (as `mixnode_delegators` was obtained by // querying the validator) // 3. determine uptime and attach delegators count - let eligible_nodes = active_mixnodes + let mut eligible_nodes: Vec = active_mixnodes .iter() .filter_map(|mix| { mixnode_delegators @@ -199,11 +222,39 @@ impl Rewarder { identity: mix.identity.clone(), uptime: mix.last_day, total_delegations, + params: None, }) }) .filter(|node| node.uptime.u8() > 0) .collect(); + if cfg!(feature = "tokenomics") { + let reward_pool = self.nymd_client.get_reward_pool().await?; + let circulating_supply = self.nymd_client.get_circulating_supply().await?; + let sybil_resistance_percent = self.nymd_client.get_sybil_resistance_percent().await?; + let epoch_reward_percent = self.nymd_client.get_epoch_reward_percent().await?; + let k = state.mixnode_active_set_size; + let period_reward_pool = (reward_pool / 100) * epoch_reward_percent as u128; + + info!("Rewarding pool stats"); + info!("-- Reward pool: {} unym", reward_pool); + info!("---- Epoch reward pool: {} unym", period_reward_pool); + info!("-- Circulating supply: {} unym", circulating_supply); + + for mix in eligible_nodes.iter_mut() { + mix.params = Some(NodeRewardParams::new( + period_reward_pool, + k.into(), + 0, + circulating_supply, + mix.uptime.u8().into(), + sybil_resistance_percent, + )); + } + } else { + info!("Tokenomics feature is OFF"); + } + Ok(eligible_nodes) } @@ -240,14 +291,20 @@ impl Rewarder { /// # Arguments /// /// * `eligible_mixnodes`: list of the nodes that are eligible to receive non-zero rewards. + /// * `rewarding_interval_nonce`: nonce associated with the current rewarding interval async fn distribute_rewards_to_mixnodes( &self, eligible_mixnodes: &[MixnodeToReward], + rewarding_interval_nonce: u32, ) -> Option> { let mut failed_chunks = Vec::new(); for (i, mix_chunk) in eligible_mixnodes.chunks(MAX_TO_REWARD_AT_ONCE).enumerate() { - if let Err(err) = self.nymd_client.reward_mixnodes(mix_chunk).await { + if let Err(err) = self + .nymd_client + .reward_mixnodes(mix_chunk, rewarding_interval_nonce) + .await + { // this is a super weird edge case that we didn't catch change to sequence and // resent rewards unnecessarily, but the mempool saved us from executing it again // however, still we want to wait until we're sure we're into the next block @@ -282,13 +339,13 @@ impl Rewarder { /// /// # Arguments /// - /// * `epoch_rewarding_id`: id of the current epoch rewarding. + /// * `epoch_rewarding_id`: id of the current epoch rewarding as stored in the databse. /// /// * `active_monitor_mixnodes`: list of the nodes that were tested at least once by the network monitor /// in the last epoch. async fn distribute_rewards( &self, - epoch_rewarding_id: i64, + epoch_rewarding_database_id: i64, active_monitor_mixnodes: &[MixnodeStatusReport], ) -> Result<(RewardingReport, Option), RewardingError> { let mut failure_data = FailureData::default(); @@ -300,12 +357,20 @@ impl Rewarder { return Err(RewardingError::NoMixnodesToReward); } + let current_rewarding_nonce = self + .nymd_client + .get_current_rewarding_interval() + .await? + .current_rewarding_interval_nonce; + self.nymd_client + .begin_mixnode_rewarding(current_rewarding_nonce + 1) + .await?; failure_data.mixnodes = self - .distribute_rewards_to_mixnodes(&eligible_mixnodes) + .distribute_rewards_to_mixnodes(&eligible_mixnodes, current_rewarding_nonce + 1) .await; let report = RewardingReport { - epoch_rewarding_id, + epoch_rewarding_id: epoch_rewarding_database_id, eligible_mixnodes: eligible_mixnodes.len() as i64, possibly_unrewarded_mixnodes: failure_data .mixnodes @@ -319,6 +384,10 @@ impl Rewarder { .unwrap_or_default(), }; + self.nymd_client + .finish_mixnode_rewarding(current_rewarding_nonce + 1) + .await?; + if failure_data.mixnodes.is_none() { Ok((report, None)) } else {