Compare commits

...

27 Commits

Author SHA1 Message Date
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
94 changed files with 1879 additions and 545 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
+9 -33
View File
@@ -894,6 +894,7 @@ dependencies = [
"cfg-if 0.1.10",
"clap 3.2.8",
"coconut-interface",
"config",
"credential-storage",
"credentials",
"crypto",
@@ -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"
@@ -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"
@@ -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",
]
@@ -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",
@@ -4922,12 +4910,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 +4971,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 +5189,7 @@ version = "0.1.0"
dependencies = [
"nymsphinx-addressing",
"ordered-buffer",
"thiserror",
]
[[package]]
@@ -6413,8 +6396,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",
]
@@ -6510,15 +6491,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 +6508,6 @@ dependencies = [
"tokio-stream",
"tokio-util 0.6.9",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web3-async-native-tls",
]
@@ -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,
+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
+1 -1
View File
@@ -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" }
+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);
+1 -4
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);
+4 -4
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
@@ -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;
@@ -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");
},
}
+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());
}
}
+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,
]
}
}
+58 -38
View File
@@ -1,6 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::contract_cache::Cache;
use crate::node_status_api::models::{
ErrorResponse, GatewayStatusReport, GatewayUptimeHistory, MixnodeStatusReport,
MixnodeUptimeHistory,
@@ -16,11 +17,12 @@ use rocket_okapi::openapi;
use schemars::JsonSchema;
use serde::Deserialize;
use validator_api_requests::models::{
CoreNodeStatusResponse, InclusionProbabilityResponse, MixnodeStatusResponse,
RewardEstimationResponse, StakeSaturationResponse, UptimeResponse,
AllInclusionProbabilitiesResponse, CoreNodeStatusResponse, InclusionProbabilityResponse,
MixnodeStatusResponse, RewardEstimationResponse, StakeSaturationResponse, UptimeResponse,
};
use super::models::Uptime;
use super::NodeStatusCache;
async fn average_mixnode_uptime(
identity: &str,
@@ -227,6 +229,12 @@ pub(crate) async fn compute_mixnode_reward_estimation(
// For these parameters we either use the provided ones, or fall back to the system ones
let uptime = if let Some(uptime) = user_reward_param.uptime {
if uptime > 100 {
return Err(ErrorResponse::new(
"Provided uptime out of bounds",
Status::UnprocessableEntity,
));
}
uptime
} else {
average_mixnode_uptime(&identity, current_epoch, storage)
@@ -245,13 +253,23 @@ pub(crate) async fn compute_mixnode_reward_estimation(
bond.mixnode_bond.total_delegation.amount = total_delegation.into();
}
if bond.mixnode_bond.pledge_amount.amount.u128()
+ bond.mixnode_bond.total_delegation.amount.u128()
> reward_params.staking_supply()
{
return Err(ErrorResponse::new(
"Pledge plus delegation too large",
Status::UnprocessableEntity,
));
}
let node_reward_params = NodeRewardParams::new(0, u128::from(uptime), is_active);
let reward_params = RewardParams::new(reward_params, node_reward_params);
estimate_reward(&bond.mixnode_bond, base_operator_cost, reward_params, as_at)
} else {
Err(ErrorResponse::new(
"mixnode bond not found",
"Mixnode bond not found",
Status::NotFound,
))
}
@@ -291,43 +309,21 @@ pub(crate) async fn get_mixnode_stake_saturation(
#[openapi(tag = "status")]
#[get("/mixnode/<identity>/inclusion-probability")]
pub(crate) async fn get_mixnode_inclusion_probability(
cache: &State<ValidatorCache>,
node_status_cache: &State<NodeStatusCache>,
identity: String,
) -> Json<Option<InclusionProbabilityResponse>> {
let mixnodes = cache.mixnodes().await;
let rewarding_params = cache.epoch_reward_params().await.into_inner();
if let Some(target_mixnode) = mixnodes.iter().find(|x| x.identity() == &identity) {
let total_bonded_tokens = mixnodes
.iter()
.fold(0u128, |acc, x| acc + x.total_bond().unwrap_or_default())
as f64;
let rewarded_set_size = rewarding_params.rewarded_set_size() as f64;
let active_set_size = rewarding_params.active_set_size() as f64;
let prob_one_draw =
target_mixnode.total_bond().unwrap_or_default() as f64 / total_bonded_tokens;
// Chance to be selected in any draw for active set
let prob_active_set = if mixnodes.len() <= active_set_size as usize {
1.0
} else {
active_set_size * prob_one_draw
};
// This is likely slightly too high, as we're not correcting form them not being selected in active, should be chance to be selected, minus the chance for being not selected in reserve
let prob_reserve_set = if mixnodes.len() <= rewarded_set_size as usize {
1.0
} else {
(rewarded_set_size - active_set_size) * prob_one_draw
};
Json(Some(InclusionProbabilityResponse {
in_active: prob_active_set.into(),
in_reserve: prob_reserve_set.into(),
}))
} else {
Json(None)
}
node_status_cache
.inclusion_probabilities()
.await
.map(Cache::into_inner)
.and_then(|p| p.node(&identity).cloned())
.map(|p| {
Json(Some(InclusionProbabilityResponse {
in_active: p.in_active.into(),
in_reserve: p.in_reserve.into(),
}))
})
.unwrap_or(Json(None))
}
#[openapi(tag = "status")]
@@ -368,3 +364,27 @@ pub(crate) async fn get_mixnode_avg_uptimes(
Ok(Json(response))
}
#[openapi(tag = "status")]
#[get("/mixnodes/inclusion_probability")]
pub(crate) async fn get_mixnode_inclusion_probabilities(
cache: &State<NodeStatusCache>,
) -> Result<Json<AllInclusionProbabilitiesResponse>, ErrorResponse> {
if let Some(prob) = cache.inclusion_probabilities().await {
let as_at = prob.timestamp();
let prob = prob.into_inner();
Ok(Json(AllInclusionProbabilitiesResponse {
inclusion_probabilities: prob.inclusion_probabilities,
samples: prob.samples,
elapsed: prob.elapsed,
delta_max: prob.delta_max,
delta_l2: prob.delta_l2,
as_at,
}))
} else {
Err(ErrorResponse::new(
"No data available".to_string(),
Status::ServiceUnavailable,
))
}
}
@@ -72,14 +72,20 @@ impl HistoricalUptimeUpdater {
while !shutdown.is_shutdown() {
tokio::select! {
_ = sleep(ONE_DAY) => {
if let Err(err) = self.update_uptimes().await {
// normally that would have been a warning rather than an error,
// however, in this case it implies some underlying issues with our database
// that might affect the entire program
error!(
"We failed to update daily uptimes of active nodes - {}",
err
)
tokio::select! {
biased;
_ = shutdown.recv() => {
trace!("UpdateHandler: Received shutdown");
}
Err(err) = self.update_uptimes() => {
// normally that would have been a warning rather than an error,
// however, in this case it implies some underlying issues with our database
// that might affect the entire program
error!(
"We failed to update daily uptimes of active nodes - {}",
err
);
}
}
}
_ = shutdown.recv() => {
+16 -3
View File
@@ -24,6 +24,7 @@ use rand::rngs::OsRng;
use std::collections::HashSet;
use std::ops::Deref;
use std::time::Duration;
use task::ShutdownListener;
use time::OffsetDateTime;
use tokio::time::sleep;
use validator_client::nymd::{Coin, SigningNymdClient};
@@ -328,11 +329,14 @@ impl RewardedSetUpdater {
Ok(())
}
pub(crate) async fn run(&mut self) -> Result<(), RewardingError> {
pub(crate) async fn run(
&mut self,
mut shutdown: ShutdownListener,
) -> Result<(), RewardingError> {
self.validator_cache.wait_for_initial_values().await;
let mut epoch = self.epoch().await?;
loop {
while !shutdown.is_shutdown() {
// wait until the cache refresher determined its time to update the rewarded/active sets
let time = OffsetDateTime::now_utc().unix_timestamp();
epoch.update_to_latest(self).await?;
@@ -351,7 +355,16 @@ impl RewardedSetUpdater {
);
// Sleep at most 300 before checking again, to keep logs busy
let s = time_to_epoch_change.min(300).max(0) as u64;
sleep(Duration::from_secs(s)).await;
tokio::select! {
_ = sleep(Duration::from_secs(s)) => {
trace!("Checking again for updating rewarded/active sets");
}
_ = shutdown.recv() => {
trace!("RewardedSetUpdater: Received shutdown");
// This break should not be necessary, but there's a following sleep after this
break;
}
}
}
// allow some blocks to pass
sleep(Duration::from_secs(10)).await;
@@ -4,7 +4,7 @@
use mixnet_contract_common::{reward_params::RewardParams, MixNode, MixNodeBond};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::{fmt, time::Duration};
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
@@ -101,16 +101,20 @@ pub type StakeSaturation = f32;
)]
pub enum SelectionChance {
VeryHigh,
High,
Moderate,
Low,
VeryLow,
}
impl From<f64> for SelectionChance {
fn from(p: f64) -> SelectionChance {
match p {
p if p > 0.15 => SelectionChance::VeryHigh,
p if p >= 0.05 => SelectionChance::Moderate,
_ => SelectionChance::Low,
p if p > 0.98 => SelectionChance::VeryHigh,
p if p > 0.9 => SelectionChance::High,
p if p > 0.7 => SelectionChance::Moderate,
p if p > 0.5 => SelectionChance::Low,
_ => SelectionChance::VeryLow,
}
}
}
@@ -119,8 +123,10 @@ impl fmt::Display for SelectionChance {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SelectionChance::VeryHigh => write!(f, "VeryHigh"),
SelectionChance::High => write!(f, "High"),
SelectionChance::Moderate => write!(f, "Moderate"),
SelectionChance::Low => write!(f, "Low"),
SelectionChance::VeryLow => write!(f, "VeryLow"),
}
}
}
@@ -145,3 +151,20 @@ impl fmt::Display for InclusionProbabilityResponse {
)
}
}
#[derive(Clone, Serialize, schemars::JsonSchema)]
pub struct AllInclusionProbabilitiesResponse {
pub inclusion_probabilities: Vec<InclusionProbability>,
pub samples: u64,
pub elapsed: Duration,
pub delta_max: f64,
pub delta_l2: f64,
pub as_at: i64,
}
#[derive(Clone, Serialize, schemars::JsonSchema)]
pub struct InclusionProbability {
pub id: String,
pub in_active: f64,
pub in_reserve: f64,
}