diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3ae81652a4a..68cf02d4c7f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -65,78 +65,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 - uses: actions-rs/cargo@v1 - with: - command: test - args: --bin nym-gateway --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 + - name: Run all tests with coconut enabled uses: actions-rs/cargo@v1 with: command: test - args: --bin nym-validator-api --features=coconut - -# CLIPPY - - - name: Run clippy on gateway with coconut feature - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --bin nym-gateway --features=coconut -- -D warnings + args: --all --features=coconut - - 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 + - name: Run clippy with coconut enabled uses: actions-rs/cargo@v1 + if: ${{ matrix.rust != 'nightly' }} 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/erc20_bridge_contract.yml b/.github/workflows/erc20_bridge_contract.yml new file mode 100644 index 00000000000..8d5d981957b --- /dev/null +++ b/.github/workflows/erc20_bridge_contract.yml @@ -0,0 +1,46 @@ +name: ERC20 Bridge Contract + +on: [push, pull_request] + +jobs: + erc20-bridge-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' }} + strategy: + matrix: + rust: [ stable, beta, 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 611decb81b7..dd95a5a6927 100644 --- a/.github/workflows/mixnet_contract.yml +++ b/.github/workflows/mixnet_contract.yml @@ -29,6 +29,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/wasm_client_build.yml b/.github/workflows/wasm_client_build.yml index b9d40cb65ba..f59cb754426 100644 --- a/.github/workflows/wasm_client_build.yml +++ b/.github/workflows/wasm_client_build.yml @@ -22,10 +22,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 +48,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/Cargo.lock b/Cargo.lock index ee1f3697247..c6f3729670c 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" @@ -302,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" @@ -321,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", @@ -411,6 +429,12 @@ 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" @@ -955,6 +979,8 @@ name = "credentials" version = "0.1.0" dependencies = [ "coconut-interface", + "crypto", + "network-defaults", "thiserror", "url", "validator-client", @@ -1550,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" @@ -1559,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" @@ -1666,6 +1743,18 @@ dependencies = [ "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" @@ -1762,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" @@ -1871,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" @@ -1906,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]] @@ -2382,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" @@ -2613,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" @@ -2771,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" @@ -3142,6 +3315,7 @@ checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d" name = "network-defaults" version = "0.1.0" dependencies = [ + "hex-literal", "serde", "time 0.3.3", "url", @@ -3334,6 +3508,7 @@ dependencies = [ name = "nym-gateway" version = "0.11.0" dependencies = [ + "bip39", "clap", "coconut-interface", "config", @@ -3342,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", @@ -3362,6 +3540,7 @@ dependencies = [ "url", "validator-client", "version-checker", + "web3", ] [[package]] @@ -3748,6 +3927,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" @@ -4114,6 +4319,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" @@ -4284,6 +4502,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" @@ -4683,6 +4907,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" @@ -4831,6 +5065,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" @@ -4961,6 +5201,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" @@ -5327,6 +5585,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" @@ -5926,6 +6199,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" @@ -6318,6 +6597,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" @@ -6415,6 +6703,7 @@ checksum = "08d3725d3efa29485e87311c5b699de63cde14b00ed4d256b8318aa30ca452cd" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "log", "pin-project-lite", @@ -6886,6 +7175,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" @@ -7096,6 +7431,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 8d959736a0d..b1e1a1c7f9e 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", diff --git a/clients/client-core/Cargo.toml b/clients/client-core/Cargo.toml index 3d13ae82347..73078fbfdba 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 6f286104942..7a91bc345c7 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 66b33d7e3ce..22ddb22f91e 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 0cdde68aa30..49b8de71ff5 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 1994fe216e7..a1e4aaeea9c 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 475f14a9b6e..b7ebad71fbf 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 d08f4ea764f..9b46ec7bbc8 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 70e196d3c17..775c97ddf4d 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 3882694f207..c6e187145ae 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 f24dc533227..14bcbda8c63 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 7b6ff6c4b5f..405b445c505 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 facd85144fb..2ddf728d3aa 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 ed1397dc437..2ac0e8ef1c4 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 247352ac319..844191d8b6d 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 00000000000..12a68e6429f --- /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 487280157e3..1ec592fb0f8 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,12 +561,16 @@ impl GatewayClient { if !self.authenticated { return Err(GatewayClientError::NotAuthenticated); } - #[cfg(feature = "coconut")] - if self.bandwidth_remaining <= self.estimate_required_bandwidth(&packets) { - return Err(GatewayClientError::NotEnoughBandwidth(( - self.estimate_required_bandwidth(&packets), - self.bandwidth_remaining, - ))); + 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); @@ -593,12 +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(( - (mix_packet.sphinx_packet().len() as i64), - self.bandwidth_remaining, - ))); + // 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); @@ -635,13 +682,6 @@ impl GatewayClient { if !self.authenticated { return Err(GatewayClientError::NotAuthenticated); } - #[cfg(feature = "coconut")] - if self.bandwidth_remaining <= 0 { - return Err(GatewayClientError::NotEnoughBandwidth(( - 0, - self.bandwidth_remaining, - ))); - } if self.connection.is_partially_delegated() { return Ok(()); } @@ -669,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 90cd77300da..f396b3e06bb 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), + #[cfg(not(feature = "coconut"))] + #[error("Could not backup keypair - {0}")] + IOError(#[from] std::io::Error), + + #[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, - NotEnoughBandwidth((i64, i64)), + + #[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, - Timeout, -} -impl From for GatewayClientError { - fn from(err: WsError) -> Self { - GatewayClientError::NetworkError(err) - } + #[error("Timed out")] + Timeout, } impl GatewayClientError { @@ -54,64 +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((estimated, remaining)) => { - write!( - f, - "Client does not have enough bandwidth: estimated {}, remaining: {}", - estimated, remaining - ) - } - 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 f1f0987392d..dc7b17d8b42 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/validator-client/src/nymd/mod.rs b/common/client-libs/validator-client/src/nymd/mod.rs index 7fc16858496..cacd7f15a89 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -30,7 +30,7 @@ pub use cosmrs::tendermint::block::Height; 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; diff --git a/common/credentials/Cargo.toml b/common/credentials/Cargo.toml index 273d0998987..8536669e009 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 5f3c898526e..39364cf0184 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 00000000000..bf480ad58aa --- /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 099f3480791..436f7882610 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 00000000000..79ed4cc9aab --- /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 00000000000..ac23bbe0016 --- /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 00000000000..46cb8f4809e --- /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 00000000000..a4757918e2f --- /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 00000000000..f878fe6526a --- /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 00000000000..bc0bedcc6b4 --- /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 00000000000..fe2126cdb0a --- /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/network-defaults/Cargo.toml b/common/network-defaults/Cargo.toml index 6cbde555867..ba50e9c473a 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 00000000000..b9d4e5fdddf --- /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 e02b6e738e8..572ccadcec0 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 @@ -63,6 +65,23 @@ pub fn default_api_endpoints() -> Vec { pub const DEFAULT_MIXNET_CONTRACT_ADDRESS: &str = "punk10pyejy66429refv3g35g2t7am0was7yalwrzen"; pub const NETWORK_MONITOR_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"; pub const BECH32_PREFIX: &str = "punk"; diff --git a/contracts/erc20-bridge/Cargo.lock b/contracts/erc20-bridge/Cargo.lock new file mode 100644 index 00000000000..9e51f6ed3e5 --- /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 00000000000..a487dbee612 --- /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 00000000000..02f3523eeb0 --- /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 00000000000..02957f9e378 --- /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 00000000000..66e0c28f4d1 --- /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 00000000000..42682827f1a --- /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 00000000000..3e1ec563d50 --- /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 00000000000..7fb16b8e058 --- /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 00000000000..ec953bc0fa2 --- /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/Cargo.lock b/contracts/mixnet/Cargo.lock index 0f7f85618ab..357afd9beda 100644 --- a/contracts/mixnet/Cargo.lock +++ b/contracts/mixnet/Cargo.lock @@ -557,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" @@ -723,6 +729,7 @@ dependencies = [ name = "network-defaults" version = "0.1.0" dependencies = [ + "hex-literal", "serde", "time", "url", diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 272ee479251..0e9b875b2e4 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 99a8b6cd15a..de5bf953562 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/types.rs b/gateway/gateway-requests/src/types.rs index 015053445ea..249a4d066ec 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 0cd5a840760..a2bed3186dd 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 574602f2c58..3838329caae 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 83519820c0b..2e881f9d099 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 dd952ce8826..fdbcaaac14c 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 81051f9cb54..3205bae00c3 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 e0de947a754..d8d1472b0d4 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 118a2ad4c47..9d50814da9c 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 f623503bff6..b242ebf6102 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 00000000000..1998e9baedf --- /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 5c413d7022c..3ae10063afc 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 435163cc63e..0dda785d38d 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 c55da12c3f2..2d6950ffd5a 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 201a4c8e2e3..0a9bb505783 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 5299f959d3d..e2367b2ee10 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 2af35291e11..a74d53a3898 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/tauri-wallet/Cargo.lock b/tauri-wallet/Cargo.lock index af3408d8766..0360d27c084 100644 --- a/tauri-wallet/Cargo.lock +++ b/tauri-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" @@ -163,7 +176,7 @@ dependencies = [ "k256", "ripemd160", "sha2", - "subtle", + "subtle 2.4.1", "zeroize", ] @@ -192,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" @@ -203,6 +228,7 @@ dependencies = [ "cc", "cfg-if 1.0.0", "constant_time_eq", + "crypto-mac 0.11.1", "digest 0.9.0", "rayon", ] @@ -261,7 +287,7 @@ dependencies = [ "group", "pairing", "rand_core 0.6.3", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -403,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" @@ -414,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" @@ -717,6 +762,8 @@ name = "credentials" version = "0.1.0" dependencies = [ "coconut-interface", + "crypto", + "network-defaults", "thiserror", "url", "validator-client", @@ -772,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" @@ -780,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" @@ -791,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]] @@ -830,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" @@ -839,7 +925,7 @@ dependencies = [ "byteorder", "digest 0.9.0", "rand_core 0.5.1", - "subtle", + "subtle 2.4.1", "zeroize", ] @@ -1085,6 +1171,8 @@ checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ "curve25519-dalek", "ed25519", + "rand 0.7.3", + "serde", "sha2", "zeroize", ] @@ -1121,7 +1209,7 @@ dependencies = [ "group", "pkcs8", "rand_core 0.6.3", - "subtle", + "subtle 2.4.1", "zeroize", ] @@ -1184,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]] @@ -1517,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]] @@ -1687,7 +1777,7 @@ dependencies = [ "byteorder", "ff", "rand_core 0.6.3", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -1840,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" @@ -1854,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", ] @@ -2191,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" @@ -2215,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" @@ -2439,6 +2569,7 @@ checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d" name = "network-defaults" version = "0.1.0" dependencies = [ + "hex-literal", "serde", "time 0.3.3", "url", @@ -2516,6 +2647,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ "autocfg 1.0.1", + "libm", ] [[package]] @@ -2575,6 +2707,13 @@ dependencies = [ "validator-client", ] +[[package]] +name = "nymsphinx-types" +version = "0.1.0" +dependencies = [ + "sphinx", +] + [[package]] name = "objc" version = "0.2.7" @@ -2741,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]] @@ -2771,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" @@ -3233,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" @@ -3889,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" @@ -4005,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" @@ -4443,7 +4639,7 @@ dependencies = [ "serde_repr", "sha2", "signature", - "subtle", + "subtle 2.4.1", "subtle-encoding", "tendermint-proto", "zeroize", @@ -5171,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" @@ -5182,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/validator-api/src/config/mod.rs b/validator-api/src/config/mod.rs index 5b8be3ac003..43029246e44 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; @@ -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 59d61c1e909..96cae891bce 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 e31d8ff1150..a4812a8d6f5 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/mod.rs b/validator-api/src/network_monitor/mod.rs index e8d0437058b..1e1f64d0898 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; @@ -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/sender.rs b/validator-api/src/network_monitor/monitor/sender.rs index 0bdd116898c..1024f298454 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(),