Compare commits

...

31 Commits

Author SHA1 Message Date
Dave Hrycyszyn 922941f2db Artificially locking the client to a specific gateway with the correct HTTPS setup 2022-09-23 17:06:58 +01:00
Dave Hrycyszyn f334acab99 Upgraded all tungstenite components so that wss connections work 2022-09-23 17:06:37 +01:00
Dave Hrycyszyn 001c98e544 Adding in a happy favicon 2022-09-23 17:03:57 +01:00
Dave Hrycyszyn d3eb238d5a Release of 1.0.0 - there was a duff 1.0.1 version published in my account 2022-09-15 16:09:31 +01:00
Mark Sinclair 23f0212a16 Update CHANGELOG 2022-09-02 15:35:06 +01:00
Mark Sinclair cb58a62ff7 wasm-client: fix up dependencies and feature flags so that wasm-pack build works 2022-09-02 15:32:57 +01:00
Mark Sinclair cc8be3bce2 wasm-client: change example to use the mainnet validator API 2022-09-02 15:32:46 +01:00
Jędrzej Stuczyński a2c6abd3dd Made nodes_to_remove field in mixnet MigrateMsg public 2022-09-01 17:10:17 +01:00
Bogdan-Ștefan Neacşu d289c46e87 Feature/nr err report (#1576)
* common/socks5: Use thiserror and add copyright notice

* Send allowlist failure msg back to socks5 client

* Add some serde unit tests

* Fix clippy after rustup update

* Update changelog
2022-08-31 16:27:02 +03:00
Bogdan-Ștefan Neacşu a6aba3defd Feature/validator api shutdown (#1573)
* Rewarded set updater shutdown (partial) handling

* Shutdown handling in monitor

* Remove shutdown from packet receiver

* Configurable shutdown timeout

* Select on test_run too

* Remove unnecessary await/async

* Add bias to shutdown select and concurrency for big tasks

* Put cpu-bound packet prep on separate thread, to avoid blocking

* Use a better fit timeout value

* Fix clippy warnings

* Update changelog

* Fix wasm client
2022-08-30 14:14:50 +03:00
Jędrzej Stuczyński 6557be3738 Delegator compoudning to bypass pending events (#1571)
* Delegator compoudning to bypass pending events

* Updated changelog
2022-08-30 11:56:13 +01:00
Bogdan-Ștefan Neacşu 7134755073 Feature/credential mode (#1570)
* Activate enabled cred mode for coconut feature

* Fix disable cred mode propagation bug
2022-08-26 16:45:32 +03:00
Sachin Kamath dd1420a65a Wallet - set gateway default port to 9000 (#1568)
* fix(wallet): client port to gateway defaults

* fix(wallet): refactor variable name
2022-08-26 13:28:58 +02:00
Jędrzej Stuczyński df1bc60464 Feature/vesting delegations queries (#1569)
* Added contract queries for vesting delegations

* Added the queries on NymdClient

* Added account_id to DelegationTimesResponse

* Returning raw u64 as opposed to wrapped Timestamp

* Updated changelog
2022-08-26 11:24:16 +01:00
Bogdan-Ștefan Neacşu 865e809342 Remove hard-coded values from credential client (#1566) 2022-08-25 17:52:43 +03:00
Jon Häggblad 51f9c1ca29 Connect: remove some sources of panics when initializing socks5-client (#1563)
* connect: remove panic when unable to load previous gateway conf

* connect: dont panic when cant get config file
2022-08-25 16:25:31 +02:00
Jon Häggblad 303b014a59 types: fix missing entries in SelectionChance (#1564) 2022-08-25 14:06:54 +02:00
Jon Häggblad e1e20fb13e inclusion-prob: make rng generic and predictable in tests (#1561) 2022-08-25 08:47:04 +02:00
Mark Sinclair 0c3c13ae88 Change nym-connect window title to NymConnect 2022-08-24 15:48:26 +01:00
Jon Häggblad 8c8b7d71d0 validator-api: create node status cache with selection probabilies (#1547)
* validator-api: create node status cache with selection probabilies

Create a node status cache to complement the contract cache. Initially
we store the simulated active set selection probabilities.

* validator-api: add validator cache watch channel

* changelog: add note

* validator-api: clippy fixes

* validator-api: fix clippy

* validator-api: additional fields to inclusion probabilities response

* selection chance: revert back to 3 buckets

* selection chance: revert buckets again

* rustfmt

* validator-api: remove the old get_mixnode_inclusion_probability

* node-status-cache: return error when refreshing

* inclusion-simulator: cap on wall clock time

* node status cache: tidy
2022-08-24 14:29:21 +02:00
Jon Häggblad 3163c5f054 gateway-client: clippy::question-mark 2022-08-24 09:51:47 +02:00
Jon Häggblad 4a1794b2f1 nymcoconut: fix named-arguments-used-positionally 2022-08-24 09:51:47 +02:00
Jon Häggblad 1898b8ed96 validator-api: add bounds checks in compute_mixnode_reward_estimation (#1559) 2022-08-24 09:30:12 +02:00
Mark Sinclair a23471859d GitHub Actions: fix up artifacts on nym-cli-publish 2022-08-23 20:19:57 +01:00
Mark Sinclair 9d8c9edf22 Add GitHub Action to build nym-cli on all platforms 2022-08-23 19:27:03 +01:00
Pierre Dommerc 5ea7b24efc fix(nym-wallet): bonding, enhance error handling (#1543)
open a dialog for user feedback when error occurs
add some better error logging
2022-08-23 17:53:57 +02:00
Jędrzej Stuczyński a43a24faa8 Exposed direct queries to smart contract on NymdClient (#1558)
* Exposed direct queries to smart contract on NymdClient

* Updated changelog
2022-08-23 15:45:43 +01:00
Jędrzej Stuczyński 39ee215005 Storing delegations using block timestamp as opposed to block height (#1544)
* Storing delegations using block timestamp as opposed to block height

* Updated changelog
2022-08-23 09:42:34 +01:00
Mark Sinclair ef7961f58e Update nym-release-publish.yml 2022-08-19 18:32:28 +01:00
Mark Sinclair e628338b33 GitHub Actions: add manual dispatch and artifact upload 2022-08-19 18:23:16 +01:00
Pierre Dommerc 1bb137f87f Nym-connect - service provider persistant storage (#1540)
* feat(nym-connect): local storage service provider

* feat(nym-connect): local storage service provider

* feat(nym-connect): local storage service provider

* Add some extra height to the window to stop the scrollbar apearing

* Show the service description when selecting a Service Provider

* Bump version

* Update changelog

* fix(nym-connect): hotfix

* fix(nym-connect): hotfix

* fix(nym-connect): wrong disabled state for connection button

Co-authored-by: Mark Sinclair <mmsinclair@gmail.com>
2022-08-18 18:53:03 +02:00
104 changed files with 2021 additions and 591 deletions
+50
View File
@@ -0,0 +1,50 @@
name: Publish Nym CLI binaries
on:
workflow_dispatch:
release:
types: [created]
env:
NETWORK: mainnet
jobs:
publish-nym-cli:
strategy:
fail-fast: false
matrix:
platform: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v3
- name: Check the release tag starts with `nym-cli-`
if: startsWith(github.ref, 'refs/tags/nym-cli-') == false && github.event_name != 'workflow_dispatch'
uses: actions/github-script@v3
with:
script: |
core.setFailed('Release tag did not start with nym-cli-...')
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Build binary
run: make build-nym-cli
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: nym-cli-${{ matrix.platform }}
path: |
target/release/nym-cli*
retention-days: 30
- name: Upload to release based on tag name
uses: softprops/action-gh-release@v1
if: github.event_name == 'release'
with:
files: |
target/release/nym-cli
+21 -2
View File
@@ -1,5 +1,7 @@
name: Publish Nym binaries
on:
workflow_dispatch:
release:
types: [created]
@@ -18,7 +20,7 @@ jobs:
- uses: actions/checkout@v3
- name: Check the release tag starts with `nym-binaries-`
if: startsWith(github.ref, 'refs/tags/nym-binaries-') == false
if: startsWith(github.ref, 'refs/tags/nym-binaries-') == false && github.event_name != 'workflow_dispatch'
uses: actions/github-script@v3
with:
script: |
@@ -35,8 +37,24 @@ jobs:
command: build
args: --workspace --release
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: my-artifact
path: |
target/release/nym-client
target/release/nym-gateway
target/release/nym-mixnode
target/release/nym-socks5-client
target/release/nym-validator-api
target/release/nym-network-requester
target/release/nym-network-statistics
target/release/nym-cli
retention-days: 30
- name: Upload to release based on tag name
uses: softprops/action-gh-release@v1
if: github.event_name == 'release'
with:
files: |
target/release/nym-client
@@ -45,4 +63,5 @@ jobs:
target/release/nym-socks5-client
target/release/nym-validator-api
target/release/nym-network-requester
target/release/nym-network-statistics
target/release/nym-network-statistics
target/release/nym-cli
+13 -1
View File
@@ -4,11 +4,19 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## Unreleased
### Added
- validator-client: added `query_contract_smart` and `query_contract_raw` on `NymdClient` ([#1558])
### Changed
- validator-client: made `fee` argument optional for `execute` and `execute_multiple` ([#1541])
- wasm-client: fixed build errors on MacOS and changed example JS code to use mainnet ([#1585])
[#1541]: https://github.com/nymtech/nym/pull/1541
[#1558]: https://github.com/nymtech/nym/pull/1558
[#1585]: https://github.com/nymtech/nym/pull/1585
## [nym-binaries-1.0.2](https://github.com/nymtech/nym/tree/nym-binaries-1.0.2)
@@ -27,6 +35,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
- validator-api: add Swagger to document the REST API ([#1249]).
- validator-api: Added new endpoints for coconut spending flow and communications with coconut & multisig contracts ([#1261])
- validator-api: add `uptime`, `estimated_operator_apy`, `estimated_delegators_apy` to `/mixnodes/detailed` endpoint ([#1393])
- validator-api: add node info cache storing simulated active set inclusion probabilities
- network-statistics: a new mixnet service that aggregates and exposes anonymized data about mixnet services ([#1328])
- mixnode: Added basic mixnode hardware reporting to the HTTP API ([#1308]).
- validator-api: endpoint, in coconut mode, for returning the validator-api cosmos address ([#1404]).
@@ -46,7 +55,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
- validator: fixed local docker-compose setup to work on Apple M1 ([#1329])
- explorer-api: listen out for SIGTERM and SIGQUIT too, making it play nicely as a system service ([#1482]).
- network-requester: fix filter for suffix-only domains ([#1487])
- validator-api: listen out for SIGTERM and SIGQUIT too, making it play nicely as a system service ([#1496]).
- validator-api: listen out for SIGTERM and SIGQUIT too, making it play nicely as a system service; cleaner shutdown, without panics ([#1496], [#1573]).
### Changed
@@ -63,6 +72,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
- gateway, network-statistics: include gateway id in the sent statistical data ([#1478])
- network explorer: tweak how active set probability is shown ([#1503])
- validator-api: rewarder set update fails without panicking on possible nymd queries ([#1520])
- network-requester, socks5 client (nym-connect): send and receive respectively a message error to be displayed about filter check failure ([#1576])
[#1249]: https://github.com/nymtech/nym/pull/1249
@@ -92,6 +102,8 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
[#1496]: https://github.com/nymtech/nym/pull/1496
[#1503]: https://github.com/nymtech/nym/pull/1503
[#1520]: https://github.com/nymtech/nym/pull/1520
[#1573]: https://github.com/nymtech/nym/pull/1573
[#1576]: https://github.com/nymtech/nym/pull/1576
## [v1.0.1](https://github.com/nymtech/nym/tree/v1.0.1) (2022-05-04)
Generated
+107 -56
View File
@@ -894,6 +894,7 @@ dependencies = [
"cfg-if 0.1.10",
"clap 3.2.8",
"coconut-interface",
"config",
"credential-storage",
"credentials",
"crypto",
@@ -1127,7 +1128,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1a816186fa68d9e426e3cb4ae4dff1fcd8e4a2c34b781bf7a822574a0d0aac8"
dependencies = [
"sct",
"sct 0.6.1",
]
[[package]]
@@ -1903,10 +1904,6 @@ name = "futures-timer"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c"
dependencies = [
"gloo-timers",
"send_wrapper",
]
[[package]]
name = "futures-util"
@@ -1957,8 +1954,8 @@ dependencies = [
"secp256k1",
"thiserror",
"tokio",
"tokio-tungstenite",
"tungstenite",
"tokio-tungstenite 0.17.2",
"tungstenite 0.17.3",
"url",
"validator-client",
"wasm-bindgen",
@@ -1983,7 +1980,7 @@ dependencies = [
"serde",
"serde_json",
"thiserror",
"tungstenite",
"tungstenite 0.17.3",
]
[[package]]
@@ -2091,18 +2088,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
[[package]]
name = "gloo-timers"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d12a7f4e95cfe710f1d624fb1210b7d961a5fb05c4fd942f4feab06e61f590e"
dependencies = [
"futures-channel",
"futures-core",
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "group"
version = "0.10.0"
@@ -2395,7 +2380,7 @@ dependencies = [
"tokio",
"tokio-rustls",
"tower-service",
"webpki",
"webpki 0.21.4",
]
[[package]]
@@ -2408,12 +2393,12 @@ dependencies = [
"futures-util",
"hyper",
"log",
"rustls",
"rustls 0.19.1",
"rustls-native-certs",
"tokio",
"tokio-rustls",
"webpki",
"webpki-roots",
"webpki 0.21.4",
"webpki-roots 0.21.1",
]
[[package]]
@@ -2500,6 +2485,7 @@ dependencies = [
name = "inclusion-probability"
version = "0.1.0"
dependencies = [
"log",
"rand 0.8.5",
"thiserror",
]
@@ -2751,9 +2737,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.16"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if 1.0.0",
]
@@ -3087,7 +3073,7 @@ dependencies = [
"serde_json",
"sled",
"tokio",
"tokio-tungstenite",
"tokio-tungstenite 0.17.2",
"topology",
"url",
"validator-client",
@@ -3134,7 +3120,7 @@ dependencies = [
"thiserror",
"tokio",
"tokio-stream",
"tokio-tungstenite",
"tokio-tungstenite 0.17.2",
"tokio-util 0.7.3",
"url",
"validator-api-requests",
@@ -3208,7 +3194,7 @@ dependencies = [
"statistics-common",
"thiserror",
"tokio",
"tokio-tungstenite",
"tokio-tungstenite 0.14.0",
"websocket-requests",
]
@@ -3225,7 +3211,7 @@ dependencies = [
"statistics-common",
"thiserror",
"tokio",
"tokio-tungstenite",
"tokio-tungstenite 0.14.0",
]
[[package]]
@@ -3314,6 +3300,7 @@ dependencies = [
"gateway-client",
"getset",
"humantime-serde",
"inclusion-probability",
"log",
"mixnet-contract-common",
"multisig-contract-common",
@@ -3333,6 +3320,7 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
"tap",
"task",
"thiserror",
"time 0.3.9",
@@ -4726,8 +4714,20 @@ dependencies = [
"base64",
"log",
"ring",
"sct",
"webpki",
"sct 0.6.1",
"webpki 0.21.4",
]
[[package]]
name = "rustls"
version = "0.20.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033"
dependencies = [
"log",
"ring",
"sct 0.7.0",
"webpki 0.22.0",
]
[[package]]
@@ -4737,7 +4737,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a07b7c1885bd8ed3831c289b7870b13ef46fe0e856d288c30d9cc17d75a2092"
dependencies = [
"openssl-probe",
"rustls",
"rustls 0.19.1",
"schannel",
"security-framework",
]
@@ -4829,6 +4829,16 @@ dependencies = [
"untrusted",
]
[[package]]
name = "sct"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "sec1"
version = "0.2.1"
@@ -4922,12 +4932,6 @@ dependencies = [
"pest",
]
[[package]]
name = "send_wrapper"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0"
[[package]]
name = "serde"
version = "1.0.136"
@@ -4989,9 +4993,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.79"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7"
dependencies = [
"itoa 1.0.1",
"ryu",
@@ -5207,6 +5211,7 @@ version = "0.1.0"
dependencies = [
"nymsphinx-addressing",
"ordered-buffer",
"thiserror",
]
[[package]]
@@ -5325,7 +5330,7 @@ dependencies = [
"once_cell",
"paste",
"percent-encoding",
"rustls",
"rustls 0.19.1",
"sha2 0.9.9",
"smallvec 1.8.0",
"sqlformat",
@@ -5334,8 +5339,8 @@ dependencies = [
"thiserror",
"tokio-stream",
"url",
"webpki",
"webpki-roots",
"webpki 0.21.4",
"webpki-roots 0.21.1",
]
[[package]]
@@ -5794,9 +5799,9 @@ version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6"
dependencies = [
"rustls",
"rustls 0.19.1",
"tokio",
"webpki",
"webpki 0.21.4",
]
[[package]]
@@ -5833,7 +5838,19 @@ dependencies = [
"log",
"pin-project",
"tokio",
"tungstenite",
"tungstenite 0.13.0",
]
[[package]]
name = "tokio-tungstenite"
version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181"
dependencies = [
"futures-util",
"log",
"tokio",
"tungstenite 0.17.3",
]
[[package]]
@@ -6101,6 +6118,28 @@ dependencies = [
"utf-8",
]
[[package]]
name = "tungstenite"
version = "0.17.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0"
dependencies = [
"base64",
"byteorder",
"bytes",
"http",
"httparse",
"log",
"rand 0.8.5",
"rustls 0.20.6",
"sha-1 0.10.0",
"thiserror",
"url",
"utf-8",
"webpki 0.22.0",
"webpki-roots 0.22.4",
]
[[package]]
name = "typenum"
version = "1.15.0"
@@ -6413,8 +6452,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
dependencies = [
"cfg-if 1.0.0",
"serde",
"serde_json",
"wasm-bindgen-macro",
]
@@ -6480,7 +6517,7 @@ version = "0.1.0"
dependencies = [
"futures",
"js-sys",
"tungstenite",
"tungstenite 0.17.3",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
@@ -6510,15 +6547,12 @@ dependencies = [
"ethereum-types",
"futures",
"futures-timer",
"getrandom 0.2.6",
"headers",
"hex",
"js-sys",
"jsonrpc-core",
"log",
"parking_lot 0.11.2",
"pin-project",
"rand 0.8.5",
"reqwest",
"rlp",
"secp256k1",
@@ -6530,8 +6564,6 @@ dependencies = [
"tokio-stream",
"tokio-util 0.6.9",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web3-async-native-tls",
]
@@ -6557,13 +6589,32 @@ dependencies = [
"untrusted",
]
[[package]]
name = "webpki"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "webpki-roots"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940"
dependencies = [
"webpki",
"webpki 0.21.4",
]
[[package]]
name = "webpki-roots"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf"
dependencies = [
"webpki 0.22.0",
]
[[package]]
@@ -133,7 +133,6 @@ where
let prepared_fragment = self
.message_preparer
.prepare_chunk_for_sending(chunk_clone, topology, &self.ack_key, &recipient)
.await
.unwrap();
real_messages.push(RealMessage::new(
@@ -83,7 +83,6 @@ where
let prepared_fragment = self
.message_preparer
.prepare_chunk_for_sending(chunk_clone, topology_ref, &self.ack_key, packet_recipient)
.await
.unwrap();
// if we have the ONLY strong reference to the ack data, it means it was removed from the
+1
View File
@@ -18,6 +18,7 @@ url = "2.2"
tokio = { version = "1.19.1", features = ["rt-multi-thread", "net", "signal", "macros"] } # async runtime
coconut-interface = { path = "../../common/coconut-interface" }
config = { path = "../../common/config" }
credentials = { path = "../../common/credentials" }
credential-storage = { path = "../../common/credential-storage" }
crypto = { path = "../../common/crypto", features = ["rand", "asymmetric", "symmetric", "aes", "hashing"] }
+3 -4
View File
@@ -2,7 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
use crate::error::Result;
use crate::{MNEMONIC, NYMD_URL};
use bip39::Mnemonic;
use network_defaults::{NymNetworkDetails, VOUCHER_INFO};
use std::str::FromStr;
@@ -17,9 +16,9 @@ pub(crate) struct Client {
}
impl Client {
pub fn new() -> Self {
let nymd_url = Url::from_str(NYMD_URL).unwrap();
let mnemonic = Mnemonic::from_str(MNEMONIC).unwrap();
pub fn new(nymd_url: &str, mnemonic: &str) -> Self {
let nymd_url = Url::from_str(nymd_url).unwrap();
let mnemonic = Mnemonic::from_str(mnemonic).unwrap();
let network_details = NymNetworkDetails::new_from_env();
let config = nymd::Config::try_from_nym_network_details(&network_details)
.expect("failed to construct valid validator client config with the provided network");
+13 -4
View File
@@ -6,7 +6,6 @@ use clap::{Args, Subcommand};
use pickledb::PickleDb;
use rand::rngs::OsRng;
use std::str::FromStr;
use url::Url;
use coconut_interface::{Attribute, Base58, BlindSignRequest, Bytable, Parameters};
use credential_storage::storage::Storage;
@@ -20,7 +19,6 @@ use validator_client::nymd::tx::Hash;
use crate::client::Client;
use crate::error::{CredentialClientError, Result};
use crate::state::{KeyPair, RequestData, State};
use crate::SIGNER_AUTHORITIES;
#[derive(Subcommand)]
pub(crate) enum Commands {
@@ -39,6 +37,12 @@ pub(crate) trait Execute {
#[derive(Args, Clone)]
pub(crate) struct Deposit {
/// The nymd URL that should be used
#[clap(long)]
nymd_url: String,
/// A mnemonic for the account that does the deposit
#[clap(long)]
mnemonic: String,
/// The amount that needs to be deposited
#[clap(long)]
amount: u64,
@@ -51,7 +55,7 @@ impl Execute for Deposit {
let signing_keypair = KeyPair::from(identity::KeyPair::new(&mut rng));
let encryption_keypair = KeyPair::from(encryption::KeyPair::new(&mut rng));
let client = Client::new();
let client = Client::new(&self.nymd_url, &self.mnemonic);
let tx_hash = client
.deposit(
self.amount,
@@ -96,6 +100,10 @@ pub(crate) struct GetCredential {
/// The hash of a successful deposit transaction
#[clap(long)]
tx_hash: String,
/// The URLs to the validator-api endpoints the are run as coconut signer authorities, separated
/// by comma (,)
#[clap(long)]
signer_authorities: String,
/// If we want to get the signature without attaching a blind sign request; it is expected that
/// there is already a signature stored on the signer
#[clap(long, parse(from_flag))]
@@ -108,7 +116,8 @@ impl Execute for GetCredential {
let mut state = db
.get::<State>(&self.tx_hash)
.ok_or(CredentialClientError::NoDeposit)?;
let urls = SIGNER_AUTHORITIES.map(|addr| Url::from_str(addr).unwrap());
let urls = config::parse_validators(&self.signer_authorities);
let params = Parameters::new(TOTAL_ATTRIBUTES).unwrap();
let bandwidth_credential_attributes = if self.__no_request {
+13 -8
View File
@@ -11,20 +11,24 @@ cfg_if::cfg_if! {
use commands::{Commands, Execute};
use error::Result;
use network_defaults::setup_env;
use clap::Parser;
use pickledb::{PickleDb, PickleDbDumpPolicy, SerializationMethod};
pub const MNEMONIC: &str = "jazz fatigue diagram account outer wrist slide cherry mother grid network pause wolf pig round answer mail junior better hair dismiss toward access end";
pub const NYMD_URL: &str = "http://127.0.0.1:26657";
pub const CONTRACT_ADDRESS: &str = "nymt1nc5tatafv6eyq7llkr2gv50ff9e22mnfp9pc5s";
pub const SIGNER_AUTHORITIES: [&str; 1] = [
"http://127.0.0.1:8080",
];
#[derive(Parser)]
#[clap(author = "Nymtech", version, about)]
struct Cli {
/// Path pointing to an env file that configures the client.
#[clap(long)]
pub(crate) config_env_file: Option<std::path::PathBuf>,
/// Path where the sqlite credental database will be located.
/// It should point to a $HOME/$CLIENT_ID/data/db.sqlite file of
/// the client that is supposed to use the credential.
#[clap(long)]
pub(crate) credential_db_path: std::path::PathBuf,
#[clap(subcommand)]
command: Commands,
}
@@ -32,8 +36,9 @@ cfg_if::cfg_if! {
#[tokio::main]
async fn main() -> Result<()> {
let args = Cli::parse();
setup_env(args.config_env_file.clone());
let shared_storage = credential_storage::initialise_storage(std::path::PathBuf::from("/tmp/credential.db")).await;
let shared_storage = credential_storage::initialise_storage(args.credential_db_path.clone()).await;
let mut db = match PickleDb::load(
"credential.db",
PickleDbDumpPolicy::AutoDump,
+1 -1
View File
@@ -28,7 +28,7 @@ rand = { version = "0.7.3", features = ["wasm-bindgen"] } # rng-related traits +
serde = { version = "1.0.104", features = ["derive"] } # for config serialization/deserialization
sled = "0.34" # for storage of replySURB decryption keys
tokio = { version = "1.19.1", features = ["rt-multi-thread", "net", "signal"] } # async runtime
tokio-tungstenite = "0.14" # websocket
tokio-tungstenite = "0.17.2" # websocket
## internal
client-core = { path = "../client-core" }
+4
View File
@@ -50,6 +50,10 @@ impl NymConfig for Config {
.join("clients")
}
fn try_default_root_directory() -> Option<PathBuf> {
dirs::home_dir().map(|path| path.join(".nym").join("clients"))
}
fn root_directory(&self) -> PathBuf {
self.base.get_nym_root_directory()
}
+3 -3
View File
@@ -199,9 +199,9 @@ impl NymClient {
Some(bandwidth_controller),
);
if self.config.get_base().get_disabled_credentials_mode() {
gateway_client.set_disabled_credentials_mode(true)
}
gateway_client
.set_disabled_credentials_mode(self.config.get_base().get_disabled_credentials_mode());
gateway_client
.authenticate_and_start()
.await
+4 -5
View File
@@ -46,10 +46,9 @@ pub(crate) struct Init {
fastmode: bool,
/// Set this client to work in a enabled credentials mode that would attempt to use gateway
/// with bandwidth credential requirement. If this value is set, --eth-endpoint and
/// --eth-private_key don't need to be set.
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[clap(long, conflicts_with_all = &["eth-endpoint", "eth-private-key"])]
/// with bandwidth credential requirement.
#[cfg(any(feature = "eth", feature = "coconut"))]
#[clap(long)]
enabled_credentials_mode: bool,
/// URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20
@@ -79,7 +78,7 @@ impl From<Init> for OverrideConfig {
port: init_config.port,
fastmode: init_config.fastmode,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[cfg(any(feature = "eth", feature = "coconut"))]
enabled_credentials_mode: init_config.enabled_credentials_mode,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
+6 -3
View File
@@ -78,7 +78,7 @@ pub(crate) struct OverrideConfig {
port: Option<u16>,
fastmode: bool,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[cfg(any(feature = "eth", feature = "coconut"))]
enabled_credentials_mode: bool,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
@@ -126,12 +126,15 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi
.get_base_mut()
.with_eth_private_key(DEFAULT_ETH_PRIVATE_KEY.to_string());
}
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[cfg(any(feature = "eth", feature = "coconut"))]
{
if args.enabled_credentials_mode {
config.get_base_mut().with_disabled_credentials(false)
}
}
#[cfg(all(feature = "eth", not(feature = "coconut")))]
{
if let Some(eth_endpoint) = args.eth_endpoint {
config.get_base_mut().with_eth_endpoint(eth_endpoint);
}
+4 -5
View File
@@ -35,10 +35,9 @@ pub(crate) struct Run {
port: Option<u16>,
/// Set this client to work in a enabled credentials mode that would attempt to use gateway
/// with bandwidth credential requirement. If this value is set, --eth-endpoint and
/// --eth-private-key don't need to be set.
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[clap(long, conflicts_with_all = &["eth-endpoint", "eth-private-key"])]
/// with bandwidth credential requirement.
#[cfg(any(feature = "eth", feature = "coconut"))]
#[clap(long)]
enabled_credentials_mode: bool,
/// URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20
@@ -62,7 +61,7 @@ impl From<Run> for OverrideConfig {
port: run_config.port,
fastmode: false,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[cfg(any(feature = "eth", feature = "coconut"))]
enabled_credentials_mode: run_config.enabled_credentials_mode,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
+4
View File
@@ -33,6 +33,10 @@ impl NymConfig for Config {
.join("socks5-clients")
}
fn try_default_root_directory() -> Option<PathBuf> {
dirs::home_dir().map(|path| path.join(".nym").join("socks5-clients"))
}
fn root_directory(&self) -> PathBuf {
self.base.get_nym_root_directory()
}
+3 -3
View File
@@ -198,9 +198,9 @@ impl NymClient {
Some(bandwidth_controller),
);
if self.config.get_base().get_disabled_credentials_mode() {
gateway_client.set_disabled_credentials_mode(true)
}
gateway_client
.set_disabled_credentials_mode(self.config.get_base().get_disabled_credentials_mode());
gateway_client
.authenticate_and_start()
.await
+4 -5
View File
@@ -46,10 +46,9 @@ pub(crate) struct Init {
fastmode: bool,
/// Set this client to work in a enabled credentials mode that would attempt to use gateway
/// with bandwidth credential requirement. If this value is set, --eth-endpoint and
/// --eth-private_key don't need to be set.
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[clap(long, conflicts_with_all = &["eth-endpoint", "eth-private-key"])]
/// with bandwidth credential requirement.
#[cfg(any(feature = "eth", feature = "coconut"))]
#[clap(long)]
enabled_credentials_mode: bool,
/// URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20
@@ -78,7 +77,7 @@ impl From<Init> for OverrideConfig {
port: init_config.port,
fastmode: init_config.fastmode,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[cfg(any(feature = "eth", feature = "coconut"))]
enabled_credentials_mode: init_config.enabled_credentials_mode,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
+5 -2
View File
@@ -78,7 +78,7 @@ pub(crate) struct OverrideConfig {
port: Option<u16>,
fastmode: bool,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[cfg(any(feature = "eth", feature = "coconut"))]
enabled_credentials_mode: bool,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
@@ -121,11 +121,14 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi
.with_eth_private_key(DEFAULT_ETH_PRIVATE_KEY.to_string());
}
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[cfg(any(feature = "eth", feature = "coconut"))]
{
if args.enabled_credentials_mode {
config.get_base_mut().with_disabled_credentials(false)
}
}
#[cfg(all(feature = "eth", not(feature = "coconut")))]
{
if let Some(eth_endpoint) = args.eth_endpoint {
config.get_base_mut().with_eth_endpoint(eth_endpoint);
}
+4 -5
View File
@@ -39,10 +39,9 @@ pub(crate) struct Run {
port: Option<u16>,
/// Set this client to work in a enabled credentials mode that would attempt to use gateway
/// with bandwidth credential requirement. If this value is set, --eth-endpoint and
/// --eth-private-key don't need to be set.
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[clap(long, conflicts_with_all = &["eth-endpoint", "eth-private-key"])]
/// with bandwidth credential requirement.
#[cfg(any(feature = "eth", feature = "coconut"))]
#[clap(long)]
enabled_credentials_mode: bool,
/// URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20
@@ -65,7 +64,7 @@ impl From<Run> for OverrideConfig {
port: run_config.port,
fastmode: false,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[cfg(any(feature = "eth", feature = "coconut"))]
enabled_credentials_mode: run_config.enabled_credentials_mode,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
@@ -54,6 +54,13 @@ impl MixnetResponseListener {
return;
}
Ok(Message::Response(data)) => data,
Ok(Message::NetworkRequesterResponse(r)) => {
error!(
"Network requester failed on connection id {} with error: {}",
r.connection_id, r.network_requester_error
);
return;
}
};
self.controller_sender
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "nym-client-wasm"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jedrzej Stuczynski <andrew@nymtech.net>"]
version = "1.0.1"
version = "1.0.0"
edition = "2021"
keywords = ["nym", "sphinx", "wasm", "webassembly", "privacy", "client"]
license = "Apache-2.0"
@@ -32,7 +32,7 @@ credentials = { path = "../../common/credentials", optional = true }
crypto = { path = "../../common/crypto" }
nymsphinx = { path = "../../common/nymsphinx" }
topology = { path = "../../common/topology" }
gateway-client = { path = "../../common/client-libs/gateway-client", default-features = false, features = ["wasm"] }
gateway-client = { path = "../../common/client-libs/gateway-client", default-features = false, features = ["wasm", "coconut"] }
validator-client = { path = "../../common/client-libs/validator-client", default-features = false }
wasm-utils = { path = "../../common/wasm-utils" }
Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

+1 -1
View File
@@ -25,7 +25,7 @@ async function main() {
set_panic_hook();
// validator server we will use to get topology from
const validator = "https://sandbox-validator.nymtech.net/api"; //"http://localhost:8081";
const validator = "https://validator.nymtech.net/api"; //"http://localhost:8081";
client = new NymClient(validator);
+17 -6
View File
@@ -132,9 +132,7 @@ impl NymClient {
bandwidth_controller,
);
if disabled_credentials_mode {
gateway_client.set_disabled_credentials_mode(true)
}
gateway_client.set_disabled_credentials_mode(disabled_credentials_mode);
gateway_client
.authenticate_and_start()
@@ -199,7 +197,6 @@ impl NymClient {
// don't bother with acks etc. for time being
let prepared_fragment = message_preparer
.prepare_chunk_for_sending(message_chunk, topology, &self.ack_key, &recipient)
.await
.unwrap();
console_warn!("packet is going to have round trip time of {:?}, but we're not going to do anything for acks anyway ", prepared_fragment.total_delay);
@@ -214,7 +211,7 @@ impl NymClient {
self
}
pub(crate) fn choose_gateway(&self) -> &gateway::Node {
pub(crate) fn choose_gateway(&self) -> gateway::Node {
let topology = self
.topology
.as_ref()
@@ -222,7 +219,21 @@ impl NymClient {
// choose the first one available
assert!(!topology.gateways().is_empty());
topology.gateways().first().unwrap()
topology.gateways().first().unwrap();
console_log!("picking nym gateway");
gateway::Node {
owner: "n1kymvkx6vsq7pvn6hfurkpg06h3j4gxj4em7tlg".to_string(),
stake: 100000000,
location: "PRIVACY HQ".to_string(),
host: "gateway1.nymtech.net".parse().unwrap(),
mix_host: "213.219.38.119:1789".parse().unwrap(),
clients_port: 443,
identity_key: identity::PublicKey::from_base58_string("E3mvZTHQCdBvhfr178Swx9g4QG3kkRUun7YnToLMcMbM").unwrap(),
sphinx_key: encryption::PublicKey::from_base58_string("CYcrjoJ8GT7Dp54zViUyyRUfegeRCyPifWQZHRgMZrfX").unwrap(),
version: "1.0.0-rc.2".to_string()
}
}
// Right now it's impossible to have async exported functions to take `&mut self` rather than mut self
+7 -7
View File
@@ -15,8 +15,8 @@ log = "0.4"
thiserror = "1.0"
url = "2.2"
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
secp256k1 = "0.20.3"
web3 = { version = "0.17.0", default-features = false }
secp256k1 = { version = "0.20.3", optional = true }
web3 = { version = "0.17.0", default-features = false, optional = true }
async-trait = { version = "0.1.51" }
# internal
@@ -30,8 +30,8 @@ network-defaults = { path = "../../network-defaults" }
validator-client = { path = "../validator-client", optional = true }
[dependencies.tungstenite]
version = "0.13"
default-features = false
version = "0.17.3"
features = ["rustls-tls-webpki-roots"]
# non-wasm-only dependencies
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
@@ -39,7 +39,7 @@ version = "1.19.1"
features = ["macros", "rt", "net", "sync", "time"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-tungstenite]
version = "0.14"
version = "0.17.2"
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.credential-storage]
path = "../../credential-storage"
@@ -73,5 +73,5 @@ features = ["js"]
[features]
coconut = ["gateway-requests/coconut", "coconut-interface", "validator-client", "credentials/coconut"]
wasm = ["web3/wasm", "web3/http", "web3/signing"]
default = ["web3/default"]
wasm = []
default = ["web3/default", "secp256k1"]
@@ -23,7 +23,7 @@ use {
},
};
#[cfg(not(feature = "coconut"))]
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
use {
credentials::token::bandwidth::TokenCredential,
crypto::asymmetric::identity,
@@ -45,7 +45,7 @@ use {
},
};
#[cfg(not(feature = "coconut"))]
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
pub fn eth_contract(web3: Web3<Http>) -> Contract<Http> {
Contract::from_json(
web3.eth(),
@@ -58,7 +58,7 @@ pub fn eth_contract(web3: Web3<Http>) -> Contract<Http> {
.expect("Invalid json abi")
}
#[cfg(not(feature = "coconut"))]
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
pub fn eth_erc20_contract(web3: Web3<Http>) -> Contract<Http> {
Contract::from_json(
web3.eth(),
@@ -76,11 +76,11 @@ pub struct BandwidthController<St: Storage> {
storage: St,
#[cfg(feature = "coconut")]
validator_endpoints: Vec<url::Url>,
#[cfg(not(feature = "coconut"))]
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
contract: Contract<Http>,
#[cfg(not(feature = "coconut"))]
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
erc20_contract: Contract<Http>,
#[cfg(not(feature = "coconut"))]
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
eth_private_key: SecretKey,
}
@@ -96,7 +96,7 @@ where
}
}
#[cfg(not(feature = "coconut"))]
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
pub fn new(
storage: St,
eth_endpoint: String,
@@ -120,7 +120,7 @@ where
})
}
#[cfg(not(feature = "coconut"))]
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
async fn backup_keypair(&self, keypair: &identity::KeyPair) -> Result<(), GatewayClientError> {
self.storage
.insert_erc20_credential(
@@ -132,7 +132,7 @@ where
Ok(())
}
#[cfg(not(feature = "coconut"))]
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
async fn restore_keypair(&self) -> Result<identity::KeyPair, GatewayClientError> {
let data = self.storage.get_next_erc20_credential().await?;
let public_key = identity::PublicKey::from_base58_string(data.public_key).unwrap();
@@ -141,7 +141,7 @@ where
Ok(identity::KeyPair::from_keys(private_key, public_key))
}
#[cfg(not(feature = "coconut"))]
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
async fn mark_keypair_as_spent(
&self,
keypair: &identity::KeyPair,
@@ -180,7 +180,7 @@ where
)?)
}
#[cfg(not(feature = "coconut"))]
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
pub async fn prepare_token_credential(
&self,
gateway_identity: identity::PublicKey,
@@ -219,7 +219,7 @@ where
))
}
#[cfg(not(feature = "coconut"))]
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
pub async fn buy_token_credential(
&self,
verification_key: identity::PublicKey,
@@ -348,7 +348,7 @@ where
}
}
#[cfg(not(feature = "coconut"))]
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
#[cfg(test)]
mod tests {
use network_defaults::ETH_EVENT_NAME;
@@ -30,7 +30,7 @@ use rand::rngs::OsRng;
use std::convert::TryFrom;
use std::sync::Arc;
use std::time::Duration;
use tungstenite::protocol::Message;
use tungstenite::Message;
#[cfg(not(target_arch = "wasm32"))]
use tokio_tungstenite::connect_async;
@@ -449,7 +449,7 @@ impl GatewayClient {
.derive_destination_address();
let encrypted_address = EncryptedAddressBytes::new(&self_address, shared_key, &iv);
let msg =
let msg: tungstenite::Message =
ClientControlRequest::new_authenticate(self_address, encrypted_address, iv).into();
match self.send_websocket_message(msg).await? {
@@ -733,6 +733,8 @@ impl GatewayClient {
}
pub async fn authenticate_and_start(&mut self) -> Result<Arc<SharedKeys>, GatewayClientError> {
println!("About to establish connection to {}", self.gateway_address);
if !self.connection.is_established() {
self.establish_connection().await?;
}
@@ -162,12 +162,10 @@ impl PartiallyDelegated {
.expect("stream sender was somehow dropped without sending anything!");
if let Some(res) = receive_res {
if let Err(err) = res {
// the receiver got an error. most likely a network one.
return Err(err);
} else {
panic!("This should have NEVER happened - returned a stream before receiving notification")
}
let _res = res?;
panic!(
"This should have NEVER happened - returned a stream before receiving notification"
)
}
// this call failing is incredibly unlikely, but not impossible.
@@ -24,7 +24,7 @@ use mixnet_contract_common::{
PagedMixDelegationsResponse, PagedMixnodeResponse, PagedRewardedSetResponse, QueryMsg,
RewardedSetUpdateDetails,
};
use serde::Serialize;
use serde::{Deserialize, Serialize};
use std::convert::TryInto;
use std::time::SystemTime;
use vesting_contract_common::ExecuteMsg as VestingExecuteMsg;
@@ -282,6 +282,30 @@ impl<C> NymdClient<C> {
self.simulated_gas_multiplier = multiplier;
}
pub async fn query_contract_smart<M, T>(
&self,
contract: &AccountId,
query_msg: &M,
) -> Result<T, NymdError>
where
C: CosmWasmClient + Sync,
M: ?Sized + Serialize + Sync,
for<'a> T: Deserialize<'a>,
{
self.client.query_contract_smart(contract, query_msg).await
}
pub async fn query_contract_raw(
&self,
contract: &AccountId,
query_data: Vec<u8>,
) -> Result<Vec<u8>, NymdError>
where
C: CosmWasmClient + Sync,
{
self.client.query_contract_raw(contract, query_data).await
}
pub fn wrap_contract_execute_message<M>(
&self,
contract_address: &AccountId,
@@ -7,9 +7,11 @@ use crate::nymd::error::NymdError;
use crate::nymd::NymdClient;
use async_trait::async_trait;
use cosmwasm_std::{Coin as CosmWasmCoin, Timestamp};
use mixnet_contract_common::IdentityKey;
use vesting_contract::vesting::Account;
use vesting_contract_common::{
messages::QueryMsg as VestingQueryMsg, OriginalVestingResponse, Period, PledgeData,
messages::QueryMsg as VestingQueryMsg, AllDelegationsResponse, DelegationTimesResponse,
OriginalVestingResponse, Period, PledgeData, VestingDelegation,
};
#[async_trait]
@@ -70,6 +72,37 @@ pub trait VestingQueryClient {
&self,
vesting_account_address: &str,
) -> Result<Period, NymdError>;
async fn get_delegation_timestamps(
&self,
address: &str,
mix_identity: String,
) -> Result<DelegationTimesResponse, NymdError>;
async fn get_all_vesting_delegations_paged(
&self,
start_after: Option<(u32, IdentityKey, u64)>,
limit: Option<u32>,
) -> Result<AllDelegationsResponse, NymdError>;
async fn get_all_vesting_delegations(&self) -> Result<Vec<VestingDelegation>, NymdError> {
let mut delegations = Vec::new();
let mut start_after = None;
loop {
let mut paged_response = self
.get_all_vesting_delegations_paged(start_after.take(), None)
.await?;
delegations.append(&mut paged_response.delegations);
if let Some(start_after_res) = paged_response.start_next_after {
start_after = Some(start_after_res)
} else {
break;
}
}
Ok(delegations)
}
}
#[async_trait]
@@ -232,4 +265,29 @@ impl<C: CosmWasmClient + Sync + Send> VestingQueryClient for NymdClient<C> {
.query_contract_smart(self.vesting_contract_address(), &request)
.await
}
async fn get_delegation_timestamps(
&self,
address: &str,
mix_identity: String,
) -> Result<DelegationTimesResponse, NymdError> {
let request = VestingQueryMsg::GetDelegationTimes {
address: address.to_string(),
mix_identity,
};
self.client
.query_contract_smart(self.vesting_contract_address(), &request)
.await
}
async fn get_all_vesting_delegations_paged(
&self,
start_after: Option<(u32, IdentityKey, u64)>,
limit: Option<u32>,
) -> Result<AllDelegationsResponse, NymdError> {
let request = VestingQueryMsg::GetAllDelegations { start_after, limit };
self.client
.query_contract_smart(self.vesting_contract_address(), &request)
.await
}
}
+24
View File
@@ -41,6 +41,30 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned {
Self::default_config_directory(id).join(Self::config_file_name())
}
// We provide a second set of functions that tries to not panic.
fn try_default_root_directory() -> Option<PathBuf>;
fn try_default_config_directory(id: Option<&str>) -> Option<PathBuf> {
if let Some(id) = id {
Self::try_default_root_directory().map(|d| d.join(id).join("config"))
} else {
Self::try_default_root_directory().map(|d| d.join("config"))
}
}
fn try_default_data_directory(id: Option<&str>) -> Option<PathBuf> {
if let Some(id) = id {
Self::try_default_root_directory().map(|d| d.join(id).join("data"))
} else {
Self::try_default_root_directory().map(|d| d.join("data"))
}
}
fn try_default_config_file_path(id: Option<&str>) -> Option<PathBuf> {
Self::try_default_config_directory(id).map(|d| d.join(Self::config_file_name()))
}
fn root_directory(&self) -> PathBuf;
fn config_directory(&self) -> PathBuf;
fn data_directory(&self) -> PathBuf;
@@ -217,7 +217,7 @@ pub enum QueryMsg {
#[serde(rename_all = "snake_case")]
pub struct MigrateMsg {
pub mixnet_denom: String,
nodes_to_remove: Option<Vec<NodeToRemove>>,
pub nodes_to_remove: Option<Vec<NodeToRemove>>,
}
impl MigrateMsg {
@@ -1,10 +1,11 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::{Coin, Timestamp};
use cosmwasm_std::{Addr, Coin, Timestamp, Uint128};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub use messages::{ExecuteMsg, InitMsg, MigrateMsg, QueryMsg};
use mixnet_contract_common::IdentityKey;
pub mod events;
pub mod messages;
@@ -73,3 +74,35 @@ impl OriginalVestingResponse {
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, JsonSchema)]
pub struct VestingDelegation {
pub account_id: u32,
pub mix_identity: IdentityKey,
pub block_timestamp: u64,
pub amount: Uint128,
}
impl VestingDelegation {
pub fn storage_key(&self) -> (u32, IdentityKey, u64) {
(
self.account_id,
self.mix_identity.clone(),
self.block_timestamp,
)
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, JsonSchema)]
pub struct DelegationTimesResponse {
pub owner: Addr,
pub account_id: u32,
pub mix_identity: IdentityKey,
pub delegation_timestamps: Vec<u64>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, JsonSchema)]
pub struct AllDelegationsResponse {
pub delegations: Vec<VestingDelegation>,
pub start_next_after: Option<(u32, IdentityKey, u64)>,
}
@@ -170,4 +170,12 @@ pub enum QueryMsg {
address: String,
},
GetLockedPledgeCap {},
GetDelegationTimes {
address: String,
mix_identity: IdentityKey,
},
GetAllDelegations {
start_after: Option<(u32, IdentityKey, u64)>,
limit: Option<u32>,
},
}
+1
View File
@@ -6,5 +6,6 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4.17"
rand = "0.8.5"
thiserror = "1.0.32"
+3 -1
View File
@@ -4,6 +4,8 @@ pub enum Error {
EmptyListCumulStake,
#[error("Sample point was unexpectedly out of bounds")]
SamplePointOutOfBounds,
#[error("Norm computation failed on different size arrarys")]
#[error("Norm computation failed on different size arrays")]
NormDifferenceSizeArrays,
#[error("Computed probabilities are fewer than input number of nodes")]
ResultsShorterThanInput,
}
+169 -21
View File
@@ -1,26 +1,50 @@
//! Active set inclusion probability simulator
use std::time::{Duration, Instant};
use error::Error;
use rand::Rng;
mod error;
const TOLERANCE_L2_NORM: f64 = 1e-4;
const TOLERANCE_MAX_NORM: f64 = 1e-3;
const TOLERANCE_MAX_NORM: f64 = 1e-4;
pub struct SelectionProbability {
pub active_set_probability: Vec<f64>,
pub reserve_set_probability: Vec<f64>,
pub samples: u32,
pub samples: u64,
pub time: Duration,
pub delta_l2: f64,
pub delta_max: f64,
}
pub fn simulate_selection_probability_mixnodes(
list_stake_for_mixnodes: &[u64],
pub fn simulate_selection_probability_mixnodes<R>(
list_stake_for_mixnodes: &[u128],
active_set_size: usize,
reserve_set_size: usize,
max_samples: u32,
) -> Result<SelectionProbability, Error> {
max_samples: u64,
max_time: Duration,
rng: &mut R,
) -> Result<SelectionProbability, Error>
where
R: Rng + ?Sized,
{
log::trace!("Simulating mixnode active set selection probability");
// In case the active set size is larger than the number of bonded mixnodes, they all have 100%
// chance we don't have to go through with the simulation
if list_stake_for_mixnodes.len() <= active_set_size {
return Ok(SelectionProbability {
active_set_probability: vec![1.0; list_stake_for_mixnodes.len()],
reserve_set_probability: vec![0.0; list_stake_for_mixnodes.len()],
samples: 0,
time: Duration::ZERO,
delta_l2: 0.0,
delta_max: 0.0,
});
}
// Total number of existing (registered) nodes
let num_mixnodes = list_stake_for_mixnodes.len();
@@ -35,7 +59,9 @@ pub fn simulate_selection_probability_mixnodes(
let mut samples = 0;
let mut delta_l2;
let mut delta_max;
let mut rng = rand::thread_rng();
// Make sure we bound the time we allow it to run
let start_time = Instant::now();
loop {
samples += 1;
@@ -46,8 +72,10 @@ pub fn simulate_selection_probability_mixnodes(
let active_set_probability_previous = active_set_probability.clone();
// Select the active nodes for the epoch (hour)
while sample_active_mixnodes.len() < active_set_size {
let candidate = sample_candidate(&list_cumul_temp, &mut rng)?;
while sample_active_mixnodes.len() < active_set_size
&& sample_active_mixnodes.len() < list_cumul_temp.len()
{
let candidate = sample_candidate(&list_cumul_temp, rng)?;
if !sample_active_mixnodes.contains(&candidate) {
sample_active_mixnodes.push(candidate);
@@ -56,8 +84,10 @@ pub fn simulate_selection_probability_mixnodes(
}
// Select the reserve nodes for the epoch (hour)
while sample_reserve_mixnodes.len() < reserve_set_size {
let candidate = sample_candidate(&list_cumul_temp, &mut rng)?;
while sample_reserve_mixnodes.len() < reserve_set_size
&& sample_reserve_mixnodes.len() + sample_active_mixnodes.len() < list_cumul_temp.len()
{
let candidate = sample_candidate(&list_cumul_temp, rng)?;
if !sample_reserve_mixnodes.contains(&candidate)
&& !sample_active_mixnodes.contains(&candidate)
@@ -78,35 +108,49 @@ pub fn simulate_selection_probability_mixnodes(
// Convergence critera only on active set.
// We devide by samples to get the average, that is not really part of the delta
// computation.
delta_l2 = l2_diff(&active_set_probability, &active_set_probability_previous)?
/ f64::from(samples);
delta_max = max_diff(&active_set_probability, &active_set_probability_previous)?
/ f64::from(samples);
delta_l2 =
l2_diff(&active_set_probability, &active_set_probability_previous)? / (samples as f64);
delta_max =
max_diff(&active_set_probability, &active_set_probability_previous)? / (samples as f64);
if samples > 10 && delta_l2 < TOLERANCE_L2_NORM && delta_max < TOLERANCE_MAX_NORM
|| samples >= max_samples
{
break;
}
// Stop if we run out of time
if start_time.elapsed() > max_time {
log::debug!("Simulation ran out of time, stopping");
break;
}
}
// Divide occurrences with the number of samples once we're done to get the probabilities.
active_set_probability
.iter_mut()
.for_each(|x| *x /= f64::from(samples));
.for_each(|x| *x /= samples as f64);
reserve_set_probability
.iter_mut()
.for_each(|x| *x /= f64::from(samples));
.for_each(|x| *x /= samples as f64);
// Some sanity checks of the output
if active_set_probability.len() != num_mixnodes || reserve_set_probability.len() != num_mixnodes
{
return Err(Error::ResultsShorterThanInput);
}
Ok(SelectionProbability {
active_set_probability,
reserve_set_probability,
samples,
time: start_time.elapsed(),
delta_l2,
delta_max,
})
}
// Compute the cumulative sum
fn cumul_sum<'a>(list: impl IntoIterator<Item = &'a u64>) -> Vec<u64> {
fn cumul_sum<'a>(list: impl IntoIterator<Item = &'a u128>) -> Vec<u128> {
let mut list_cumul = Vec::new();
let mut cumul = 0;
for entry in list {
@@ -116,7 +160,10 @@ fn cumul_sum<'a>(list: impl IntoIterator<Item = &'a u64>) -> Vec<u64> {
list_cumul
}
fn sample_candidate(list_cumul: &[u64], rng: &mut rand::rngs::ThreadRng) -> Result<usize, Error> {
fn sample_candidate<R>(list_cumul: &[u128], rng: &mut R) -> Result<usize, Error>
where
R: Rng + ?Sized,
{
use rand::distributions::{Distribution, Uniform};
let uniform = Uniform::from(0..*list_cumul.last().ok_or(Error::EmptyListCumulStake)?);
let r = uniform.sample(rng);
@@ -132,7 +179,7 @@ fn sample_candidate(list_cumul: &[u64], rng: &mut rand::rngs::ThreadRng) -> Resu
}
// Update list of cumulative stake to reflect eliminating the picked node
fn remove_mixnode_from_cumul_stake(candidate: usize, list_cumul_stake: &mut [u64]) {
fn remove_mixnode_from_cumul_stake(candidate: usize, list_cumul_stake: &mut [u128]) {
let prob_candidate = if candidate == 0 {
list_cumul_stake[0]
} else {
@@ -171,8 +218,14 @@ fn max_diff(v1: &[f64], v2: &[f64]) -> Result<f64, Error> {
#[cfg(test)]
mod tests {
use rand::{rngs::StdRng, SeedableRng};
use super::*;
fn test_rng() -> StdRng {
StdRng::seed_from_u64(42)
}
#[test]
fn compute_cumul_sum() {
let v = cumul_sum(&vec![1, 2, 3]);
@@ -212,11 +265,14 @@ mod tests {
];
let max_samples = 100_000;
let max_time = Duration::from_secs(10);
let mut rng = test_rng();
let SelectionProbability {
active_set_probability,
reserve_set_probability,
samples,
time,
delta_l2,
delta_max,
} = simulate_selection_probability_mixnodes(
@@ -224,9 +280,15 @@ mod tests {
active_set_size,
standby_set_size,
max_samples,
max_time,
&mut rng,
)
.unwrap();
// Check that any possible test failure wasn't because we ran it on 1970s hardware, and the
// sampling aborted prematurely due to hitting `max_time`.
assert!(time < max_time);
// These values comes from running the python simulator for a very long time
let expected_active_set_probability = vec![
0.025_070_8,
@@ -271,7 +333,93 @@ mod tests {
);
// We converge around 20_000, add another 500 for some slack due to random values
assert!(samples < 20_500);
assert_eq!(samples, 20_001);
assert!(delta_l2 < TOLERANCE_L2_NORM);
assert!(delta_max < TOLERANCE_MAX_NORM);
}
#[test]
fn fewer_nodes_than_active_set_size() {
let active_set_size = 10;
let standby_set_size = 3;
let list_mix = vec![100, 100, 3000];
let max_samples = 100_000;
let max_time = Duration::from_secs(10);
let mut rng = test_rng();
let SelectionProbability {
active_set_probability,
reserve_set_probability,
samples,
time: _,
delta_l2,
delta_max,
} = simulate_selection_probability_mixnodes(
&list_mix,
active_set_size,
standby_set_size,
max_samples,
max_time,
&mut rng,
)
.unwrap();
// These values comes from running the python simulator for a very long time
let expected_active_set_probability = vec![1.0, 1.0, 1.0];
let expected_reserve_set_probability = vec![0.0, 0.0, 0.0];
assert!(
max_diff(&active_set_probability, &expected_active_set_probability).unwrap()
< 1e1 * f64::EPSILON
);
assert!(
max_diff(&reserve_set_probability, &expected_reserve_set_probability).unwrap()
< 1e1 * f64::EPSILON
);
// We converge around 20_000, add another 500 for some slack due to random values
assert_eq!(samples, 0);
assert!(delta_l2 < f64::EPSILON);
assert!(delta_max < f64::EPSILON);
}
#[test]
fn fewer_nodes_than_reward_set_size() {
let active_set_size = 4;
let standby_set_size = 3;
let list_mix = vec![100, 100, 3000, 342, 3_498_234];
let max_samples = 100_000_000;
let max_time = Duration::from_secs(10);
let mut rng = test_rng();
let SelectionProbability {
active_set_probability,
reserve_set_probability,
samples,
time: _,
delta_l2,
delta_max,
} = simulate_selection_probability_mixnodes(
&list_mix,
active_set_size,
standby_set_size,
max_samples,
max_time,
&mut rng,
)
.unwrap();
// These values comes from running the python simulator for a very long time
let expected_active_set_probability = vec![0.546, 0.538, 0.999, 0.915, 1.0];
let expected_reserve_set_probability = vec![0.453, 0.461, 0.0005, 0.084, 0.0];
assert!(
max_diff(&active_set_probability, &expected_active_set_probability).unwrap() < 1e-2,
);
assert!(
max_diff(&reserve_set_probability, &expected_reserve_set_probability).unwrap() < 1e-2,
);
// We converge around 20_000, add another 500 for some slack due to random values
assert_eq!(samples, 20_001);
assert!(delta_l2 < TOLERANCE_L2_NORM);
assert!(delta_max < TOLERANCE_MAX_NORM);
}
+1 -1
View File
@@ -42,7 +42,7 @@ pub enum CoconutError {
)]
DeserializationMinLength { min: usize, actual: usize },
#[error("Tried to deserialize {object} with bytes of invalid length. Expected {actual} < {} or {modulus_target} % {modulus} == 0")]
#[error("Tried to deserialize {object} with bytes of invalid length. Expected {actual} < {object} or {modulus_target} % {modulus} == 0")]
DeserializationInvalidLength {
actual: usize,
target: usize,
+4 -6
View File
@@ -213,7 +213,7 @@ where
/// - compute vk_b = g^x || v_b
/// - compute sphinx_plaintext = SURB_ACK || g^x || v_b
/// - compute sphinx_packet = Sphinx(recipient, sphinx_plaintext)
pub async fn prepare_chunk_for_sending(
pub fn prepare_chunk_for_sending(
&mut self,
fragment: Fragment,
topology: &NymTopology,
@@ -222,8 +222,7 @@ where
) -> Result<PreparedFragment, NymTopologyError> {
// create an ack
let (ack_delay, surb_ack_bytes) = self
.generate_surb_ack(fragment.fragment_identifier(), topology, ack_key)
.await?
.generate_surb_ack(fragment.fragment_identifier(), topology, ack_key)?
.prepare_for_sending();
// TODO:
@@ -294,7 +293,7 @@ where
}
/// Construct an acknowledgement SURB for the given [`FragmentIdentifier`]
async fn generate_surb_ack(
fn generate_surb_ack(
&mut self,
fragment_id: FragmentIdentifier,
topology: &NymTopology,
@@ -357,8 +356,7 @@ where
// gateways could not distinguish reply packets from normal messages due to lack of said acks
// note: the ack delay is irrelevant since we do not know the delay of actual surb
let (_, surb_ack_bytes) = self
.generate_surb_ack(reply_id, topology, ack_key)
.await?
.generate_surb_ack(reply_id, topology, ack_key)?
.prepare_for_sending();
let zero_pad_len = self.packet_size.plaintext_size()
+1
View File
@@ -9,3 +9,4 @@ edition = "2021"
[dependencies]
nymsphinx-addressing = { path = "../../../common/nymsphinx/addressing" }
ordered-buffer = {path = "../ordered-buffer"}
thiserror = "1"
+5
View File
@@ -1,7 +1,12 @@
// Copyright 2020-2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod msg;
pub mod network_requester_response;
pub mod request;
pub mod response;
pub use msg::*;
pub use network_requester_response::*;
pub use request::*;
pub use response::*;
+28 -15
View File
@@ -1,36 +1,40 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2020-2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use thiserror::Error;
use crate::network_requester_response::{Error as NrError, NetworkRequesterResponse};
use crate::request::{Request, RequestError};
use crate::response::{Response, ResponseError};
#[derive(Debug)]
#[derive(Debug, Error)]
pub enum MessageError {
#[error("{0}")]
Request(RequestError),
Response(ResponseError),
NoData,
UnknownMessageType,
}
impl std::fmt::Display for MessageError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MessageError::Request(r) => write!(f, "{}", r),
MessageError::Response(r) => write!(f, "{:?}", r),
MessageError::NoData => write!(f, "no data provided"),
MessageError::UnknownMessageType => write!(f, "unknown message type received"),
}
}
#[error("{0:?}")]
Response(ResponseError),
#[error("{0}")]
NetworkRequesterResponseError(NrError),
#[error("no data")]
NoData,
#[error("unknown message type received")]
UnknownMessageType,
}
pub enum Message {
Request(Request),
Response(Response),
NetworkRequesterResponse(NetworkRequesterResponse),
}
impl Message {
const REQUEST_FLAG: u8 = 0;
const RESPONSE_FLAG: u8 = 1;
const NR_RESPONSE_FLAG: u8 = 2;
pub fn conn_id(&self) -> u64 {
match self {
@@ -39,6 +43,7 @@ impl Message {
Request::Send(conn_id, _, _) => *conn_id,
},
Message::Response(resp) => resp.connection_id,
Message::NetworkRequesterResponse(resp) => resp.connection_id,
}
}
@@ -49,6 +54,7 @@ impl Message {
Request::Send(_, data, _) => data.len(),
},
Message::Response(resp) => resp.data.len(),
Message::NetworkRequesterResponse(_) => 0,
}
}
@@ -65,6 +71,10 @@ impl Message {
Response::try_from_bytes(&b[1..])
.map(Message::Response)
.map_err(MessageError::Response)
} else if b[0] == Self::NR_RESPONSE_FLAG {
NetworkRequesterResponse::try_from_bytes(&b[1..])
.map(Message::NetworkRequesterResponse)
.map_err(MessageError::NetworkRequesterResponseError)
} else {
Err(MessageError::UnknownMessageType)
}
@@ -78,6 +88,9 @@ impl Message {
Self::Response(r) => std::iter::once(Self::RESPONSE_FLAG)
.chain(r.into_bytes().iter().cloned())
.collect(),
Self::NetworkRequesterResponse(r) => std::iter::once(Self::NR_RESPONSE_FLAG)
.chain(r.into_bytes().iter().cloned())
.collect(),
}
}
}
@@ -0,0 +1,112 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::ConnectionId;
#[derive(Debug)]
pub struct NetworkRequesterResponse {
pub connection_id: ConnectionId,
pub network_requester_error: String,
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum Error {
#[error("no data provided")]
NoData,
#[error("not enough bytes to recover the connection id")]
ConnectionIdTooShort,
#[error("message is not utf8 encoded")]
MalformedErrorMessage(#[from] std::string::FromUtf8Error),
}
impl NetworkRequesterResponse {
pub fn new(connection_id: ConnectionId, network_requester_error: String) -> Self {
NetworkRequesterResponse {
connection_id,
network_requester_error,
}
}
pub fn try_from_bytes(b: &[u8]) -> Result<NetworkRequesterResponse, Error> {
if b.is_empty() {
return Err(Error::NoData);
}
if b.len() < 8 {
return Err(Error::ConnectionIdTooShort);
}
let mut connection_id_bytes = b.to_vec();
let network_requester_error_bytes = connection_id_bytes.split_off(8);
let connection_id = u64::from_be_bytes([
connection_id_bytes[0],
connection_id_bytes[1],
connection_id_bytes[2],
connection_id_bytes[3],
connection_id_bytes[4],
connection_id_bytes[5],
connection_id_bytes[6],
connection_id_bytes[7],
]);
let network_requester_error = String::from_utf8(network_requester_error_bytes)?;
Ok(NetworkRequesterResponse {
connection_id,
network_requester_error,
})
}
pub fn into_bytes(self) -> Vec<u8> {
self.connection_id
.to_be_bytes()
.iter()
.cloned()
.chain(self.network_requester_error.into_bytes().into_iter())
.collect()
}
}
#[cfg(test)]
mod network_requester_response_serde_tests {
use super::*;
#[test]
fn simple_serde() {
let conn_id = 42;
let network_requester_error = String::from("This is a test msg");
let response = NetworkRequesterResponse::new(conn_id, network_requester_error.clone());
let bytes = response.into_bytes();
let deserialized_response = NetworkRequesterResponse::try_from_bytes(&bytes).unwrap();
assert_eq!(conn_id, deserialized_response.connection_id);
assert_eq!(
network_requester_error,
deserialized_response.network_requester_error
);
}
#[test]
fn deserialization_errors() {
let err = NetworkRequesterResponse::try_from_bytes(&[]).err().unwrap();
assert_eq!(err, Error::NoData);
let bytes: [u8; 5] = [1, 2, 3, 4, 5];
let err = NetworkRequesterResponse::try_from_bytes(&bytes)
.err()
.unwrap();
assert_eq!(err, Error::ConnectionIdTooShort);
let bytes: Vec<u8> = 42u64
.to_be_bytes()
.into_iter()
.chain([0, 159, 146, 150].into_iter())
.collect();
let err = NetworkRequesterResponse::try_from_bytes(&bytes)
.err()
.unwrap();
assert!(matches!(err, Error::MalformedErrorMessage(_)));
}
}
+18 -24
View File
@@ -1,6 +1,9 @@
// Copyright 2020-2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nymsphinx_addressing::clients::{Recipient, RecipientFormattingError};
use std::convert::TryFrom;
use std::fmt::{self};
use thiserror::Error;
pub type ConnectionId = u64;
pub type RemoteAddress = String;
@@ -12,39 +15,30 @@ pub enum RequestFlag {
Send = 1,
}
#[derive(Debug)]
#[derive(Debug, Error)]
pub enum RequestError {
#[error("not enough bytes to recover the length of the address")]
AddressLengthTooShort,
#[error("not enough bytes to recover the address")]
AddressTooShort,
#[error("not enough bytes to recover the connection id")]
ConnectionIdTooShort,
#[error("no data provided")]
NoData,
#[error("request of unknown type")]
UnknownRequestFlag,
#[error("too short return address")]
ReturnAddressTooShort,
#[error("malformed return address - {0}")]
MalformedReturnAddress(RecipientFormattingError),
}
impl fmt::Display for RequestError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
match self {
RequestError::AddressLengthTooShort => {
write!(f, "not enough bytes to recover the length of the address")
}
RequestError::AddressTooShort => write!(f, "not enough bytes to recover the address"),
RequestError::ConnectionIdTooShort => {
write!(f, "not enough bytes to recover the connection id")
}
RequestError::NoData => write!(f, "no data provided"),
RequestError::UnknownRequestFlag => write!(f, "request of unknown type"),
RequestError::ReturnAddressTooShort => write!(f, "too short return address"),
RequestError::MalformedReturnAddress(recipient_err) => {
write!(f, "malformed return address - {}", recipient_err)
}
}
}
}
impl std::error::Error for RequestError {}
impl RequestError {
pub fn is_malformed_return(&self) -> bool {
matches!(self, RequestError::MalformedReturnAddress(_))
+8 -1
View File
@@ -1,8 +1,15 @@
// Copyright 2020-2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use thiserror::Error;
use crate::ConnectionId;
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, Error, PartialEq, Eq)]
pub enum ResponseError {
#[error("not enough bytes to recover the connection id")]
ConnectionIdTooShort,
#[error("no data provided")]
NoData,
}
/// A remote network response retrieved by the Socks5 service provider. This
+11 -2
View File
@@ -5,13 +5,14 @@ use std::time::Duration;
use tokio::sync::watch::{self, error::SendError};
const SHUTDOWN_TIMER_SECS: u64 = 5;
const DEFAULT_SHUTDOWN_TIMER_SECS: u64 = 5;
/// Used to notify other tasks to gracefully shutdown
#[derive(Debug)]
pub struct ShutdownNotifier {
notify_tx: watch::Sender<()>,
notify_rx: Option<watch::Receiver<()>>,
shutdown_timer_secs: u64,
}
impl Default for ShutdownNotifier {
@@ -20,11 +21,19 @@ impl Default for ShutdownNotifier {
Self {
notify_tx,
notify_rx: Some(notify_rx),
shutdown_timer_secs: DEFAULT_SHUTDOWN_TIMER_SECS,
}
}
}
impl ShutdownNotifier {
pub fn new(shutdown_timer_secs: u64) -> Self {
Self {
shutdown_timer_secs,
..Default::default()
}
}
pub fn subscribe(&self) -> ShutdownListener {
ShutdownListener::new(
self.notify_rx
@@ -50,7 +59,7 @@ impl ShutdownNotifier {
_ = tokio::signal::ctrl_c() => {
log::info!("Forcing shutdown");
}
_ = tokio::time::sleep(Duration::from_secs(SHUTDOWN_TIMER_SECS)) => {
_ = tokio::time::sleep(Duration::from_secs(self.shutdown_timer_secs)) => {
log::info!("Timout reached, forcing shutdown");
},
}
+1 -1
View File
@@ -89,7 +89,7 @@ impl Node {
}
pub fn clients_address(&self) -> String {
format!("ws://{}:{}", self.host, self.clients_port)
format!("wss://{}:{}", self.host, self.clients_port)
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ wasm-bindgen-futures = "0.4"
# we don't want entire tokio-tungstenite, tungstenite itself is just fine - we just want message and error enums
[dependencies.tungstenite]
version = "0.13"
version = "0.17.3"
default-features = false
[dependencies.web-sys]
+5
View File
@@ -85,6 +85,11 @@ pub struct JSWebsocket {
impl JSWebsocket {
pub fn new(url: &str) -> Result<Self, JsValue> {
console_log!(
"Attempting to establish wasm websocket connection to {}",
url
);
let ws = WebSocket::new(url)?;
// we don't want to ever have to deal with blobs
ws.set_binary_type(web_sys::BinaryType::Arraybuffer);
+19
View File
@@ -1,3 +1,22 @@
## Unreleased
### Added
- vesting-contract: added queries for delegation timestamps and paged query for all vesting delegations in the contract ([#1569])
### Changed
- mixnet-contract: compounding delegator rewards now happens instantaneously as opposed to having to wait for the current epoch to finish ([#1571])
### Fixed
- vesting-contract: the contract now correctly stores delegations with their timestamp as opposed to using block height ([#1544])
- mixnet-contract: compounding delegator rewards is now possible even if the associated mixnode had already unbonded ([#1571])
[#1544]: https://github.com/nymtech/nym/pull/1544
[#1569]: https://github.com/nymtech/nym/pull/1569
[#1569]: https://github.com/nymtech/nym/pull/1571
## [nym-contracts-v1.0.1](https://github.com/nymtech/nym/tree/nym-contracts-v1.0.1) (2022-06-22)
### Added
+24 -23
View File
@@ -8,7 +8,6 @@ use super::storage::{
use crate::constants;
use crate::contract::debug_with_visibility;
use crate::delegations::storage as delegations_storage;
use crate::delegations::transactions::_try_delegate_to_mixnode;
use crate::error::ContractError;
use crate::mixnet_contract_settings::storage::mix_denom;
use crate::mixnodes::storage::mixnodes;
@@ -18,7 +17,7 @@ use crate::rewards::helpers;
use crate::support::helpers::{is_authorized, operator_cost_at_epoch};
use cosmwasm_std::{
coins, wasm_execute, Addr, Api, BankMsg, Coin, DepsMut, Env, MessageInfo, Order, Response,
Storage, Uint128,
StdResult, Storage, Uint128,
};
use cw_storage_plus::Bound;
use mixnet_contract_common::events::{
@@ -450,18 +449,15 @@ pub fn try_compound_delegator_reward(
pub fn _try_compound_delegator_reward(
block_height: u64,
mut deps: DepsMut<'_>,
deps: DepsMut<'_>,
owner_address: &str,
mix_identity: &str,
proxy: Option<Addr>,
) -> Result<Uint128, ContractError> {
let delegation_map = crate::delegations::storage::delegations();
let mix_denom = mix_denom(deps.storage)?;
let key = mixnet_contract_common::delegation::generate_storage_key(
&deps.api.addr_validate(owner_address)?,
proxy.as_ref(),
);
let owner = deps.api.addr_validate(owner_address)?;
let key = mixnet_contract_common::delegation::generate_storage_key(&owner, proxy.as_ref());
let reward = calculate_delegator_reward(deps.storage, deps.api, key.clone(), mix_identity)?;
let mut total_delegation_delegate = Uint128::zero();
@@ -469,8 +465,7 @@ pub fn _try_compound_delegator_reward(
let delegation_heights = delegation_map
.prefix((mix_identity.to_string(), key.clone()))
.keys(deps.storage, None, None, cosmwasm_std::Order::Ascending)
.filter_map(|v| v.ok())
.collect::<Vec<u64>>();
.collect::<StdResult<Vec<_>>>()?;
for h in delegation_heights {
let delegation =
@@ -494,30 +489,36 @@ pub fn _try_compound_delegator_reward(
// since we know that the target node exists and because the total_delegation bucket
// entry is created whenever the node itself is added, the unwrap here is fine
// as the entry MUST exist
Ok(total_delegation
.unwrap()
.saturating_sub(total_delegation_delegate))
Ok(total_delegation.unwrap() + reward)
},
)?;
_try_delegate_to_mixnode(
deps.branch(),
block_height,
mix_identity,
owner_address,
// let's simplify the entire procedure. Rather than creating a fresh delegation on the mixnode
// via `_try_delegate_to_mixnode` and then waiting for reconcile to happen,
// just save it directly to the storage right now.
// my reasoning for that is simple: `_try_delegate_to_mixnode` could fail if the node the
// delegator has delegated to no longer exists.
let delegation = Delegation::new(
owner,
mix_identity.into(),
Coin {
amount: compounded_delegation,
denom: mix_denom,
},
block_height,
proxy,
);
delegation_map.save(
deps.storage,
(mix_identity.into(), key.clone(), block_height),
&delegation,
)?;
}
{
if let Some(mut bond) = mixnodes().may_load(deps.storage, mix_identity)? {
bond.accumulated_rewards = Some(bond.accumulated_rewards().saturating_sub(reward));
mixnodes().save(deps.storage, mix_identity, &bond, block_height)?;
}
if let Some(mut bond) = mixnodes().may_load(deps.storage, mix_identity)? {
bond.accumulated_rewards = Some(bond.accumulated_rewards().saturating_sub(reward));
mixnodes().save(deps.storage, mix_identity, &bond, block_height)?;
}
DELEGATOR_REWARD_CLAIMED_HEIGHT.save(
+73 -5
View File
@@ -1,17 +1,18 @@
use crate::errors::ContractError;
use crate::queued_migrations::migrate_config_from_env;
use crate::storage::{
account_from_address, locked_pledge_cap, update_locked_pledge_cap, ADMIN,
MIXNET_CONTRACT_ADDRESS, MIX_DENOM,
account_from_address, locked_pledge_cap, update_locked_pledge_cap, BlockTimestampSecs, ADMIN,
DELEGATIONS, MIXNET_CONTRACT_ADDRESS, MIX_DENOM,
};
use crate::traits::{
DelegatingAccount, GatewayBondingAccount, MixnodeBondingAccount, VestingAccount,
};
use crate::vesting::{populate_vesting_periods, Account};
use cosmwasm_std::{
coin, entry_point, to_binary, BankMsg, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse,
Response, Timestamp, Uint128,
coin, entry_point, to_binary, BankMsg, Coin, Deps, DepsMut, Env, MessageInfo, Order,
QueryResponse, Response, StdResult, Timestamp, Uint128,
};
use cw_storage_plus::Bound;
use mixnet_contract_common::{Gateway, IdentityKey, MixNode};
use vesting_contract_common::events::{
new_ownership_transfer_event, new_periodic_vesting_account_event,
@@ -22,7 +23,10 @@ use vesting_contract_common::events::{
use vesting_contract_common::messages::{
ExecuteMsg, InitMsg, MigrateMsg, QueryMsg, VestingSpecification,
};
use vesting_contract_common::{OriginalVestingResponse, Period, PledgeData};
use vesting_contract_common::{
AllDelegationsResponse, DelegationTimesResponse, OriginalVestingResponse, Period, PledgeData,
VestingDelegation,
};
pub const INITIAL_LOCKED_PLEDGE_CAP: Uint128 = Uint128::new(100_000_000_000);
@@ -518,6 +522,13 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result<QueryResponse, C
QueryMsg::GetCurrentVestingPeriod { address } => {
to_binary(&try_get_current_vesting_period(&address, deps, env)?)
}
QueryMsg::GetDelegationTimes {
address,
mix_identity,
} => to_binary(&try_get_delegation_times(deps, &address, mix_identity)?),
QueryMsg::GetAllDelegations { start_after, limit } => {
to_binary(&try_get_all_delegations(deps, start_after, limit)?)
}
};
Ok(query_res?)
@@ -634,6 +645,63 @@ pub fn try_get_delegated_vesting(
account.get_delegated_vesting(block_time, &env, deps.storage)
}
pub fn try_get_delegation_times(
deps: Deps<'_>,
vesting_account_address: &str,
mix_identity: String,
) -> Result<DelegationTimesResponse, ContractError> {
let owner = deps.api.addr_validate(vesting_account_address)?;
let account = account_from_address(vesting_account_address, deps.storage, deps.api)?;
let delegation_timestamps = DELEGATIONS
.prefix((account.storage_key(), mix_identity.clone()))
.keys(deps.storage, None, None, Order::Ascending)
.collect::<StdResult<Vec<_>>>()?;
Ok(DelegationTimesResponse {
owner,
account_id: account.storage_key(),
mix_identity,
delegation_timestamps,
})
}
pub fn try_get_all_delegations(
deps: Deps<'_>,
start_after: Option<(u32, IdentityKey, BlockTimestampSecs)>,
limit: Option<u32>,
) -> Result<AllDelegationsResponse, ContractError> {
let limit = limit.unwrap_or(100).min(200) as usize;
let start = start_after.map(Bound::exclusive);
let delegations = DELEGATIONS
.range(deps.storage, start, None, Order::Ascending)
.map(|kv| {
kv.map(
|((account_id, mix_identity, block_timestamp), amount)| VestingDelegation {
account_id,
mix_identity,
block_timestamp,
amount,
},
)
})
.collect::<StdResult<Vec<_>>>()?;
let start_next_after = if delegations.len() < limit {
None
} else {
delegations
.last()
.map(|delegation| delegation.storage_key())
};
Ok(AllDelegationsResponse {
delegations,
start_next_after,
})
}
fn validate_funds(funds: &[Coin], mix_denom: String) -> Result<Coin, ContractError> {
if funds.is_empty() || funds[0].amount.is_zero() {
return Err(ContractError::EmptyFunds);
+4 -4
View File
@@ -5,7 +5,7 @@ use cw_storage_plus::{Item, Map};
use mixnet_contract_common::IdentityKey;
use vesting_contract_common::PledgeData;
type BlockHeight = u64;
pub(crate) type BlockTimestampSecs = u64;
pub const KEY: Item<'_, u32> = Item::new("key");
const ACCOUNTS: Map<'_, String, Account> = Map::new("acc");
@@ -14,7 +14,7 @@ const BALANCES: Map<'_, u32, Uint128> = Map::new("blc");
const WITHDRAWNS: Map<'_, u32, Uint128> = Map::new("wthd");
const BOND_PLEDGES: Map<'_, u32, PledgeData> = Map::new("bnd");
const GATEWAY_PLEDGES: Map<'_, u32, PledgeData> = Map::new("gtw");
pub const DELEGATIONS: Map<'_, (u32, IdentityKey, BlockHeight), Uint128> = Map::new("dlg");
pub const DELEGATIONS: Map<'_, (u32, IdentityKey, BlockTimestampSecs), Uint128> = Map::new("dlg");
pub const ADMIN: Item<'_, String> = Item::new("adm");
pub const MIXNET_CONTRACT_ADDRESS: Item<'_, String> = Item::new("mix");
pub const MIX_DENOM: Item<'_, String> = Item::new("den");
@@ -35,7 +35,7 @@ pub fn update_locked_pledge_cap(
}
pub fn save_delegation(
key: (u32, IdentityKey, BlockHeight),
key: (u32, IdentityKey, BlockTimestampSecs),
amount: Uint128,
storage: &mut dyn Storage,
) -> Result<(), ContractError> {
@@ -44,7 +44,7 @@ pub fn save_delegation(
}
pub fn remove_delegation(
key: (u32, IdentityKey, BlockHeight),
key: (u32, IdentityKey, BlockTimestampSecs),
storage: &mut dyn Storage,
) -> Result<(), ContractError> {
DELEGATIONS.remove(storage, key);
@@ -88,7 +88,7 @@ impl DelegatingAccount for Account {
vec![coin.clone()],
)?;
self.track_delegation(
env.block.height,
env.block.time.seconds(),
mix_identity,
current_balance,
coin,
@@ -129,14 +129,14 @@ impl DelegatingAccount for Account {
fn track_delegation(
&self,
block_height: u64,
block_timestamp_secs: u64,
mix_identity: IdentityKey,
current_balance: Uint128,
delegation: Coin,
storage: &mut dyn Storage,
) -> Result<(), ContractError> {
save_delegation(
(self.storage_key(), mix_identity, block_height),
(self.storage_key(), mix_identity, block_timestamp_secs),
delegation.amount,
storage,
)?;
+16 -1
View File
@@ -3,7 +3,7 @@ use crate::errors::ContractError;
use crate::storage::{
load_balance, load_bond_pledge, load_gateway_pledge, load_withdrawn, remove_bond_pledge,
remove_delegation, remove_gateway_pledge, save_account, save_balance, save_bond_pledge,
save_gateway_pledge, save_withdrawn, DELEGATIONS, KEY,
save_gateway_pledge, save_withdrawn, BlockTimestampSecs, DELEGATIONS, KEY,
};
use cosmwasm_std::{Addr, Coin, Order, Storage, Timestamp, Uint128};
use cw_storage_plus::Bound;
@@ -261,4 +261,19 @@ impl Account {
.filter_map(|x| x.ok())
.fold(Uint128::zero(), |acc, (_key, val)| acc + val))
}
pub fn total_delegations_at_timestamp(
&self,
storage: &dyn Storage,
start_time: BlockTimestampSecs,
) -> Result<Uint128, ContractError> {
Ok(DELEGATIONS
.sub_prefix(self.storage_key())
.range(storage, None, None, Order::Ascending)
.filter_map(|x| x.ok())
.filter(|((_mix, block_time), _amount)| *block_time <= start_time)
.fold(Uint128::zero(), |acc, ((_mix, _block_time), amount)| {
acc + amount
}))
}
}
@@ -1,7 +1,7 @@
use crate::errors::ContractError;
use crate::storage::{delete_account, save_account, DELEGATIONS, MIX_DENOM};
use crate::storage::{delete_account, save_account, MIX_DENOM};
use crate::traits::VestingAccount;
use cosmwasm_std::{Addr, Coin, Env, Order, Storage, Timestamp, Uint128};
use cosmwasm_std::{Addr, Coin, Env, Storage, Timestamp, Uint128};
use vesting_contract_common::{OriginalVestingResponse, Period};
use super::Account;
@@ -16,13 +16,6 @@ impl VestingAccount for Account {
+ self.get_pledged_vesting(None, env, storage)?.amount)
}
fn track_reward(&self, amount: Coin, storage: &mut dyn Storage) -> Result<(), ContractError> {
let current_balance = self.load_balance(storage)?;
let new_balance = current_balance + amount.amount;
self.save_balance(new_balance, storage)?;
Ok(())
}
fn locked_coins(
&self,
block_time: Option<Timestamp>,
@@ -141,14 +134,7 @@ impl VestingAccount for Account {
Period::In(idx) => self.periods[idx as usize].start_time,
};
let coin = DELEGATIONS
.sub_prefix(self.storage_key())
.range(storage, None, None, Order::Ascending)
.filter_map(|x| x.ok())
.filter(|((_mix, block_time), _amount)| *block_time < start_time)
.fold(Uint128::zero(), |acc, ((_mix, _block_time), amount)| {
acc + amount
});
let coin = self.total_delegations_at_timestamp(storage, start_time)?;
let amount = Uint128::new(coin.u128().min(max_available.u128()));
@@ -158,6 +144,7 @@ impl VestingAccount for Account {
})
}
// TODO: why do we allow querying for block times in the past? - just use env.block.time all the time
fn get_delegated_vesting(
&self,
block_time: Option<Timestamp>,
@@ -166,9 +153,18 @@ impl VestingAccount for Account {
) -> Result<Coin, ContractError> {
let block_time = block_time.unwrap_or(env.block.time);
let delegated_free = self.get_delegated_free(Some(block_time), env, storage)?;
let total_delegations = self.total_delegations(storage)?;
let amount = total_delegations - delegated_free.amount;
let period = self.get_current_vesting_period(block_time);
let start_time = match period {
Period::Before => 0,
Period::After => u64::MAX,
Period::In(idx) => self.periods[idx as usize].start_time,
};
let delegations_before_start_time =
self.total_delegations_at_timestamp(storage, start_time)?;
let amount = delegations_before_start_time - delegated_free.amount;
Ok(Coin {
amount,
@@ -261,4 +257,11 @@ impl VestingAccount for Account {
save_account(self, storage)?;
Ok(())
}
fn track_reward(&self, amount: Coin, storage: &mut dyn Storage) -> Result<(), ContractError> {
let current_balance = self.load_balance(storage)?;
let new_balance = current_balance + amount.amount;
self.save_balance(new_balance, storage)?;
Ok(())
}
}
+169 -1
View File
@@ -44,10 +44,11 @@ mod tests {
use crate::traits::DelegatingAccount;
use crate::traits::VestingAccount;
use crate::traits::{GatewayBondingAccount, MixnodeBondingAccount};
use crate::vesting::{populate_vesting_periods, Account};
use cosmwasm_std::testing::{mock_env, mock_info};
use cosmwasm_std::{coins, Addr, Coin, Timestamp, Uint128};
use mixnet_contract_common::{Gateway, MixNode};
use vesting_contract_common::messages::ExecuteMsg;
use vesting_contract_common::messages::{ExecuteMsg, VestingSpecification};
use vesting_contract_common::Period;
#[test]
@@ -757,4 +758,171 @@ mod tests {
.unwrap();
assert_eq!(Uint128::zero(), bonded_vesting.amount);
}
#[test]
fn delegated_free() {
let mut deps = init_contract();
let mut env = mock_env();
let vesting_period_length_secs = 3600;
let account_creation_timestamp = 1650000000;
let account_creation_blockheight = 12345;
// this value is completely arbitrary, I just wanted to keep consistent
// (and make sure that if block timestamp increases so does the block height)
let blocks_per_period = 100;
env.block.height = account_creation_blockheight;
env.block.time = Timestamp::from_seconds(account_creation_timestamp);
// lets define some helper timestamps
// our account is set to be created after 2 vesting periods already passed
let vesting_start_blockheight = account_creation_blockheight - 2 * blocks_per_period;
let vesting_start_timestamp = account_creation_timestamp - 2 * vesting_period_length_secs;
let vesting_period2_start_blockheight = vesting_start_blockheight + blocks_per_period;
let vesting_period2_start_timestamp = vesting_start_timestamp + vesting_period_length_secs;
// this vesting period is currently in progress!
let vesting_period3_start_blockheight =
vesting_period2_start_blockheight + blocks_per_period;
let vesting_period3_start_timestamp =
vesting_period2_start_timestamp + vesting_period_length_secs;
// and this one is in the future! (in relation to account creation)
let vesting_period4_start_blockheight =
vesting_period3_start_blockheight + blocks_per_period;
let vesting_period4_start_timestamp =
vesting_period3_start_timestamp + vesting_period_length_secs;
// lets create our vesting account
let periods = populate_vesting_periods(
vesting_start_timestamp,
VestingSpecification::new(None, Some(vesting_period_length_secs), None),
);
let vesting_account = Account::new(
Addr::unchecked("owner"),
Some(Addr::unchecked("staking")),
Coin {
amount: Uint128::new(1_000_000_000_000),
denom: TEST_COIN_DENOM.to_string(),
},
Timestamp::from_seconds(account_creation_timestamp),
periods,
deps.as_mut().storage,
)
.unwrap();
// time for some delegations
let mix_identity = "alice".to_string();
let delegation = Coin {
amount: Uint128::new(90_000_000_000),
denom: TEST_COIN_DENOM.to_string(),
};
// delegate explicitly at the time the account was created
// (i.e. after 2 vesting periods already elapsed)
env.block.height = account_creation_blockheight;
env.block.time = Timestamp::from_seconds(account_creation_timestamp);
let ok = vesting_account.try_delegate_to_mixnode(
mix_identity.clone(),
delegation.clone(),
&env,
&mut deps.storage,
);
assert!(ok.is_ok());
let vested_coins = vesting_account
.get_vested_coins(None, &env, &deps.storage)
.unwrap();
let vesting_coins = vesting_account
.get_vesting_coins(None, &env, &deps.storage)
.unwrap();
assert_eq!(vested_coins.amount, Uint128::new(250_000_000_000));
assert_eq!(vesting_coins.amount, Uint128::new(750_000_000_000));
let delegated_free = vesting_account
.get_delegated_free(None, &env, &deps.storage)
.unwrap();
let delegated_vesting = vesting_account
.get_delegated_vesting(None, &env, &deps.storage)
.unwrap();
// all good so far
assert_eq!(delegated_free.amount, Uint128::new(90_000_000_000));
assert_eq!(delegated_vesting.amount, Uint128::zero());
// some time passes, and we're now into the next vesting period, more of our coins got unlocked!
env.block.height = vesting_period4_start_blockheight;
env.block.time = Timestamp::from_seconds(vesting_period4_start_timestamp);
let vested_coins = vesting_account
.get_vested_coins(None, &env, &deps.storage)
.unwrap();
let vesting_coins = vesting_account
.get_vesting_coins(None, &env, &deps.storage)
.unwrap();
assert_eq!(vested_coins.amount, Uint128::new(375_000_000_000));
assert_eq!(vesting_coins.amount, Uint128::new(625_000_000_000));
// and nothing about our existing delegation changed
let delegated_free = vesting_account
.get_delegated_free(None, &env, &deps.storage)
.unwrap();
let delegated_vesting = vesting_account
.get_delegated_vesting(None, &env, &deps.storage)
.unwrap();
assert_eq!(delegated_free.amount, Uint128::new(90_000_000_000));
assert_eq!(delegated_vesting.amount, Uint128::zero());
// however, create a new delegation now in this brand new vesting period
let delegation = Coin {
amount: Uint128::new(50_000_000_000),
denom: TEST_COIN_DENOM.to_string(),
};
let ok = vesting_account.try_delegate_to_mixnode(
mix_identity.clone(),
delegation.clone(),
&env,
&mut deps.storage,
);
assert!(ok.is_ok());
// we're still good here, we have delegated in total 140M from our vested tokens!
let delegated_free = vesting_account
.get_delegated_free(None, &env, &deps.storage)
.unwrap();
let delegated_vesting = vesting_account
.get_delegated_vesting(None, &env, &deps.storage)
.unwrap();
assert_eq!(delegated_free.amount, Uint128::new(140_000_000_000));
assert_eq!(delegated_vesting.amount, Uint128::zero());
// but let's ask now a different question:
// how many vested tokens have I had delegated during vesting period3? (i.e. after account creation)
let delegated_free = vesting_account
.get_delegated_free(
Some(Timestamp::from_seconds(vesting_period3_start_timestamp)),
&env,
&deps.storage,
)
.unwrap();
let delegated_vesting = vesting_account
.get_delegated_vesting(
Some(Timestamp::from_seconds(vesting_period3_start_timestamp)),
&env,
&deps.storage,
)
.unwrap();
// returns 90M as the 50M delegation didn't exist at this point of time
assert_eq!(delegated_free.amount, Uint128::new(90_000_000_000));
// the 50M delegation wasn't a thing here for VESTING tokens either
assert_eq!(delegated_vesting.amount, Uint128::zero());
}
}
+1 -1
View File
@@ -33,7 +33,7 @@ subtle-encoding = { version = "0.5", features = ["bech32-preview"]}
thiserror = "1"
tokio = { version = "1.19.1", features = [ "rt-multi-thread", "net", "signal", "fs" ] }
tokio-stream = { version = "0.1.9", features = [ "fs" ] }
tokio-tungstenite = "0.14"
tokio-tungstenite = "0.17"
tokio-util = { version = "0.7.3", features = [ "codec" ] }
url = { version = "2.2", features = [ "serde" ] }
web3 = "0.17.0"
+1 -1
View File
@@ -29,6 +29,6 @@ credentials = { path = "../../common/credentials" }
coconut = ["coconut-interface", "credentials/coconut"]
[dependencies.tungstenite]
version = "0.13.0"
version = "0.17.3"
default-features = false
@@ -9,7 +9,8 @@ pub use self::shared_key::{SharedKeySize, SharedKeys};
use crypto::asymmetric::identity;
use futures::{Sink, Stream};
use rand::{CryptoRng, RngCore};
use tungstenite::{Error as WsError, Message as WsMessage};
use tungstenite::error::Error as WsError;
use tungstenite::protocol::Message as WsMessage;
pub(crate) type WsItem = Result<WsMessage, WsError>;
+8 -9
View File
@@ -18,7 +18,6 @@ use std::{
convert::{TryFrom, TryInto},
fmt::{self, Error, Formatter},
};
use tungstenite::protocol::Message;
#[cfg(feature = "coconut")]
use coconut_interface::Credential;
@@ -192,12 +191,12 @@ impl ClientControlRequest {
}
}
impl From<ClientControlRequest> for Message {
impl From<ClientControlRequest> for tungstenite::Message {
fn from(req: ClientControlRequest) -> Self {
// it should be safe to call `unwrap` here as the message is generated by the server
// so if it fails (and consequently panics) it's a bug that should be resolved
let str_req = serde_json::to_string(&req).unwrap();
Message::Text(str_req)
tungstenite::Message::Text(str_req)
}
}
@@ -258,12 +257,12 @@ impl ServerResponse {
}
}
impl From<ServerResponse> for Message {
impl From<ServerResponse> for tungstenite::Message {
fn from(res: ServerResponse) -> Self {
// it should be safe to call `unwrap` here as the message is generated by the server
// so if it fails (and consequently panics) it's a bug that should be resolved
let str_res = serde_json::to_string(&res).unwrap();
Message::Text(str_res)
tungstenite::Message::Text(str_res)
}
}
@@ -314,8 +313,8 @@ impl BinaryRequest {
BinaryRequest::ForwardSphinx(mix_packet)
}
pub fn into_ws_message(self, shared_key: &SharedKeys) -> Message {
Message::Binary(self.into_encrypted_tagged_bytes(shared_key))
pub fn into_ws_message(self, shared_key: &SharedKeys) -> tungstenite::Message {
tungstenite::Message::Binary(self.into_encrypted_tagged_bytes(shared_key))
}
}
@@ -367,8 +366,8 @@ impl BinaryResponse {
BinaryResponse::PushedMixMessage(msg)
}
pub fn into_ws_message(self, shared_key: &SharedKeys) -> Message {
Message::Binary(self.into_encrypted_tagged_bytes(shared_key))
pub fn into_ws_message(self, shared_key: &SharedKeys) -> tungstenite::Message {
tungstenite::Message::Binary(self.into_encrypted_tagged_bytes(shared_key))
}
}
+3 -3
View File
@@ -52,7 +52,7 @@ pub struct Init {
mnemonic: Option<String>,
/// Set this gateway to work in a enabled credentials mode that would disallow clients to bypass bandwidth credential requirement
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[cfg(any(feature = "eth", feature = "coconut"))]
#[clap(long)]
enabled_credentials_mode: Option<bool>,
@@ -83,7 +83,7 @@ impl From<Init> for OverrideConfig {
validators: init_config.validators,
mnemonic: init_config.mnemonic,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[cfg(any(feature = "eth", feature = "coconut"))]
enabled_credentials_mode: init_config.enabled_credentials_mode,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
@@ -177,7 +177,7 @@ mod tests {
mnemonic: None,
statistics_service_url: None,
enabled_statistics: None,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[cfg(any(feature = "eth", feature = "coconut"))]
enabled_credentials_mode: None,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
eth_endpoint: "".to_string(),
+8 -11
View File
@@ -52,7 +52,7 @@ pub(crate) struct OverrideConfig {
validators: Option<String>,
mnemonic: Option<String>,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[cfg(any(feature = "eth", feature = "coconut"))]
enabled_credentials_mode: Option<bool>,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
@@ -118,8 +118,8 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi
config = config.with_custom_validator_apis(parse_validators(&raw_validators))
}
if let Some(raw_validators) = args.validators {
config = config.with_custom_validator_nymd(parse_validators(&raw_validators));
if let Some(ref raw_validators) = args.validators {
config = config.with_custom_validator_nymd(parse_validators(raw_validators));
} else if std::env::var(CONFIGURED).is_ok() {
let raw_validators = std::env::var(NYMD_VALIDATOR).expect("nymd validator not set");
config = config.with_custom_validator_nymd(parse_validators(&raw_validators))
@@ -146,18 +146,15 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi
config = config.with_eth_endpoint(String::from(DEFAULT_ETH_ENDPOINT));
}
// We set the disabled credentials mode flag if we either compile without 'eth', or if there is a flag we
// can read from, which is when we build with 'eth' (and without 'coconut').
if cfg!(not(feature = "eth")) {
config = config.with_disabled_credentials_mode(true);
#[cfg(any(feature = "eth", feature = "coconut"))]
{
if let Some(enabled_credentials_mode) = args.enabled_credentials_mode {
config = config.with_disabled_credentials_mode(!enabled_credentials_mode);
}
}
#[cfg(all(feature = "eth", not(feature = "coconut")))]
{
if let Some(enabled_credentials_mode) = args.enabled_credentials_mode {
config = config.with_disabled_credentials_mode(enabled_credentials_mode);
}
if let Some(raw_validators) = args.validators {
config = config.with_custom_validator_nymd(parse_validators(&raw_validators));
}
+2 -2
View File
@@ -52,7 +52,7 @@ pub struct Run {
mnemonic: Option<String>,
/// Set this gateway to work in a enabled credentials mode that would disallow clients to bypass bandwidth credential requirement
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[cfg(any(feature = "eth", feature = "coconut"))]
#[clap(long)]
enabled_credentials_mode: Option<bool>,
@@ -83,7 +83,7 @@ impl From<Run> for OverrideConfig {
validators: run_config.validators,
mnemonic: run_config.mnemonic,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[cfg(any(feature = "eth", feature = "coconut"))]
enabled_credentials_mode: run_config.enabled_credentials_mode,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
+5
View File
@@ -66,6 +66,10 @@ impl NymConfig for Config {
.join("gateways")
}
fn try_default_root_directory() -> Option<PathBuf> {
dirs::home_dir().map(|path| path.join(".nym").join("gateways"))
}
fn root_directory(&self) -> PathBuf {
self.gateway.nym_root_directory.clone()
}
@@ -123,6 +127,7 @@ impl Config {
self
}
#[cfg(any(feature = "eth", feature = "coconut"))]
pub fn with_disabled_credentials_mode(mut self, disabled_credentials_mode: bool) -> Self {
self.gateway.disabled_credentials_mode = disabled_credentials_mode;
self
+4
View File
@@ -96,6 +96,10 @@ impl NymConfig for Config {
.join("mixnodes")
}
fn try_default_root_directory() -> Option<PathBuf> {
dirs::home_dir().map(|path| path.join(".nym").join("mixnodes"))
}
fn root_directory(&self) -> PathBuf {
self.mixnode.nym_root_directory.clone()
}
+11
View File
@@ -1,3 +1,14 @@
## [nym-connect-v1.0.2](https://github.com/nymtech/nym/tree/nym-connect-v1.0.2) (2022-08-18)
### Changed
- nym-connect: "load balance" the service providers by picking a random Service Provider for each Service and storing in local storage so it remains sticky for the user ([#1540])
- nym-connect: the ServiceProviderSelector only displays the available Services, and picks a random Service Provider for Services the user has never used before ([#1540])
- nym-connect: add `local-forage` for storing user settings ([#1540])
[#1540]: https://github.com/nymtech/nym/pull/1540
## [nym-connect-v1.0.1](https://github.com/nymtech/nym/tree/nym-connect-v1.0.1) (2022-07-22)
### Added
+2 -1
View File
@@ -3404,7 +3404,7 @@ dependencies = [
[[package]]
name = "nym-connect"
version = "1.0.1"
version = "1.0.2"
dependencies = [
"bip39",
"client-core",
@@ -5227,6 +5227,7 @@ version = "0.1.0"
dependencies = [
"nymsphinx-addressing",
"ordered-buffer",
"thiserror",
]
[[package]]
+1
View File
@@ -40,6 +40,7 @@
"react-hook-form": "^7.14.2",
"react-router-dom": "^5.2.0",
"semver": "^6.3.0",
"@tauri-apps/tauri-forage": "^1.0.0-beta.2",
"yup": "^0.32.9"
},
"devDependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-connect"
version = "1.0.1"
version = "1.0.2"
description = "nym-connect"
authors = ["Nym Technologies SA"]
license = ""
+15 -15
View File
@@ -37,9 +37,7 @@ pub async fn get_config_file_location(
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<String> {
let id = get_config_id(state).await?;
Ok(Config::config_file_location(&id)
.to_string_lossy()
.to_string())
Config::config_file_location(&id).map(|d| d.to_string_lossy().to_string())
}
#[derive(Debug)]
@@ -94,8 +92,9 @@ impl Config {
Ok(())
}
pub fn config_file_location(id: &str) -> PathBuf {
Socks5Config::default_config_file_path(Some(id))
pub fn config_file_location(id: &str) -> Result<PathBuf> {
Socks5Config::try_default_config_file_path(Some(id))
.ok_or(BackendError::CouldNotGetFilename)
}
}
@@ -107,9 +106,9 @@ pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: Str
log::debug!(
"Attempting to use config file location: {}",
Config::config_file_location(&id).to_string_lossy(),
Config::config_file_location(&id)?.to_string_lossy(),
);
let already_init = Config::config_file_location(&id).exists();
let already_init = Config::config_file_location(&id)?.exists();
if already_init {
log::info!(
"SOCKS5 client \"{}\" was already initialised before! \
@@ -147,12 +146,12 @@ pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: Str
Some(&chosen_gateway_id),
config.get_socks5(),
)
.await;
.await?;
config.get_base_mut().with_gateway_endpoint(gateway);
let config_save_location = config.get_socks5().get_config_file_save_location();
config.get_socks5().save_to_file(None).tap_err(|_| {
log::warn!("Failed to save the config file");
log::error!("Failed to save the config file");
})?;
log::info!("Saved configuration file to {:?}", config_save_location);
@@ -183,7 +182,7 @@ async fn setup_gateway(
register: bool,
user_chosen_gateway_id: Option<&str>,
config: &Socks5Config,
) -> GatewayEndpoint {
) -> Result<GatewayEndpoint> {
if register {
// Get the gateway details by querying the validator-api. Either pick one at random or use
// the chosen one if it's among the available ones.
@@ -201,7 +200,7 @@ async fn setup_gateway(
.await;
println!("Saved all generated keys");
gateway.into()
Ok(gateway.into())
} else if user_chosen_gateway_id.is_some() {
// Just set the config, don't register or create any keys
// This assumes that the user knows what they are doing, and that the existing keys are
@@ -213,19 +212,20 @@ async fn setup_gateway(
)
.await;
log::debug!("Querying gateway gives: {}", gateway);
gateway.into()
Ok(gateway.into())
} else {
println!("Not registering gateway, will reuse existing config and keys");
match Socks5Config::load_from_file(Some(id)) {
Ok(existing_config) => existing_config.get_base().get_gateway_endpoint().clone(),
Ok(existing_config) => Ok(existing_config.get_base().get_gateway_endpoint().clone()),
Err(err) => {
panic!(
log::error!(
"Unable to configure gateway: {err}. \n
Seems like the client was already initialized but it was not possible to read \
the existing configuration file. \n
CAUTION: Consider backing up your gateway keys and try force gateway registration, or \
removing the existing configuration and starting over."
)
);
Err(BackendError::CouldNotLoadExistingGatewayConfiguration(err))
}
}
}
+4
View File
@@ -52,6 +52,10 @@ pub enum BackendError {
CouldNotInitWithoutServiceProvider,
#[error("Could not get file name")]
CouldNotGetFilename,
#[error("Could not get config file location")]
CouldNotGetConfigFilename,
#[error("Could not load existing gateway configuration")]
CouldNotLoadExistingGatewayConfiguration(std::io::Error),
}
impl Serialize for BackendError {
+1 -1
View File
@@ -101,7 +101,7 @@ impl State {
// Setup configuration by writing to file
if let Err(err) = self.init_config().await {
log::warn!("Failed to initialize: {}", err);
log::error!("Failed to initialize: {}", err);
// Wait a little to give the user some rudimentary feedback that the click actually
// registered.
+3 -3
View File
@@ -1,7 +1,7 @@
{
"package": {
"productName": "nym-connect",
"version": "1.0.1"
"version": "1.0.2"
},
"build": {
"distDir": "../dist",
@@ -65,9 +65,9 @@
},
"windows": [
{
"title": "Nym Connect",
"title": "NymConnect",
"width": 240,
"height": 480,
"height": 500,
"resizable": false
}
],
+50 -50
View File
@@ -1,6 +1,56 @@
import React from 'react';
import { ConnectionStatusKind } from '../types';
const getBusyFillColor = (color: string): string => {
if (color === '#60D6EF') {
return '#21D072';
}
return '#60D6EF';
};
const getStatusFillColor = (status: ConnectionStatusKind, hover: boolean, isError: boolean): string => {
if (isError && hover) {
return '#21D072';
}
if (isError) {
return '#40475C';
}
switch (status) {
case ConnectionStatusKind.disconnected:
if (hover) {
return '#21D072';
}
return '#60D6EF';
case ConnectionStatusKind.connecting:
case ConnectionStatusKind.disconnecting:
return '#60D6EF';
default:
// connected
if (hover) {
return '#DA465B';
}
return '#21D072';
}
};
const getStatusText = (status: ConnectionStatusKind, hover: boolean): string => {
switch (status) {
case ConnectionStatusKind.disconnected:
return 'Connect';
case ConnectionStatusKind.connecting:
return 'Connecting';
case ConnectionStatusKind.disconnecting:
return 'Connected';
default:
// connected
if (hover) {
return 'Disconnect';
}
return 'Connected';
}
};
export const ConnectionButton: React.FC<{
status: ConnectionStatusKind;
disabled?: boolean;
@@ -130,53 +180,3 @@ export const ConnectionButton: React.FC<{
</svg>
);
};
const getBusyFillColor = (color: string): string => {
if (color === '#60D6EF') {
return '#21D072';
}
return '#60D6EF';
};
const getStatusFillColor = (status: ConnectionStatusKind, hover: boolean, isError: boolean): string => {
if (isError && hover) {
return '#21D072';
}
if (isError) {
return '#40475C';
}
switch (status) {
case ConnectionStatusKind.disconnected:
if (hover) {
return '#21D072';
}
return '#60D6EF';
case ConnectionStatusKind.connecting:
case ConnectionStatusKind.disconnecting:
return '#60D6EF';
default:
// connected
if (hover) {
return '#DA465B';
}
return '#21D072';
}
};
const getStatusText = (status: ConnectionStatusKind, hover: boolean): string => {
switch (status) {
case ConnectionStatusKind.disconnected:
return 'Connect';
case ConnectionStatusKind.connecting:
return 'Connecting';
case ConnectionStatusKind.disconnecting:
return 'Connected';
default:
// connected
if (hover) {
return 'Disconnect';
}
return 'Connected';
}
};
@@ -1,24 +1,53 @@
import React from 'react';
import React, { useEffect, useMemo } from 'react';
import IconButton from '@mui/material/IconButton';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import ArrowDropDownCircleIcon from '@mui/icons-material/ArrowDropDownCircle';
import { Box, CircularProgress, Stack, Tooltip, Typography } from '@mui/material';
import { ServiceProvider, Services } from '../types/directory';
import { ServiceProvider, Service, Services } from '../types/directory';
type ServiceWithRandomSp = {
id: string;
description: string;
sp: ServiceProvider;
};
export const ServiceProviderSelector: React.FC<{
onChange?: (serviceProvider: ServiceProvider) => void;
services?: Services;
}> = ({ services, onChange }) => {
const [serviceProvider, setServiceProvider] = React.useState<ServiceProvider | undefined>();
currentSp?: ServiceProvider;
}> = ({ services, currentSp, onChange }) => {
const [service, setService] = React.useState<Service>();
const [serviceProvider, setServiceProvider] = React.useState<ServiceProvider | undefined>(currentSp);
const textEl = React.useRef<null | HTMLElement>(null);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
useEffect(() => {
if (!serviceProvider && currentSp) {
setServiceProvider(currentSp);
}
}, [currentSp]);
useEffect(() => {
if (services && serviceProvider) {
// retrieve the service corresponding to this service provider
setService(
services.find((s) =>
s.items.some(
({ id, address, gateway }) =>
id === serviceProvider.id && address === serviceProvider.address && gateway === serviceProvider.gateway,
),
),
);
}
}, [serviceProvider, services]);
const handleClick = () => {
setAnchorEl(textEl.current);
};
const handleClose = (newServiceProvider?: ServiceProvider) => {
if (newServiceProvider) {
if (newServiceProvider && newServiceProvider !== currentSp) {
setServiceProvider(newServiceProvider);
onChange?.(newServiceProvider);
}
@@ -39,6 +68,16 @@ export const ServiceProviderSelector: React.FC<{
);
}
const servicesWithRandomSp: ServiceWithRandomSp[] = useMemo(
() =>
services.map(({ id, items, description }) => ({
id,
description,
sp: items[Math.floor(Math.random() * items.length)],
})),
[services],
);
return (
<>
<Box display="flex" alignItems="center" justifyContent="space-between" sx={{ mt: 3 }}>
@@ -48,7 +87,7 @@ export const ServiceProviderSelector: React.FC<{
fontWeight={700}
color={(theme) => (serviceProvider ? undefined : theme.palette.primary.main)}
>
{serviceProvider ? serviceProvider.description : 'Select a service'}
{service ? service.description : 'Select a service'}
</Typography>
<IconButton
id="service-provider-button"
@@ -65,44 +104,46 @@ export const ServiceProviderSelector: React.FC<{
anchorEl={anchorEl}
open={open}
onClose={() => handleClose()}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
MenuListProps={{
'aria-labelledby': 'service-provider-button',
sx: {
minWidth: 160,
},
}}
>
{services.map((service) => (
<>
<MenuItem disabled dense sx={{ fontSize: 'small', fontWeight: 'bold', mb: -1 }}>
{service.description}
</MenuItem>
{service.items.map((sp) => (
<MenuItem dense sx={{ fontSize: 'small', ml: 2, height: 'auto' }} onClick={() => handleClose(sp)}>
<Tooltip
title={
<Stack direction="column">
<Typography fontSize="inherit">
<code>{sp.id}</code>
</Typography>
<Typography fontSize="inherit" fontWeight={700}>
{sp.description}
</Typography>
<Typography fontSize="inherit">
Gateway <code>{sp.gateway.slice(0, 10)}...</code>
</Typography>
<Typography fontSize="inherit">
Provider <code>{sp.address.slice(0, 10)}...</code>
</Typography>
</Stack>
}
arrow
placement="top"
>
<Typography fontSize="inherit" noWrap>
{servicesWithRandomSp.map(({ id, description, sp }) => (
<MenuItem dense key={id} sx={{ fontSize: 'small', fontWeight: 'bold' }} onClick={() => handleClose(sp)}>
<Tooltip
title={
<Stack direction="column">
<Typography fontSize="inherit">
<code>{sp.id}</code>
</Typography>
<Typography fontSize="inherit" fontWeight={700}>
{sp.description}
</Typography>
</Tooltip>
</MenuItem>
))}
</>
<Typography fontSize="inherit">
Gateway <code>{sp.gateway.slice(0, 10)}...</code>
</Typography>
<Typography fontSize="inherit">
Provider <code>{sp.address.slice(0, 10)}...</code>
</Typography>
</Stack>
}
arrow
placement="top"
>
<Typography>{description}</Typography>
</Tooltip>
</MenuItem>
))}
</Menu>
</>
+61 -22
View File
@@ -1,8 +1,9 @@
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { DateTime } from 'luxon';
import { invoke } from '@tauri-apps/api';
import type { UnlistenFn } from '@tauri-apps/api/event';
import { listen } from '@tauri-apps/api/event';
import { forage } from '@tauri-apps/tauri-forage';
import { ConnectionStatusKind } from '../types';
import { ConnectionStatsItem } from '../components/ConnectionStats';
import { ServiceProvider, Services } from '../types/directory';
@@ -36,7 +37,7 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatusKind>(ConnectionStatusKind.disconnected);
const [connectionStats, setConnectionStats] = useState<ConnectionStatsItem[]>();
const [connectedSince, setConnectedSince] = useState<DateTime>();
const [services, setServices] = React.useState<Services>();
const [services, setServices] = React.useState<Services>([]);
const [serviceProvider, setRawServiceProvider] = React.useState<ServiceProvider>();
useEffect(() => {
@@ -72,33 +73,71 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
await invoke('start_disconnecting');
}, []);
const setSpInStorage = async (sp: ServiceProvider) => {
await forage.setItem({
key: 'nym-connect-sp',
value: sp,
} as any)();
};
const setServiceProvider = useCallback(async (newServiceProvider: ServiceProvider) => {
await invoke('set_gateway', { gateway: newServiceProvider.gateway });
await invoke('set_service_provider', { serviceProvider: newServiceProvider.address });
await setSpInStorage(newServiceProvider);
setRawServiceProvider(newServiceProvider);
}, []);
return (
<ClientContext.Provider
value={{
mode,
setMode,
connectionStatus,
setConnectionStatus,
connectionStats,
setConnectionStats,
connectedSince,
setConnectedSince,
startConnecting,
startDisconnecting,
services,
serviceProvider,
setServiceProvider,
}}
>
{children}
</ClientContext.Provider>
const getSpFromStorage = async () => {
try {
const spFromStorage = await forage.getItem({ key: 'nym-connect-sp' })();
if (spFromStorage) {
setRawServiceProvider(spFromStorage);
}
} catch (e) {
console.warn(e);
}
};
useEffect(() => {
const validityCheck = async () => {
if (services.length > 0 && serviceProvider) {
const isValid = services.some(({ items }) => items.some(({ id }) => id === serviceProvider.id));
if (!isValid) {
console.warn('invalid SP, cleaning local storage');
await forage.removeItem({
key: 'nym-connect-sp',
})();
setRawServiceProvider(undefined);
}
}
};
validityCheck();
}, [services, serviceProvider]);
useEffect(() => {
getSpFromStorage();
}, []);
const contextValue = useMemo(
() => ({
mode,
setMode,
connectionStatus,
setConnectionStatus,
connectionStats,
setConnectionStats,
connectedSince,
setConnectedSince,
startConnecting,
startDisconnecting,
services,
serviceProvider,
setServiceProvider,
}),
[mode, connectedSince, connectionStatus, connectionStats, connectedSince, services, serviceProvider],
);
return <ClientContext.Provider value={contextValue}>{children}</ClientContext.Provider>;
};
export const useClientContext = () => useContext(ClientContext);
+5 -2
View File
@@ -6,6 +6,7 @@ import { ConnectionStatusKind } from '../types';
import { NeedHelp } from '../components/NeedHelp';
import { ServiceProviderSelector } from '../components/ServiceProviderSelector';
import { ServiceProvider, Services } from '../types/directory';
import { useClientContext } from '../context/main';
export const DefaultLayout: React.FC<{
status: ConnectionStatusKind;
@@ -20,6 +21,8 @@ export const DefaultLayout: React.FC<{
setServiceProvider(newServiceProvider);
onServiceProviderChange?.(newServiceProvider);
};
const { serviceProvider: currentSp } = useClientContext();
return (
<AppWindowFrame>
<Typography fontWeight="400" fontSize="12px" textAlign="center" sx={{ opacity: 0.6 }}>
@@ -31,10 +34,10 @@ export const DefaultLayout: React.FC<{
<br />
Nym mixnet for privacy.
</Typography>
<ServiceProviderSelector services={services} onChange={handleServiceProviderChange} />
<ServiceProviderSelector services={services} onChange={handleServiceProviderChange} currentSp={currentSp} />
<ConnectionButton
status={status}
disabled={serviceProvider === undefined}
disabled={serviceProvider === undefined && currentSp === undefined}
busy={busy}
isError={isError}
onClick={onConnectClick}
@@ -1,9 +1,6 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// Specify filenames and other platform specific constants to respect platform conventions, or at
// least, something popular on each respective platform.
pub const CONFIG_DIR_NAME: &str = "nym-wallet";
pub const CONFIG_FILENAME: &str = "config.toml";
pub const STORAGE_DIR_NAME: &str = "nym-wallet";
@@ -11,7 +11,7 @@ import { simulateBondGateway, simulateVestingBondGateway } from 'src/requests';
import { TBondGatewayArgs } from 'src/types';
import { BondGatewayForm } from '../forms/BondGatewayForm';
const defaultMixnodeValues: GatewayData = {
const defaultGatewayValues: GatewayData = {
identityKey: '',
sphinxKey: '',
ownerSignature: '',
@@ -19,7 +19,7 @@ const defaultMixnodeValues: GatewayData = {
host: '',
version: '',
mixPort: 1789,
clientsPort: 1790,
clientsPort: 9000,
};
const defaultAmountValues = (denom: CurrencyDenom) => ({
@@ -43,7 +43,7 @@ export const BondGatewayModal = ({
onError: (e: string) => void;
}) => {
const [step, setStep] = useState<1 | 2 | 3>(1);
const [gatewayData, setGatewayData] = useState<GatewayData>(defaultMixnodeValues);
const [gatewayData, setGatewayData] = useState<GatewayData>(defaultGatewayValues);
const [amountData, setAmountData] = useState<GatewayAmount>(defaultAmountValues(denom));
const { fee, getFee, resetFeeState, feeError } = useGetFee();
+7 -3
View File
@@ -118,7 +118,6 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
stakeSaturation: 0,
numberOfDelegators: 0,
};
try {
const statusResponse = await getMixnodeStatus(identityKey);
additionalDetails.status = statusResponse.status;
@@ -163,7 +162,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
try {
operatorRewards = await getOperatorRewards(clientDetails?.client_address);
} catch (e) {
console.warn(`get_operator_rewards request failed: ${e}`);
Console.warn(`get_operator_rewards request failed: ${e}`);
}
if (data) {
const { status, stakeSaturation, numberOfDelegators } = await getAdditionalMixnodeDetails(
@@ -186,7 +185,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
} as TBondedMixnode);
}
} catch (e: any) {
console.warn(e);
Console.warn(e);
setError(`While fetching current bond state, an error occurred: ${e}`);
}
}
@@ -207,6 +206,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
} as TBondedGateway);
}
} catch (e: any) {
Console.warn(e);
setError(`While fetching current bond state, an error occurred: ${e}`);
}
}
@@ -235,6 +235,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
}
return tx;
} catch (e: any) {
Console.warn(e);
setError(`an error occurred: ${e}`);
} finally {
setIsLoading(false);
@@ -256,6 +257,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
}
return tx;
} catch (e: any) {
Console.warn(e);
setError(`an error occurred: ${e}`);
} finally {
setIsLoading(false);
@@ -272,6 +274,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
if (bondedNode && isGateway(bondedNode) && bondedNode.proxy) tx = await vestingUnbondGateway(fee?.fee);
if (bondedNode && isGateway(bondedNode) && !bondedNode.proxy) tx = await unbondGatewayRequest(fee?.fee);
} catch (e) {
Console.warn(e);
setError(`an error occurred: ${e as string}`);
} finally {
setIsLoading(false);
@@ -286,6 +289,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
if (bondedNode?.proxy) tx = await updateMixnodeVestingRequest(pm, fee?.fee);
else tx = await updateMixnodeRequest(pm, fee?.fee);
} catch (e: any) {
Console.warn(e);
setError(`an error occurred: ${e}`);
} finally {
setIsLoading(false);
+16 -5
View File
@@ -1,4 +1,4 @@
import React, { useContext, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { FeeDetails } from '@nymproject/types';
import { TPoolOption } from 'src/components';
import { Bond } from 'src/components/Bonding/Bond';
@@ -16,9 +16,8 @@ import { isGateway, isMixnode, TBondGatewayArgs, TBondMixNodeArgs } from 'src/ty
import { BondedGateway } from 'src/components/Bonding/BondedGateway';
import { RedeemRewardsModal } from 'src/components/Bonding/modals/RedeemRewardsModal';
import { CompoundRewardsModal } from 'src/components/Bonding/modals/CompoundRewardsModal';
import { PageLayout } from '../../layouts';
import { BondingContextProvider, useBondingContext } from '../../context';
import { Box } from '@mui/material';
import { BondingContextProvider, useBondingContext } from '../../context';
const Bonding = () => {
const [showModal, setShowModal] = useState<
@@ -42,19 +41,31 @@ const Bonding = () => {
compoundRewards,
isLoading,
checkOwnership,
error,
} = useBondingContext();
useEffect(() => {
if (error) {
setShowModal(undefined);
setConfirmationDetails({
status: 'error',
title: 'An error occurred',
subtitle: error,
});
}
}, [error]);
const handleCloseModal = async () => {
setShowModal(undefined);
await checkOwnership();
};
const handleError = (error: string) => {
const handleError = (e: string) => {
setShowModal(undefined);
setConfirmationDetails({
status: 'error',
title: 'An error occurred',
subtitle: error,
subtitle: e,
});
};
@@ -30,14 +30,18 @@ const DataField = ({ title, info, Indicator }: { title: string; info: string; In
);
const colorMap: { [key in SelectionChance]: string } = {
VeryLow: 'error.main',
Low: 'error.main',
Moderate: 'warning.main',
High: 'success.main',
VeryHigh: 'success.main',
};
const textMap: { [key in SelectionChance]: string } = {
VeryLow: 'VeryLow',
Low: 'Low',
Moderate: 'Moderate',
High: 'High',
VeryHigh: 'Very high',
};
@@ -13,7 +13,9 @@ use log::*;
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::receiver::ReconstructedMessage;
use proxy_helpers::connection_controller::{Controller, ControllerCommand, ControllerSender};
use socks5_requests::{ConnectionId, Message as Socks5Message, Request, Response};
use socks5_requests::{
ConnectionId, Message as Socks5Message, NetworkRequesterResponse, Request, Response,
};
use statistics_common::collector::StatisticsSender;
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
@@ -192,7 +194,16 @@ impl ServiceProvider {
return_address: Recipient,
) {
if !self.open_proxy && !self.outbound_request_filter.check(&remote_addr) {
log::info!("Domain {:?} failed filter check", remote_addr);
let log_msg = format!("Domain {:?} failed filter check", remote_addr);
log::info!("{}", log_msg);
mix_input_sender
.unbounded_send((
Socks5Message::NetworkRequesterResponse(NetworkRequesterResponse::new(
conn_id, log_msg,
)),
return_address,
))
.unwrap();
return;
}
@@ -275,7 +286,7 @@ impl ServiceProvider {
self.handle_proxy_send(controller_sender, conn_id, data, closed)
}
},
Socks5Message::Response(_) => {}
Socks5Message::Response(_) | Socks5Message::NetworkRequesterResponse(_) => {}
}
}
@@ -1 +1 @@
export type SelectionChance = 'VeryHigh' | 'Moderate' | 'Low';
export type SelectionChance = 'VeryHigh' | 'High' | 'Moderate' | 'Low' | 'VeryLow';
+8 -7
View File
@@ -16,7 +16,9 @@ rust-version = "1.56"
[dependencies]
async-trait = "0.1.52"
cfg-if = "1.0"
clap = "2.33.0"
console-subscriber = { version = "0.1.1", optional = true} # validator-api needs to be built with RUSTFLAGS="--cfg tokio_unstable"
dirs = "4.0"
dotenv = "0.15.0"
futures = "0.3"
@@ -31,6 +33,7 @@ rocket = { version = "0.5.0-rc.2", features = ["json"] }
rocket_cors = { git="https://github.com/lawliet89/rocket_cors", rev="dfd3662c49e2f6fc37df35091cb94d82f7fb5915" }
serde = "1.0"
serde_json = "1.0"
tap = "1.0.1"
thiserror = "1"
time = { version = "0.3", features = ["serde-human-readable", "parsing"]}
tokio = { version = "1.19.1", features = ["rt-multi-thread", "macros", "signal", "time"] }
@@ -51,25 +54,23 @@ schemars = { version = "0.8", features = ["preserve_order"] }
## internal
coconut-bandwidth-contract-common = { path = "../common/cosmwasm-smart-contracts/coconut-bandwidth-contract" }
coconut-interface = { path = "../common/coconut-interface", optional = true }
config = { path = "../common/config" }
cosmwasm-std = "1.0.0"
credential-storage = { path = "../common/credential-storage" }
credentials = { path = "../common/credentials", optional = true }
crypto = { path="../common/crypto" }
gateway-client = { path="../common/client-libs/gateway-client" }
inclusion-probability = { path = "../common/inclusion-probability" }
mixnet-contract-common = { path= "../common/cosmwasm-smart-contracts/mixnet-contract" }
multisig-contract-common = { path = "../common/cosmwasm-smart-contracts/multisig-contract" }
nymsphinx = { path="../common/nymsphinx" }
nymcoconut = { path = "../common/nymcoconut", optional = true }
nymsphinx = { path="../common/nymsphinx" }
task = { path = "../common/task" }
topology = { path="../common/topology" }
validator-api-requests = { path = "validator-api-requests" }
validator-client = { path="../common/client-libs/validator-client", features = ["nymd-client"] }
version-checker = { path="../common/version-checker" }
coconut-interface = { path = "../common/coconut-interface", optional = true }
credentials = { path = "../common/credentials", optional = true }
credential-storage = { path = "../common/credential-storage" }
# validator-api needs to be built with RUSTFLAGS="--cfg tokio_unstable"
console-subscriber = { version = "0.1.1", optional = true}
cfg-if = "1.0"
[features]
coconut = ["coconut-interface", "credentials", "gateway-client/coconut", "credentials/coconut", "validator-api-requests/coconut", "nymcoconut"]
+4
View File
@@ -72,6 +72,10 @@ impl NymConfig for Config {
.join("validator-api")
}
fn try_default_root_directory() -> Option<PathBuf> {
dirs::home_dir().map(|path| path.join(".nym").join("validator-api"))
}
fn root_directory(&self) -> PathBuf {
Self::default_root_directory()
}
+39 -11
View File
@@ -23,7 +23,7 @@ use std::collections::HashSet;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use tokio::sync::{watch, RwLock};
use tokio::time;
use validator_api_requests::models::{MixNodeBondAnnotated, MixnodeStatus};
use validator_client::nymd::CosmWasmClient;
@@ -31,6 +31,13 @@ use validator_client::nymd::CosmWasmClient;
pub(crate) mod reward_estimate;
pub(crate) mod routes;
// The cache can emit notifications to listeners about the current state
#[derive(Debug, PartialEq, Eq)]
pub enum CacheNotification {
Start,
Updated,
}
pub struct ValidatorCacheRefresher<C> {
nymd_client: Client<C>,
cache: ValidatorCache,
@@ -38,6 +45,9 @@ pub struct ValidatorCacheRefresher<C> {
// Readonly: some of the quantities cached depends on values from the storage.
storage: Option<ValidatorApiStorage>,
// Notify listeners that the cache has been updated
update_notifier: watch::Sender<CacheNotification>,
}
#[derive(Clone)]
@@ -73,14 +83,14 @@ pub struct Cache<T> {
}
impl<T: Clone> Cache<T> {
fn new(value: T) -> Self {
pub(super) fn new(value: T) -> Self {
Cache {
value,
as_at: current_unix_timestamp(),
}
}
fn update(&mut self, value: T) {
pub(super) fn update(&mut self, value: T) {
self.value = value;
self.as_at = current_unix_timestamp()
}
@@ -101,11 +111,13 @@ impl<C> ValidatorCacheRefresher<C> {
cache: ValidatorCache,
storage: Option<ValidatorApiStorage>,
) -> Self {
let (tx, _) = watch::channel(CacheNotification::Start);
ValidatorCacheRefresher {
nymd_client,
cache,
caching_interval,
storage,
update_notifier: tx,
}
}
@@ -117,6 +129,10 @@ impl<C> ValidatorCacheRefresher<C> {
.ok()
}
pub fn subscribe(&self) -> watch::Receiver<CacheNotification> {
self.update_notifier.subscribe()
}
async fn annotate_bond_with_details(
&self,
mixnodes: Vec<MixNodeBond>,
@@ -250,6 +266,10 @@ impl<C> ValidatorCacheRefresher<C> {
)
.await;
if let Err(err) = self.update_notifier.send(CacheNotification::Updated) {
warn!("Failed to notify validator cache refresh: {}", err);
}
Ok(())
}
@@ -261,17 +281,25 @@ impl<C> ValidatorCacheRefresher<C> {
while !shutdown.is_shutdown() {
tokio::select! {
_ = interval.tick() => {
if let Err(err) = self.refresh_cache().await {
error!("Failed to refresh validator cache - {}", err);
} else {
// relaxed memory ordering is fine here. worst case scenario network monitor
// will just have to wait for an additional backoff to see the change.
// And so this will not really incur any performance penalties by setting it every loop iteration
self.cache.initialised.store(true, Ordering::Relaxed)
tokio::select! {
biased;
_ = shutdown.recv() => {
trace!("ValidatorCacheRefresher: Received shutdown");
}
ret = self.refresh_cache() => {
if let Err(err) = ret {
error!("Failed to refresh validator cache - {}", err);
} else {
// relaxed memory ordering is fine here. worst case scenario network monitor
// will just have to wait for an additional backoff to see the change.
// And so this will not really incur any performance penalties by setting it every loop iteration
self.cache.initialised.store(true, Ordering::Relaxed)
}
}
}
}
_ = shutdown.recv() => {
trace!("UpdateHandler: Received shutdown");
trace!("ValidatorCacheRefresher: Received shutdown");
}
}
}
+33 -9
View File
@@ -19,6 +19,7 @@ use anyhow::Result;
use clap::{crate_version, App, Arg, ArgMatches};
use contract_cache::ValidatorCache;
use log::{info, warn};
use node_status_api::NodeStatusCache;
use okapi::openapi3::OpenApi;
use rocket::fairing::AdHoc;
use rocket::http::Method;
@@ -470,7 +471,8 @@ async fn setup_rocket(
.mount("/swagger", make_swagger_ui(&swagger::get_docs()))
.attach(setup_cors()?)
.attach(setup_liftoff_notify(liftoff_notify))
.attach(ValidatorCache::stage());
.attach(ValidatorCache::stage())
.attach(NodeStatusCache::stage());
// This is not a very nice approach. A lazy value would be more suitable, but that's still
// a nightly feature: https://github.com/rust-lang/rust/issues/74465
@@ -566,7 +568,8 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
let signing_nymd_client = Client::new_signing(&config);
let liftoff_notify = Arc::new(Notify::new());
let shutdown = ShutdownNotifier::default();
// We need a bigger timeout
let shutdown = ShutdownNotifier::new(10);
// let's build our rocket!
let rocket = setup_rocket(
@@ -579,10 +582,11 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
let monitor_builder = setup_network_monitor(&config, system_version, &rocket);
let validator_cache = rocket.state::<ValidatorCache>().unwrap().clone();
let node_status_cache = rocket.state::<NodeStatusCache>().unwrap().clone();
// if network monitor is disabled, we're not going to be sending any rewarding hence
// we're not starting signing client
if config.get_network_monitor_enabled() {
let validator_cache_listener = if config.get_network_monitor_enabled() {
// Main storage
let storage = rocket.state::<ValidatorApiStorage>().unwrap().clone();
@@ -592,33 +596,53 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
let shutdown_listener = shutdown.subscribe();
tokio::spawn(async move { uptime_updater.run(shutdown_listener).await });
// spawn the cache refresher
// spawn the validator cache refresher
let validator_cache_refresher = ValidatorCacheRefresher::new(
signing_nymd_client.clone(),
config.get_caching_interval(),
validator_cache.clone(),
Some(storage.clone()),
);
let validator_cache_listener = validator_cache_refresher.subscribe();
let shutdown_listener = shutdown.subscribe();
tokio::spawn(async move { validator_cache_refresher.run(shutdown_listener).await });
// spawn rewarded set updater
let mut rewarded_set_updater =
RewardedSetUpdater::new(signing_nymd_client, validator_cache.clone(), storage).await?;
tokio::spawn(async move { rewarded_set_updater.run().await.unwrap() });
let shutdown_listener = shutdown.subscribe();
tokio::spawn(async move { rewarded_set_updater.run(shutdown_listener).await.unwrap() });
validator_cache_listener
} else {
// Spawn the validator cache refresher.
// When the network monitor is not enabled, we spawn the validator cache refresher task
// with just a nymd client, in contrast to a signing client.
let nymd_client = Client::new_query(&config);
let validator_cache_refresher = ValidatorCacheRefresher::new(
nymd_client,
config.get_caching_interval(),
validator_cache,
validator_cache.clone(),
None,
);
let validator_cache_listener = validator_cache_refresher.subscribe();
let shutdown_listener = shutdown.subscribe();
// spawn our cacher
tokio::spawn(async move { validator_cache_refresher.run(shutdown_listener).await });
}
validator_cache_listener
};
// Spawn the node status cache refresher.
// It is primarily refreshed in-sync with the validator cache, however provide a fallback
// caching interval that is twice the validator cache
let mut validator_api_cache_refresher = node_status_api::NodeStatusCacheRefresher::new(
node_status_cache,
validator_cache,
validator_cache_listener,
config.get_caching_interval().saturating_mul(2),
);
let shutdown_listener = shutdown.subscribe();
tokio::spawn(async move { validator_api_cache_refresher.run(shutdown_listener).await });
// launch the rocket!
// Rocket handles shutdown on it's own, but its shutdown handling should be incorporated
+4 -4
View File
@@ -12,6 +12,7 @@ use topology::NymTopology;
const DEFAULT_AVERAGE_PACKET_DELAY: Duration = Duration::from_millis(200);
const DEFAULT_AVERAGE_ACK_DELAY: Duration = Duration::from_millis(200);
#[derive(Clone)]
pub(crate) struct Chunker {
rng: OsRng,
message_preparer: MessagePreparer<OsRng>,
@@ -30,7 +31,7 @@ impl Chunker {
}
}
pub(crate) async fn prepare_packets_from(
pub(crate) fn prepare_packets_from(
&mut self,
message: Vec<u8>,
topology: &NymTopology,
@@ -40,10 +41,10 @@ impl Chunker {
// but without some significant API changes in the `MessagePreparer` this was the easiest
// way to being able to have variable sender address.
self.message_preparer.set_sender_address(packet_sender);
self.prepare_packets(message, topology, packet_sender).await
self.prepare_packets(message, topology, packet_sender)
}
async fn prepare_packets(
fn prepare_packets(
&mut self,
message: Vec<u8>,
topology: &NymTopology,
@@ -62,7 +63,6 @@ impl Chunker {
let prepared_fragment = self
.message_preparer
.prepare_chunk_for_sending(message_chunk, topology, &ack_key, &packet_sender)
.await
.unwrap();
mix_packets.push(prepared_fragment.mix_packet);
@@ -7,6 +7,7 @@ use crypto::asymmetric::identity;
use crypto::asymmetric::identity::PUBLIC_KEY_LENGTH;
use log::{debug, info, trace, warn};
use std::time::Duration;
use task::ShutdownListener;
use tokio::time::{sleep, Instant};
// TODO: should it perhaps be moved to config along other timeout values?
@@ -143,10 +144,22 @@ impl GatewayPinger {
info!("Pinging all active gateways took {:?}", time_taken);
}
pub(crate) async fn run(&self) {
loop {
sleep(self.pinging_interval).await;
self.ping_and_cleanup_all_gateways().await
pub(crate) async fn run(&self, mut shutdown: ShutdownListener) {
while !shutdown.is_shutdown() {
tokio::select! {
_ = sleep(self.pinging_interval) => {
tokio::select! {
biased;
_ = shutdown.recv() => {
trace!("GatewaysPinger: Received shutdown");
}
_ = self.ping_and_cleanup_all_gateways() => (),
}
}
_ = shutdown.recv() => {
trace!("GatewaysPinger: Received shutdown");
}
}
}
}
}
@@ -122,11 +122,15 @@ impl Monitor {
let mut packets = Vec::with_capacity(routes.len());
for route in routes {
packets.push(
self.packet_preparer
.prepare_test_route_viability_packets(route, self.route_test_packets)
.await,
);
let mut packet_preparer = self.packet_preparer.clone();
let route = route.clone();
let route_test_packets = self.route_test_packets;
let gateway_packets = tokio::spawn(async move {
packet_preparer.prepare_test_route_viability_packets(&route, route_test_packets)
})
.await
.unwrap();
packets.push(gateway_packets);
}
self.received_processor.set_route_test_nonce().await;
@@ -306,12 +310,20 @@ impl Monitor {
.await;
self.packet_sender
.spawn_gateways_pinger(self.gateway_ping_interval);
.spawn_gateways_pinger(self.gateway_ping_interval, shutdown.clone());
let mut run_interval = tokio::time::interval(self.run_interval);
while !shutdown.is_shutdown() {
tokio::select! {
_ = run_interval.tick() => self.test_run().await,
_ = run_interval.tick() => {
tokio::select! {
biased;
_ = shutdown.recv() => {
trace!("UpdateHandler: Received shutdown");
}
_ = self.test_run() => (),
}
}
_ = shutdown.recv() => {
trace!("UpdateHandler: Received shutdown");
}
@@ -117,6 +117,7 @@ pub(crate) struct PreparedPackets {
pub(super) invalid_gateways: Vec<InvalidNode>,
}
#[derive(Clone)]
pub(crate) struct PacketPreparer {
system_version: String,
chunker: Option<Chunker>,
@@ -151,7 +152,7 @@ impl PacketPreparer {
}
}
async fn wrap_test_packet(
fn wrap_test_packet(
&mut self,
packet: &TestPacket,
topology: &NymTopology,
@@ -162,12 +163,11 @@ impl PacketPreparer {
if self.chunker.is_none() {
self.chunker = Some(Chunker::new(packet_recipient));
}
let mut mix_packets = self
.chunker
.as_mut()
.unwrap()
.prepare_packets_from(packet.to_bytes(), topology, packet_recipient)
.await;
let mut mix_packets = self.chunker.as_mut().unwrap().prepare_packets_from(
packet.to_bytes(),
topology,
packet_recipient,
);
assert_eq!(
mix_packets.len(),
1,
@@ -351,7 +351,7 @@ impl PacketPreparer {
)
}
pub(crate) async fn prepare_test_route_viability_packets(
pub(crate) fn prepare_test_route_viability_packets(
&mut self,
route: &TestRoute,
num: usize,
@@ -360,9 +360,7 @@ impl PacketPreparer {
let test_packet = route.self_test_packet();
let recipient = self.create_packet_sender(route.gateway());
for _ in 0..num {
let mix_packet = self
.wrap_test_packet(&test_packet, route.topology(), recipient)
.await;
let mix_packet = self.wrap_test_packet(&test_packet, route.topology(), recipient);
mix_packets.push(mix_packet)
}
@@ -451,9 +449,7 @@ impl PacketPreparer {
let topology = test_route.substitute_mix(mixnode);
// produce n mix packets
for _ in 0..self.per_node_test_packets {
let mix_packet = self
.wrap_test_packet(&test_packet, &topology, recipient)
.await;
let mix_packet = self.wrap_test_packet(&test_packet, &topology, recipient);
mix_packets.push(mix_packet);
}
}
@@ -476,9 +472,7 @@ impl PacketPreparer {
let topology = test_route.substitute_gateway(gateway);
// produce n mix packets
for _ in 0..self.per_node_test_packets {
let mix_packet = self
.wrap_test_packet(&test_packet, &topology, recipient)
.await;
let mix_packet = self.wrap_test_packet(&test_packet, &topology, recipient);
gateway_mix_packets.push(mix_packet);
}
@@ -140,8 +140,7 @@ impl ReceivedProcessor {
self.permit_changer = Some(permit_sender);
tokio::spawn(async move {
loop {
let permit = wait_for_permit(&mut permit_receiver, &*inner).await;
while let Some(permit) = wait_for_permit(&mut permit_receiver, &*inner).await {
receive_or_release_permit(&mut permit_receiver, permit).await;
}
@@ -151,16 +150,20 @@ impl ReceivedProcessor {
) {
loop {
tokio::select! {
permit_receiver = permit_receiver.next() => match permit_receiver.unwrap() {
LockPermit::Release => return,
LockPermit::Free => error!("somehow we got notification that the lock is free to take while we already hold it!"),
permit_receiver = permit_receiver.next() => match permit_receiver {
Some(LockPermit::Release) => return,
Some(LockPermit::Free) => error!("somehow we got notification that the lock is free to take while we already hold it!"),
None => return,
},
messages = inner.packets_receiver.next() => {
for message in messages.expect("packet receiver has died!") {
if let Err(err) = inner.on_message(message) {
warn!(target: "Monitor", "failed to process received gateway message - {}", err)
messages = inner.packets_receiver.next() => match messages {
Some(messages) => {
for message in messages {
if let Err(err) = inner.on_message(message) {
warn!(target: "Monitor", "failed to process received gateway message - {}", err)
}
}
}
None => return,
},
}
}
@@ -172,14 +175,15 @@ impl ReceivedProcessor {
async fn wait_for_permit<'a>(
permit_receiver: &mut mpsc::Receiver<LockPermit>,
inner: &'a Mutex<ReceivedProcessorInner>,
) -> MutexGuard<'a, ReceivedProcessorInner> {
) -> Option<MutexGuard<'a, ReceivedProcessorInner>> {
loop {
match permit_receiver.next().await.unwrap() {
match permit_receiver.next().await {
// we should only ever get this on the very first run
LockPermit::Release => debug!(
Some(LockPermit::Release) => debug!(
"somehow got request to drop our lock permit while we do not hold it!"
),
LockPermit::Free => return inner.lock().await,
Some(LockPermit::Free) => return Some(inner.lock().await),
None => return None,
}
}
}
@@ -59,6 +59,10 @@ impl PacketReceiver {
pub(crate) async fn run(&mut self, mut shutdown: ShutdownListener) {
while !shutdown.is_shutdown() {
tokio::select! {
biased;
_ = shutdown.recv() => {
trace!("UpdateHandler: Received shutdown");
}
// unwrap here is fine as it can only return a `None` if the PacketSender has died
// and if that was the case, then the entire monitor is already in an undefined state
update = self.clients_updater.next() => self.process_gateway_update(update.unwrap()),
@@ -68,9 +72,6 @@ impl PacketReceiver {
Some((_gateway_id, message)) = self.gateways_reader.stream_map().next() => {
self.process_gateway_messages(message)
}
_ = shutdown.recv() => {
trace!("UpdateHandler: Received shutdown");
}
}
}
}
@@ -24,6 +24,7 @@ use std::pin::Pin;
use std::sync::Arc;
use std::task::Poll;
use std::time::Duration;
use task::ShutdownListener;
use gateway_client::bandwidth::BandwidthController;
@@ -176,7 +177,11 @@ impl PacketSender {
}
}
pub(crate) fn spawn_gateways_pinger(&self, pinging_interval: Duration) {
pub(crate) fn spawn_gateways_pinger(
&self,
pinging_interval: Duration,
shutdown: ShutdownListener,
) {
let gateway_pinger = GatewayPinger::new(
self.active_gateway_clients.clone(),
self.fresh_gateway_client_data
@@ -185,7 +190,7 @@ impl PacketSender {
pinging_interval,
);
tokio::spawn(async move { gateway_pinger.run().await });
tokio::spawn(async move { gateway_pinger.run(shutdown).await });
}
fn new_gateway_client_handle(
@@ -216,9 +221,8 @@ impl PacketSender {
Some(fresh_gateway_client_data.bandwidth_controller.clone()),
);
if fresh_gateway_client_data.disabled_credentials_mode {
gateway_client.set_disabled_credentials_mode(true)
}
gateway_client
.set_disabled_credentials_mode(fresh_gateway_client_data.disabled_credentials_mode);
(
GatewayClientHandle::new(gateway_client),
+257
View File
@@ -0,0 +1,257 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use rocket::fairing::AdHoc;
use serde::Serialize;
use tap::TapFallible;
use tokio::{
sync::{watch, RwLock},
time,
};
use std::{sync::Arc, time::Duration};
use mixnet_contract_common::{reward_params::EpochRewardParams, MixNodeBond};
use task::ShutdownListener;
use validator_api_requests::models::InclusionProbability;
use crate::contract_cache::{Cache, CacheNotification, ValidatorCache};
const CACHE_TIMOUT_MS: u64 = 100;
const MAX_SIMULATION_SAMPLES: u64 = 5000;
const MAX_SIMULATION_TIME_SEC: u64 = 15;
enum NodeStatusCacheError {
SimulationFailed,
}
// A node status cache suitable for caching values computed in one sweep, such as active set
// inclusion probabilities that are computed for all mixnodes at the same time.
//
// The cache can be triggered to update on contract cache changes, and/or periodically on a timer.
#[derive(Clone)]
pub struct NodeStatusCache {
inner: Arc<RwLock<NodeStatusCacheInner>>,
}
struct NodeStatusCacheInner {
inclusion_probabilities: Cache<InclusionProbabilities>,
}
#[derive(Clone, Default, Serialize, schemars::JsonSchema)]
pub(crate) struct InclusionProbabilities {
pub inclusion_probabilities: Vec<InclusionProbability>,
pub samples: u64,
pub elapsed: Duration,
pub delta_max: f64,
pub delta_l2: f64,
}
impl InclusionProbabilities {
pub fn node(&self, id: &str) -> Option<&InclusionProbability> {
self.inclusion_probabilities.iter().find(|x| x.id == id)
}
}
impl NodeStatusCache {
fn new() -> Self {
NodeStatusCache {
inner: Arc::new(RwLock::new(NodeStatusCacheInner::new())),
}
}
pub fn stage() -> AdHoc {
AdHoc::on_ignite("Node Status Cache", |rocket| async {
rocket.manage(Self::new())
})
}
async fn update_cache(&self, inclusion_probabilities: InclusionProbabilities) {
match time::timeout(Duration::from_millis(CACHE_TIMOUT_MS), self.inner.write()).await {
Ok(mut cache) => {
cache
.inclusion_probabilities
.update(inclusion_probabilities);
}
Err(e) => error!("{e}"),
}
}
pub(crate) async fn inclusion_probabilities(&self) -> Option<Cache<InclusionProbabilities>> {
match time::timeout(Duration::from_millis(CACHE_TIMOUT_MS), self.inner.read()).await {
Ok(cache) => Some(cache.inclusion_probabilities.clone()),
Err(e) => {
error!("{e}");
None
}
}
}
}
impl NodeStatusCacheInner {
pub fn new() -> Self {
Self {
inclusion_probabilities: Default::default(),
}
}
}
// Long running task responsible of keeping the cache up-to-date.
pub struct NodeStatusCacheRefresher {
cache: NodeStatusCache,
contract_cache: ValidatorCache,
contract_cache_listener: watch::Receiver<CacheNotification>,
fallback_caching_interval: Duration,
}
impl NodeStatusCacheRefresher {
pub(crate) fn new(
cache: NodeStatusCache,
contract_cache: ValidatorCache,
contract_cache_listener: watch::Receiver<CacheNotification>,
fallback_caching_interval: Duration,
) -> Self {
Self {
cache,
contract_cache,
contract_cache_listener,
fallback_caching_interval,
}
}
pub async fn run(&mut self, mut shutdown: ShutdownListener) {
let mut fallback_interval = time::interval(self.fallback_caching_interval);
while !shutdown.is_shutdown() {
tokio::select! {
biased;
_ = shutdown.recv() => {
log::trace!("NodeStatusCacheRefresher: Received shutdown");
}
// Update node status cache when the contract cache / validator cache is updated
Ok(_) = self.contract_cache_listener.changed() => {
tokio::select! {
_ = self.update_on_notify(&mut fallback_interval) => (),
_ = shutdown.recv() => {
log::trace!("NodeStatusCacheRefresher: Received shutdown");
}
}
}
// ... however, if we don't receive any notifications we fall back to periodic
// refreshes
_ = fallback_interval.tick() => {
tokio::select! {
_ = self.update_on_timer() => (),
_ = shutdown.recv() => {
log::trace!("NodeStatusCacheRefresher: Received shutdown");
}
}
}
}
}
log::info!("NodeStatusCacheRefresher: Exiting");
}
async fn update_on_notify(&self, fallback_interval: &mut time::Interval) {
log::debug!(
"Validator cache event detected: {:?}",
&*self.contract_cache_listener.borrow(),
);
let _ = self.refresh_cache().await;
fallback_interval.reset();
}
async fn update_on_timer(&self) {
log::debug!("Timed trigger for the node status cache");
let have_contract_cache_data =
*self.contract_cache_listener.borrow() != CacheNotification::Start;
if have_contract_cache_data {
let _ = self.refresh_cache().await;
} else {
log::trace!(
"Skipping updating node status cache, is the contract cache not yet available?"
);
}
}
async fn refresh_cache(&self) -> Result<(), NodeStatusCacheError> {
log::info!("Updating node status cache");
let mixnode_bonds = self.contract_cache.mixnodes().await;
let params = self.contract_cache.epoch_reward_params().await.into_inner();
let inclusion_probabilities = compute_inclusion_probabilities(&mixnode_bonds, params)
.ok_or_else(|| {
error!(
"Failed to simulate selection probabilties for mixnodes, not updating cache"
);
NodeStatusCacheError::SimulationFailed
})?;
self.cache.update_cache(inclusion_probabilities).await;
Ok(())
}
}
fn compute_inclusion_probabilities(
mixnode_bonds: &[MixNodeBond],
params: EpochRewardParams,
) -> Option<InclusionProbabilities> {
let active_set_size = params
.active_set_size()
.try_into()
.tap_err(|e| error!("Active set size unexpectantly large: {e}"))
.ok()?;
let standby_set_size = (params.rewarded_set_size() - params.active_set_size())
.try_into()
.tap_err(|e| error!("Active set size larger than rewarded set size, a contradiction: {e}"))
.ok()?;
// Unzip list of total bonds into ids and bonds.
// We need to go through this zip/unzip procedure to make sure we have matching identities
// for the input to the simulator, which assumes the identity is the position in the vec
let (ids, mixnode_total_bonds) = unzip_into_mixnode_ids_and_total_bonds(mixnode_bonds);
// Compute inclusion probabilitites and keep track of how long time it took.
let mut rng = rand::thread_rng();
let results = inclusion_probability::simulate_selection_probability_mixnodes(
&mixnode_total_bonds,
active_set_size,
standby_set_size,
MAX_SIMULATION_SAMPLES,
Duration::from_secs(MAX_SIMULATION_TIME_SEC),
&mut rng,
)
.tap_err(|err| error!("{err}"))
.ok()?;
Some(InclusionProbabilities {
inclusion_probabilities: zip_ids_together_with_results(&ids, &results),
samples: results.samples,
elapsed: results.time,
delta_max: results.delta_max,
delta_l2: results.delta_l2,
})
}
fn unzip_into_mixnode_ids_and_total_bonds(
mixnode_bonds: &[MixNodeBond],
) -> (Vec<&String>, Vec<u128>) {
mixnode_bonds
.iter()
.filter_map(|m| m.total_bond().map(|b| (m.identity(), b)))
.unzip()
}
fn zip_ids_together_with_results(
ids: &[&String],
results: &inclusion_probability::SelectionProbability,
) -> Vec<InclusionProbability> {
ids.iter()
.zip(results.active_set_probability.iter())
.zip(results.reserve_set_probability.iter())
.map(|((id, a), r)| InclusionProbability {
id: (*id).to_string(),
in_active: *a,
in_reserve: *r,
})
.collect()
}
+5
View File
@@ -1,11 +1,14 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub(crate) use cache::{NodeStatusCache, NodeStatusCacheRefresher};
use okapi::openapi3::OpenApi;
use rocket::Route;
use rocket_okapi::{openapi_get_routes_spec, settings::OpenApiSettings};
use std::time::Duration;
pub(crate) mod cache;
pub(crate) mod local_guard;
pub(crate) mod models;
pub(crate) mod routes;
@@ -35,6 +38,7 @@ pub(crate) fn node_status_routes(
routes::get_mixnode_inclusion_probability,
routes::get_mixnode_avg_uptime,
routes::get_mixnode_avg_uptimes,
routes::get_mixnode_inclusion_probabilities,
]
} else {
// in the minimal variant we would not have access to endpoints relying on existence
@@ -43,6 +47,7 @@ pub(crate) fn node_status_routes(
routes::get_mixnode_status,
routes::get_mixnode_stake_saturation,
routes::get_mixnode_inclusion_probability,
routes::get_mixnode_inclusion_probabilities,
]
}
}

Some files were not shown because too many files have changed in this diff Show More