Compare commits

...

43 Commits

Author SHA1 Message Date
benedettadavico b7aa84cd5a updating mainnet mixnet conract address 2022-11-15 19:54:28 +01:00
Fouad bfbd509e4b Update/last minute release updates (#1753)
* fix vesting update bond settings

* style and text updates

* show tx fee when updating node settings

* allow cost param update with vesting tokens
2022-11-10 16:22:09 +00:00
Jon Häggblad 09b9601c7e Update client-core to 1.1.0 2022-11-10 09:38:38 +01:00
Mark Sinclair 2a1dd138e0 GH Actions: Install same dependencies as build..yml 2022-11-09 18:21:28 +00:00
Mark Sinclair 9874daa061 Release v1.1.0: bump versions and update CHANGELOGs (#1746)
* Bump version of nym-cli to 1.1.0 and move CHANGELOG to standalone file

* Bump version of nym-connect to v1.1.0 and update CHANGELOG

* Bump version of nym-wallet to v1.1.0 and update CHANGELOG

* Bump version of explorer-api to v1.1.0

* Bump versions of binaries (native-client, socks5-client, mixnode, gateway, network-requester) to v1.1.0

* Bump version of validator-api to v1.1.0

* Bump version of mixnet contract to v1.1.0 (vesting contract already v1.1.0 from #1472)

* Bump Nym Platform version to v1.1.0 and update CHANGELOG

* Update CHANGELOG.md

* Update CHANGELOG.md

* Updated changelog with v2-related changes

* Update CHANGELOG.md

Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
Co-authored-by: Fouad <fmtabbara@hotmail.co.uk>
Co-authored-by: Pierre Dommerc <dommerc.pierre@gmail.com>
Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>
Co-authored-by: Jess <31625607+jessgess@users.noreply.github.com>
2022-11-09 15:04:41 +00:00
Fouad baa61c07d5 Bug fix/ne snag list (#1741)
* use uncapped saturation

* display uncapped saturation

update profit margin tooltip

update operating cost

update rewards tooltip

update stake saturation tooltip

update reward tooltips

update profit margin tooltip

* allow full gateway field to be clickable

use uncapped saturation on mixnode details page
2022-11-09 12:39:14 +00:00
Jon Häggblad 62e9c8236a Remove log from vesting-contract (#1745)
* Remove log from vesting-contract

* rustfmt
2022-11-09 11:04:32 +01:00
durch cf268ffcd5 Actually save updated pledge cap 2022-11-09 10:19:21 +01:00
Drazen Urch b28ff17c30 Set default pledge cap to 10% (#1739)
* Set default pledge cap to 10%

* fix clippy beta lints
2022-11-07 14:40:52 +01:00
Fouad a7cd7a58f2 Bugfix/delegations sort by no bonded node (#1737)
* use sorting function

* create hardcoded examples in  storybook
2022-11-03 13:44:09 +00:00
Gala 8bbdb94b13 Merge pull request #1733 from nymtech/417-inputs-label
Wallet: make input's label always shrink
2022-11-02 16:42:17 +01:00
Pierre Dommerc e32601ab86 feat(wallet): normalize decimal places in ui (#1731) 2022-11-02 15:48:49 +01:00
Gala 161138bdff Merge branch 'release/v1.1.0' into 417-inputs-label 2022-11-02 14:26:03 +01:00
Fouad 0529e84a31 add account how to links (#1732) 2022-11-02 13:15:48 +00:00
Gala 95f98016de make label always shrink 2022-11-02 13:49:29 +01:00
Fouad 4967bbb5bd Feature/delegations without bonded node (#1727)
* refactor delegations list to include separate delegation and pending delegation item

* show tooltip on delegation with unbonded node

* feat(wallet): add operating cost in delegations list

* add additional state to check for unbonding event

* disable actions when pending unbond event

* add request and type guard for pending unbond event

* add mixnode_is_unbonding to delegation item type

Co-authored-by: pierre <dommerc.pierre@gmail.com>
2022-11-02 10:46:45 +00:00
Fouad 2952144d32 add profit margin percent to response (#1729)
* add profit margin percent to response

* use display percentage function

* fix profit margin display

* fix up filters
2022-11-01 13:02:34 +00:00
Jędrzej Stuczyński 80c21b3ed9 (chore) setting up a common/logging crate (#1730) 2022-11-01 11:46:47 +00:00
Jędrzej Stuczyński 1f0d5f8ad0 Feature/vesting contract version query (#1726)
* Introduced vesting contract query for build information

* Fixed import paths

* Changelog
2022-10-31 17:37:16 +00:00
Jędrzej Stuczyński 49ce56c367 Jedrzej/feature/version field in framed sphinx packets (#1723)
* introduced PacketVersion into FramedSphinxPacket

* Using legacy mode by default in mixnodes and gateways

* fixed unit tests
2022-10-31 16:56:37 +00:00
Pierre Dommerc 4ab6f4c3a9 refactor(explorer-api): route ping use mix_id as param (#1728) 2022-10-31 16:58:30 +01:00
Jędrzej Stuczyński 3727370b9e Improved error propagation for fallible validator api queries (#1681)
* Improved error propagation for fallible validator api queries

* Updated changelog
2022-10-31 15:28:54 +00:00
Jędrzej Stuczyński b3272097f9 Jedrzej/bugfix/historical uptimes recording (#1721)
* Removed commented out type alias

* typos

* Using the same  underlying timer for uptime updater

* Updating uptimes at 23:00 UTC each day

* Changelog
2022-10-31 12:19:14 +00:00
Jon Häggblad ebc13c4327 client: make channel to mix traffic controller bounded and add backpressure handling v1.1 (#1725)
* clients: change mix traffic channel to bounded

* clients: dynamically adjust sending delay in steps

* rustfmt

* wasm-client: update channel

* client: introduce SendingDelayController

* client-core: downgrade two debug statements to trace

* sending delay controller: tweak parameters

* wasm-client: add tokio dependency

* client-core: rework delay controller

* Revert "client-core: downgrade two debug statements to trace"

This reverts commit e0a7772fafac7bff0e4a2c50ba25e94b52b794e6.

* Remove outdated comment

* Remove WIP comments

* changelog: add note

* out queue controller: simplify with just send

* client-core: document constants

* client: move creating mix msg channel to mix traffic controller

* client-core: downgrade a warning log msg to debug

* changelog: update
2022-10-31 12:21:02 +01:00
Jon Häggblad ec3a6b3e27 socks5: wait to close buffer (v1.1 branch) (#1724)
* socks5: wait to close buffer

This is the fix proposed by @simonwicky in
https://github.com/nymtech/nym/issues/1701

* socks5: fix typo in patch

* socks5: fix tests

* socks5: add type for returned data and index

* socks5: make closed_at_index an Option

* changelog: add note

* changelog: update
2022-10-31 11:56:31 +01:00
Pierre Dommerc 19f3c76f72 Feature/explorer operating cost (#1719)
* feat(explorer): operating cost
2022-10-28 16:24:52 +02:00
Pierre Dommerc 90cc239999 Feature/ne gateway details (#1722)
* create gateway details page

* adding uptime chart

* adding loading state for gateways

* adding link style

* fixing gateways pagination

* remove gateway name and desc

* adding correct toolpit text and cleaning

* fix build

* PR requested changes

* fix build

* requested changes

* fix build a rever console utility addition

Co-authored-by: Gala <calero.vg@gmail.com>
2022-10-28 15:12:37 +02:00
Jędrzej Stuczyński c1bd5db902 comment regarding ts-rs compilation warning 2022-10-28 14:00:54 +01:00
Pierre Dommerc fb1649bab5 fix(explorer): gateway list location column (#1718)
* fix(explorer): gateway list location column

* fix(explorer): gateway list columns width
2022-10-28 12:17:23 +01:00
Jędrzej Stuczyński b21ca41e16 Adding staking supply scale factor in rewarding params update (#1716) 2022-10-28 12:17:17 +01:00
Pierre Dommerc 8656abcbde fix(explorer): minoxde details page (#1715)
* fix(explorer): minoxde details page

* feat(explorer): add mix_id column in mixnodes list
2022-10-27 17:16:30 +02:00
Tommy Verrall 3ae9ea5de6 Merge pull request #1709 from nymtech/fix/explorerapi-geoip-lookup
fix(explorer-api): geoip lookup
2022-10-27 12:47:36 +02:00
Jędrzej Stuczyński 8bcec241a2 Moves Percent tests to the crate with the type definition (#1714) 2022-10-27 11:33:33 +01:00
Jędrzej Stuczyński 306e9b9dc2 Bugfix/correct staking supply accounting (#1706)
* Added staking_supply_scale_factor field on RewardingParams

* Scaling the amount of tokens released to the circulating/staking supplies
2022-10-27 10:51:09 +01:00
Fouad d36e349cc6 Display routing scores for bonded gateway (#1693)
* access gateway report from node status api

* Create 4 response types for gateway and mixnode uptime and status

* Add the three remaining validator-client functions

* display gatways routing scores

* handle undefined gateway report

Co-authored-by: Jon Häggblad <jon.haggblad@gmail.com>
2022-10-27 10:34:11 +01:00
Jon Häggblad 4990a4745f Add two more sphinx extended packet sizes (8kb and 16kb) (#1694)
* Rename to ExtendedPacketSize32

* Add two more extended packet sizes

* Update config handling for new packet sizes

* Update wasm-client

* Changelog: update

* wasm-client: fix ref

* Switch use enum instead of string for config
2022-10-27 10:52:57 +02:00
Jon Häggblad 0d399f7d70 connect: tidy error enum (#1712) 2022-10-26 17:26:31 +02:00
Jon Häggblad 56cf181770 validator-api: use response type for history and report endpoints (#1711) 2022-10-26 17:26:17 +02:00
Tommy Verrall f0aa2feb76 Merge pull request #1710 from nymtech/fix/explorer-table-ui
fix(explorer): mixnode location overflow
2022-10-26 17:13:29 +02:00
pierre 4df927cc3d fix(explorer): mixnode location overflow 2022-10-26 16:51:25 +02:00
pierre 3f4373eb98 feat(explorer-api): add debug logs 2022-10-26 12:29:01 +02:00
pierre cf10bb12ef feat(explorer-api): use mixnode port in ip lookup 2022-10-26 12:23:33 +02:00
pierre cb1e93e58d fix(explorer-api): geoip lookup 2022-10-26 12:06:47 +02:00
180 changed files with 2956 additions and 1061 deletions
@@ -25,6 +25,9 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools
- name: Check the release tag starts with `nym-binaries-`
if: startsWith(github.ref, 'refs/tags/nym-binaries-') == false && github.event_name != 'workflow_dispatch'
uses: actions/github-script@v3
+28 -15
View File
@@ -2,33 +2,40 @@
Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
## [v1.1.0](https://github.com/nymtech/nym/tree/v1.1.0) (2022-11-09)
### Added
- nym-cli: added CLI tool for interacting with the Nyx blockchain and Nym mixnet smart contracts ([#1577])
- validator-client: added `query_contract_smart` and `query_contract_raw` on `NymdClient` ([#1558])
- network-requester: added additional Blockstream Green wallet endpoint to `example.allowed.list` ([#1611](https://github.com/nymtech/nym/pull/1611))
- clients: add testing-only support for two more extended packet sizes (8kb and 16kb).
- common/ledger: new library for communicating with a Ledger device ([#1640])
- native-client/socks5-client: `disable_loop_cover_traffic_stream` Debug config option to disable the separate loop cover traffic stream ([#1666])
- native-client/socks5-client: `disable_main_poisson_packet_distribution` Debug config option to make the client ignore poisson distribution in the main packet stream and ONLY send real message (and as fast as they come) ([#1664])
- native-client/socks5-client: `use_extended_packet_size` Debug config option to make the client use 'ExtendedPacketSize' for its traffic (32kB as opposed to 2kB in 1.0.2) ([#1671])
- native-client/socks5-client/wasm-client: `disable_loop_cover_traffic_stream` Debug config option to disable the separate loop cover traffic stream ([#1666])
- native-client/socks5-client/wasm-client: `disable_main_poisson_packet_distribution` Debug config option to make the client ignore poisson distribution in the main packet stream and ONLY send real message (and as fast as they come) ([#1664])
- native-client/socks5-client/wasm-client: `use_extended_packet_size` Debug config option to make the client use 'ExtendedPacketSize' for its traffic (32kB as opposed to 2kB in 1.0.2) ([#1671])
- network-requester: added additional Blockstream Green wallet endpoint to `example.allowed.list` ([#1611])
- validator-api: add `interval_operating_cost` and `profit_margin_percent` to compute reward estimation endpoint
- validator-client: added `query_contract_smart` and `query_contract_raw` on `NymdClient` ([#1558])
- wasm-client: uses updated wasm-compatible `client-core` so that it's now capable of packet retransmission, cover traffic and poisson delay (among other things!) ([#1673])
- validator-api: add `interval_operating_cost` and `profit_margin_percent` to cmpute reward estimation endpoint
- vesting-contract: optional locked token pledge cap per account ([#1687]), defaults to 100_000 NYM
### Fixed
- validator-api, mixnode, gateway should now prefer values in config.toml over mainnet defaults ([#1645])
- socks5-client: fix bug where in some cases packet reordering could trigger a connection being closed too early ([#1702],[#1724])
- validator-api: mixnode, gateway should now prefer values in config.toml over mainnet defaults ([#1645])
- validator-api: should now correctly update historical uptimes for all mixnodes and gateways every 24h ([#1721])
### Changed
- validator-client: made `fee` argument optional for `execute` and `execute_multiple` ([#1541])
- socks5 client: graceful shutdown should fix error on disconnect in nym-connect ([#1591])
- wasm-client: fixed build errors on MacOS and changed example JS code to use mainnet ([#1585])
- clients: bound the sphinx packet channel and reduce sending rate if gateway can't keep up ([#1703],[#1725])
- gateway-client: will attempt to read now as many as 8 websocket messages at once, assuming they're already available on the socket ([#1669])
- moved `Percent` struct to to `contracts-common`, change affects explorer-api
- moved `Percent` struct to `contracts-common`, change affects explorer-api
- socks5 client: graceful shutdown should fix error on disconnect in nym-connect ([#1591])
- validator-api: changed error serialization on `inclusion_probability`, `stake-saturation` and `reward-estimation` endpoints to provide more accurate information ([#1681])
- 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])
- validator-api: changes to internal SQL schema due to the mixnet contract revamp ([#1472])
- validator-api: changes to internal data structures due to the mixnet contract revamp ([#1472])
- validator-api: split epoch-operations into multiple separate transactions ([#1472])
[#1472]: https://github.com/nymtech/nym/pull/1472
[#1541]: https://github.com/nymtech/nym/pull/1541
[#1558]: https://github.com/nymtech/nym/pull/1558
[#1577]: https://github.com/nymtech/nym/pull/1577
@@ -36,12 +43,18 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
[#1591]: https://github.com/nymtech/nym/pull/1591
[#1640]: https://github.com/nymtech/nym/pull/1640
[#1645]: https://github.com/nymtech/nym/pull/1645
[#1611]: https://github.com/nymtech/nym/pull/1611
[#1664]: https://github.com/nymtech/nym/pull/1664
[#1666]: https://github.com/nymtech/nym/pull/1645
[#1669]: https://github.com/nymtech/nym/pull/1669
[#1671]: https://github.com/nymtech/nym/pull/1671
[#1673]: https://github.com/nymtech/nym/pull/1673
[#1687]: https://github.com/nymtech/nym/pull/1687
[#1681]: https://github.com/nymtech/nym/pull/1681
[#1702]: https://github.com/nymtech/nym/pull/1702
[#1703]: https://github.com/nymtech/nym/pull/1703
[#1721]: https://github.com/nymtech/nym/pull/1721
[#1724]: https://github.com/nymtech/nym/pull/1724
[#1725]: https://github.com/nymtech/nym/pull/1725
## [nym-binaries-1.0.2](https://github.com/nymtech/nym/tree/nym-binaries-1.0.2)
Generated
+30 -11
View File
@@ -578,7 +578,7 @@ dependencies = [
[[package]]
name = "client-core"
version = "1.0.1"
version = "1.1.0"
dependencies = [
"config",
"crypto",
@@ -739,6 +739,7 @@ dependencies = [
"cosmwasm-std",
"schemars",
"serde",
"serde_json",
"thiserror",
]
@@ -1581,16 +1582,17 @@ dependencies = [
[[package]]
name = "explorer-api"
version = "1.0.1"
version = "1.1.0"
dependencies = [
"chrono",
"clap 3.2.8",
"dotenv",
"contracts-common",
"dotenv",
"humantime-serde",
"isocountry",
"itertools",
"log",
"logging",
"maxminddb",
"mixnet-contract-common",
"network-defaults",
@@ -2729,6 +2731,14 @@ dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "logging"
version = "0.1.0"
dependencies = [
"log",
"pretty_env_logger",
]
[[package]]
name = "loom"
version = "0.5.4"
@@ -3060,7 +3070,7 @@ dependencies = [
[[package]]
name = "nym-cli"
version = "1.0.0"
version = "1.1.0"
dependencies = [
"anyhow",
"base64",
@@ -3071,6 +3081,7 @@ dependencies = [
"clap_complete_fig",
"dotenv",
"log",
"logging",
"network-defaults",
"nym-cli-commands",
"pretty_env_logger",
@@ -3111,7 +3122,7 @@ dependencies = [
[[package]]
name = "nym-client"
version = "1.0.2"
version = "1.1.0"
dependencies = [
"clap 3.2.8",
"client-core",
@@ -3126,6 +3137,7 @@ dependencies = [
"gateway-client",
"gateway-requests",
"log",
"logging",
"network-defaults",
"nymsphinx",
"pemstore",
@@ -3147,7 +3159,7 @@ dependencies = [
[[package]]
name = "nym-gateway"
version = "1.0.2"
version = "1.1.0"
dependencies = [
"anyhow",
"async-trait",
@@ -3167,6 +3179,7 @@ dependencies = [
"gateway-requests",
"humantime-serde",
"log",
"logging",
"mixnet-client",
"mixnode-common",
"network-defaults",
@@ -3193,7 +3206,7 @@ dependencies = [
[[package]]
name = "nym-mixnode"
version = "1.0.2"
version = "1.1.0"
dependencies = [
"anyhow",
"bs58",
@@ -3209,6 +3222,7 @@ dependencies = [
"humantime-serde",
"lazy_static",
"log",
"logging",
"mixnet-client",
"mixnode-common",
"nonexhaustive-delayqueue",
@@ -3234,7 +3248,7 @@ dependencies = [
[[package]]
name = "nym-network-requester"
version = "1.0.2"
version = "1.1.0"
dependencies = [
"async-trait",
"clap 3.2.8",
@@ -3243,6 +3257,7 @@ dependencies = [
"futures",
"ipnetwork 0.20.0",
"log",
"logging",
"network-defaults",
"nymsphinx",
"ordered-buffer",
@@ -3268,6 +3283,7 @@ version = "1.0.2"
dependencies = [
"dirs",
"log",
"logging",
"pretty_env_logger",
"rocket",
"serde",
@@ -3279,7 +3295,7 @@ dependencies = [
[[package]]
name = "nym-socks5-client"
version = "1.0.2"
version = "1.1.0"
dependencies = [
"clap 3.2.8",
"client-core",
@@ -3294,6 +3310,7 @@ dependencies = [
"gateway-client",
"gateway-requests",
"log",
"logging",
"network-defaults",
"nymsphinx",
"ordered-buffer",
@@ -3342,7 +3359,7 @@ dependencies = [
[[package]]
name = "nym-validator-api"
version = "1.0.2"
version = "1.1.0"
dependencies = [
"anyhow",
"async-trait",
@@ -3367,6 +3384,7 @@ dependencies = [
"humantime-serde",
"inclusion-probability",
"log",
"logging",
"mixnet-contract-common",
"multisig-contract-common",
"nymcoconut",
@@ -6390,6 +6408,7 @@ dependencies = [
"coconut-interface",
"colored",
"config",
"contracts-common",
"cosmrs",
"cosmwasm-std",
"cw3",
@@ -6485,6 +6504,7 @@ dependencies = [
"schemars",
"serde",
"thiserror",
"vergen 5.1.17",
"vesting-contract-common",
]
@@ -6494,7 +6514,6 @@ version = "0.1.0"
dependencies = [
"contracts-common",
"cosmwasm-std",
"log",
"mixnet-contract-common",
"schemars",
"serde",
+1
View File
@@ -41,6 +41,7 @@ members = [
"common/execute",
"common/inclusion-probability",
"common/ledger",
"common/logging",
"common/mixnode-common",
"common/network-defaults",
"common/nonexhaustive-delayqueue",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "client-core"
version = "1.0.1"
version = "1.1.0"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
edition = "2021"
@@ -16,6 +16,7 @@ use rand::{rngs::OsRng, CryptoRng, Rng};
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc::error::TrySendError;
#[cfg(not(target_arch = "wasm32"))]
use tokio::time;
@@ -171,11 +172,18 @@ impl LoopCoverTrafficStream<OsRng> {
)
.expect("Somehow failed to generate a loop cover message with a valid topology");
// if this one fails, there's no retrying because it means that either:
// - we run out of memory
// - the receiver channel is closed
// in either case there's no recovery and we can only panic
self.mix_tx.unbounded_send(vec![cover_message]).unwrap();
if let Err(err) = self.mix_tx.try_send(vec![cover_message]) {
match err {
TrySendError::Full(_) => {
// This isn't a problem, if the channel is full means we're already sending the
// max amount of messages downstream can handle.
log::debug!("Failed to send cover message - channel full");
}
TrySendError::Closed(_) => {
log::warn!("Failed to send cover message - channel closed");
}
}
}
// TODO: I'm not entirely sure whether this is really required, because I'm not 100%
// sure how `yield_now()` works - whether it just notifies the scheduler or whether it
+17 -15
View File
@@ -2,15 +2,15 @@
// SPDX-License-Identifier: Apache-2.0
use crate::spawn_future;
use futures::channel::mpsc;
use futures::StreamExt;
use gateway_client::GatewayClient;
use log::*;
use nymsphinx::forwarding::packet::MixPacket;
pub type BatchMixMessageSender = mpsc::UnboundedSender<Vec<MixPacket>>;
pub type BatchMixMessageReceiver = mpsc::UnboundedReceiver<Vec<MixPacket>>;
pub type BatchMixMessageSender = tokio::sync::mpsc::Sender<Vec<MixPacket>>;
pub type BatchMixMessageReceiver = tokio::sync::mpsc::Receiver<Vec<MixPacket>>;
// We remind ourselves that 32 x 32kb = 1024kb, a reasonable size for a network buffer.
pub const MIX_MESSAGE_RECEIVER_BUFFER_SIZE: usize = 32;
const MAX_FAILURE_COUNT: usize = 100;
pub struct MixTrafficController {
@@ -25,15 +25,17 @@ pub struct MixTrafficController {
}
impl MixTrafficController {
pub fn new(
mix_rx: BatchMixMessageReceiver,
gateway_client: GatewayClient,
) -> MixTrafficController {
MixTrafficController {
gateway_client,
mix_rx,
consecutive_gateway_failure_count: 0,
}
pub fn new(gateway_client: GatewayClient) -> (MixTrafficController, BatchMixMessageSender) {
let (sphinx_message_sender, sphinx_message_receiver) =
tokio::sync::mpsc::channel(MIX_MESSAGE_RECEIVER_BUFFER_SIZE);
(
MixTrafficController {
gateway_client,
mix_rx: sphinx_message_receiver,
consecutive_gateway_failure_count: 0,
},
sphinx_message_sender,
)
}
async fn on_messages(&mut self, mut mix_packets: Vec<MixPacket>) {
@@ -72,7 +74,7 @@ impl MixTrafficController {
while !shutdown.is_shutdown() {
tokio::select! {
mix_packets = self.mix_rx.next() => match mix_packets {
mix_packets = self.mix_rx.recv() => match mix_packets {
Some(mix_packets) => {
self.on_messages(mix_packets).await;
},
@@ -96,7 +98,7 @@ impl MixTrafficController {
spawn_future(async move {
debug!("Started MixTrafficController without graceful shutdown support");
while let Some(mix_packets) = self.mix_rx.next().await {
while let Some(mix_packets) = self.mix_rx.recv().await {
self.on_messages(mix_packets).await;
}
})
@@ -27,6 +27,23 @@ use tokio::time;
#[cfg(target_arch = "wasm32")]
use wasm_timer;
// The minimum time between increasing the average delay between packets. If we hit the ceiling in
// the available buffer space we want to take somewhat swift action, but we still need to give a
// short time to give the channel a chance reduce pressure.
const INCREASE_DELAY_MIN_CHANGE_INTERVAL_SECS: u64 = 1;
// The minimum time between decreasing the average delay between packets. We don't want to change
// to quickly to keep things somewhat stable. Also there are buffers downstreams meaning we need to
// wait a little to see the effect before we decrease further.
const DECREASE_DELAY_MIN_CHANGE_INTERVAL_SECS: u64 = 30;
// If we enough time passes without any sign of backpressure in the channel, we can consider
// lowering the average delay. The goal is to keep somewhat stable, rather than maxing out
// bandwidth at all times.
const ACCEPTABLE_TIME_WITHOUT_BACKPRESSURE_SECS: u64 = 30;
// The maximum multiplier we apply to the base average Poisson delay.
const MAX_DELAY_MULTIPLIER: u32 = 6;
// The minium multiplier we apply to the base average Poisson delay.
const MIN_DELAY_MULTIPLIER: u32 = 1;
/// Configurable parameters of the `OutQueueControl`
pub(crate) struct Config {
/// Average delay an acknowledgement packet is going to get delay at a single mixnode.
@@ -68,6 +85,101 @@ impl Config {
}
}
struct SendingDelayController {
/// Multiply the average sending delay.
/// This is normally set to unity, but if we detect backpressure we increase this
/// multiplier. We use discrete steps.
current_multiplier: u32,
/// Maximum delay multiplier
upper_bound: u32,
/// Minimum delay multiplier
lower_bound: u32,
/// To make sure we don't change the multiplier to fast, we limit a change to some duration
#[cfg(not(target_arch = "wasm32"))]
time_when_changed: time::Instant,
#[cfg(target_arch = "wasm32")]
time_when_changed: wasm_timer::Instant,
/// If we have a long enough time without any backpressure detected we try reducing the sending
/// delay multiplier
#[cfg(not(target_arch = "wasm32"))]
time_when_backpressure_detected: time::Instant,
#[cfg(target_arch = "wasm32")]
time_when_backpressure_detected: wasm_timer::Instant,
}
#[cfg(not(target_arch = "wasm32"))]
fn get_time_now() -> time::Instant {
time::Instant::now()
}
#[cfg(target_arch = "wasm32")]
fn get_time_now() -> wasm_timer::Instant {
wasm_timer::Instant::now()
}
impl SendingDelayController {
fn new(lower_bound: u32, upper_bound: u32) -> Self {
assert!(lower_bound <= upper_bound);
let now = get_time_now();
SendingDelayController {
current_multiplier: MIN_DELAY_MULTIPLIER,
upper_bound,
lower_bound,
time_when_changed: now,
time_when_backpressure_detected: now,
}
}
fn current_multiplier(&self) -> u32 {
self.current_multiplier
}
fn increase_delay_multiplier(&mut self) {
self.current_multiplier =
(self.current_multiplier + 1).clamp(self.lower_bound, self.upper_bound);
self.time_when_changed = get_time_now();
log::debug!(
"Increasing sending delay multiplier to: {}",
self.current_multiplier
);
}
fn decrease_delay_multiplier(&mut self) {
self.current_multiplier =
(self.current_multiplier - 1).clamp(self.lower_bound, self.upper_bound);
self.time_when_changed = get_time_now();
log::debug!(
"Decreasing sending delay multiplier to: {}",
self.current_multiplier
);
}
fn record_backpressure_detected(&mut self) {
self.time_when_backpressure_detected = get_time_now();
}
fn not_increased_delay_recently(&self) -> bool {
get_time_now()
> self.time_when_changed + Duration::from_secs(INCREASE_DELAY_MIN_CHANGE_INTERVAL_SECS)
}
fn is_sending_reliable(&self) -> bool {
let now = get_time_now();
let delay_change_interval = Duration::from_secs(DECREASE_DELAY_MIN_CHANGE_INTERVAL_SECS);
let acceptable_time_without_backpressure =
Duration::from_secs(ACCEPTABLE_TIME_WITHOUT_BACKPRESSURE_SECS);
now > self.time_when_backpressure_detected + acceptable_time_without_backpressure
&& now > self.time_when_changed + delay_change_interval
}
}
pub(crate) struct OutQueueControl<R>
where
R: CryptoRng + Rng,
@@ -89,6 +201,10 @@ where
#[cfg(target_arch = "wasm32")]
next_delay: Option<Pin<Box<wasm_timer::Delay>>>,
// To make sure we don't overload the mix_tx channel, we limit the rate we are pushing
// messages.
sending_rate_controller: SendingDelayController,
/// Channel used for sending prepared sphinx packets to `MixTrafficController` that sends them
/// out to the network without any further delays.
mix_tx: BatchMixMessageSender,
@@ -156,6 +272,10 @@ where
ack_key,
sent_notifier,
next_delay: None,
sending_rate_controller: SendingDelayController::new(
MIN_DELAY_MULTIPLIER,
MAX_DELAY_MULTIPLIER,
),
mix_tx,
real_receiver,
our_full_destination,
@@ -212,15 +332,8 @@ where
}
};
// if this one fails, there's no retrying because it means that either:
// - we run out of memory
// - the receiver channel is closed
// in either case there's no recovery and we can only panic
if let Err(err) = self.mix_tx.unbounded_send(vec![next_message]) {
log::warn!(
"Failed to send {} packets (possible process shutdown?)",
err.into_inner().len()
);
if let Err(err) = self.mix_tx.send(vec![next_message]).await {
log::error!("Failed to send - channel closed: {}", err);
}
// JS: Not entirely sure why or how it fixes stuff, but without the yield call,
@@ -234,7 +347,44 @@ where
tokio::task::yield_now().await;
}
fn current_average_message_sending_delay(&self) -> Duration {
self.config.average_message_sending_delay
* self.sending_rate_controller.current_multiplier()
}
fn adjust_current_average_message_sending_delay(&mut self) {
let used_slots = self.mix_tx.max_capacity() - self.mix_tx.capacity();
log::trace!(
"used_slots: {used_slots}, current_multiplier: {}",
self.sending_rate_controller.current_multiplier()
);
// Even just a single used slot is enough to signal backpressure
if used_slots > 0 {
log::trace!("Backpressure detected");
self.sending_rate_controller.record_backpressure_detected();
}
// If the buffer is running out, slow down the sending rate
if self.mix_tx.capacity() == 0
&& self.sending_rate_controller.not_increased_delay_recently()
{
self.sending_rate_controller.increase_delay_multiplier();
}
// Very carefully step up the sending rate in case it seems like we can solidly handle the
// current rate.
if self.sending_rate_controller.is_sending_reliable() {
self.sending_rate_controller.decrease_delay_multiplier();
}
}
fn poll_poisson(&mut self, cx: &mut Context<'_>) -> Poll<Option<StreamMessage>> {
// The average delay could change depending on if backpressure in the downstream channel
// (mix_tx) was detected.
self.adjust_current_average_message_sending_delay();
let avg_delay = self.current_average_message_sending_delay();
if let Some(ref mut next_delay) = &mut self.next_delay {
// it is not yet time to return a message
if next_delay.as_mut().poll(cx).is_pending() {
@@ -243,7 +393,6 @@ where
// we know it's time to send a message, so let's prepare delay for the next one
// Get the `now` by looking at the current `delay` deadline
let avg_delay = self.config.average_message_sending_delay;
let next_poisson_delay = sample_poisson_duration(&mut self.rng, avg_delay);
// The next interval value is `next_poisson_delay` after the one that just
+24 -5
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use config::NymConfig;
use nymsphinx::params::PacketSize;
use serde::{Deserialize, Serialize};
use std::marker::PhantomData;
use std::path::PathBuf;
@@ -247,8 +248,8 @@ impl<T: NymConfig> Config<T> {
self.debug.disable_main_poisson_packet_distribution
}
pub fn get_use_extended_packet_size(&self) -> bool {
self.debug.use_extended_packet_size
pub fn get_use_extended_packet_size(&self) -> Option<ExtendedPacketSize> {
self.debug.use_extended_packet_size.clone()
}
pub fn get_version(&self) -> &str {
@@ -470,8 +471,16 @@ pub struct Debug {
/// poisson distribution.
pub disable_main_poisson_packet_distribution: bool,
/// Controls whether the sent sphinx packet use the NON-DEFAULT bigger size.
pub use_extended_packet_size: bool,
/// Controls whether the sent sphinx packet use a NON-DEFAULT bigger size.
pub use_extended_packet_size: Option<ExtendedPacketSize>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ExtendedPacketSize {
Extended8,
Extended16,
Extended32,
}
impl Default for Debug {
@@ -488,7 +497,17 @@ impl Default for Debug {
topology_resolution_timeout: DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT,
disable_loop_cover_traffic_stream: false,
disable_main_poisson_packet_distribution: false,
use_extended_packet_size: false,
use_extended_packet_size: None,
}
}
}
impl From<ExtendedPacketSize> for PacketSize {
fn from(size: ExtendedPacketSize) -> PacketSize {
match size {
ExtendedPacketSize::Extended8 => PacketSize::ExtendedPacket8,
ExtendedPacketSize::Extended16 => PacketSize::ExtendedPacket16,
ExtendedPacketSize::Extended32 => PacketSize::ExtendedPacket32,
}
}
}
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.0.2"
version = "1.1.0"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
@@ -38,6 +38,7 @@ completions = { path = "../../common/completions" }
credential-storage = { path = "../../common/credential-storage" }
credentials = { path = "../../common/credentials", optional = true }
crypto = { path = "../../common/crypto" }
logging = { path = "../../common/logging"}
gateway-client = { path = "../../common/client-libs/gateway-client" }
gateway-requests = { path = "../../gateway/gateway-requests" }
network-defaults = { path = "../../common/network-defaults" }
+18 -22
View File
@@ -6,9 +6,7 @@ use client_core::client::inbound_messages::{
InputMessage, InputMessageReceiver, InputMessageSender,
};
use client_core::client::key_manager::KeyManager;
use client_core::client::mix_traffic::{
BatchMixMessageReceiver, BatchMixMessageSender, MixTrafficController,
};
use client_core::client::mix_traffic::{BatchMixMessageSender, MixTrafficController};
use client_core::client::real_messages_control;
use client_core::client::real_messages_control::RealMessagesController;
use client_core::client::received_buffer::{
@@ -31,7 +29,6 @@ use log::*;
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::addressing::nodes::NodeIdentity;
use nymsphinx::anonymous_replies::ReplySurb;
use nymsphinx::params::PacketSize;
use nymsphinx::receiver::ReconstructedMessage;
use task::{wait_for_signal, ShutdownListener, ShutdownNotifier};
@@ -103,8 +100,9 @@ impl NymClient {
topology_accessor,
);
if self.config.get_base().get_use_extended_packet_size() {
stream.set_custom_packet_size(PacketSize::ExtendedPacket)
if let Some(size) = self.config.get_base().get_use_extended_packet_size() {
log::debug!("Setting extended packet size: {:?}", size);
stream.set_custom_packet_size(size.into());
}
stream.start_with_shutdown(shutdown);
@@ -132,8 +130,9 @@ impl NymClient {
self.as_mix_recipient(),
);
if self.config.get_base().get_use_extended_packet_size() {
controller_config.set_custom_packet_size(PacketSize::ExtendedPacket)
if let Some(size) = self.config.get_base().get_use_extended_packet_size() {
log::debug!("Setting extended packet size: {:?}", size);
controller_config.set_custom_packet_size(size.into());
}
info!("Starting real traffic stream...");
@@ -263,13 +262,13 @@ impl NymClient {
// over it. Perhaps GatewayClient needs to be thread-shareable or have some channel for
// requests?
fn start_mix_traffic_controller(
&mut self,
mix_rx: BatchMixMessageReceiver,
gateway_client: GatewayClient,
shutdown: ShutdownListener,
) {
) -> BatchMixMessageSender {
info!("Starting mix traffic controller...");
MixTrafficController::new(mix_rx, gateway_client).start_with_shutdown(shutdown);
let (mix_traffic_controller, mix_tx) = MixTrafficController::new(gateway_client);
mix_traffic_controller.start_with_shutdown(shutdown);
mix_tx
}
fn start_websocket_listener(
@@ -356,11 +355,6 @@ impl NymClient {
// rather than creating them here, so say for example the buffer controller would create the request channels
// and would allow anyone to clone the sender channel
// sphinx_message_sender is the transmitter for any component generating sphinx packets that are to be sent to the mixnet
// they are used by cover traffic stream and real traffic stream
// sphinx_message_receiver is the receiver used by MixTrafficController that sends the actual traffic
let (sphinx_message_sender, sphinx_message_receiver) = mpsc::unbounded();
// unwrapped_sphinx_sender is the transmitter of mixnet messages received from the gateway
// unwrapped_sphinx_receiver is the receiver for said messages - used by ReceivedMessagesBuffer
let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded();
@@ -397,11 +391,13 @@ impl NymClient {
.start_gateway_client(mixnet_messages_sender, ack_sender, shutdown.subscribe())
.await;
self.start_mix_traffic_controller(
sphinx_message_receiver,
gateway_client,
shutdown.subscribe(),
);
// The sphinx_message_sender is the transmitter for any component generating sphinx packets
// that are to be sent to the mixnet. They are used by cover traffic stream and real
// traffic stream.
// The MixTrafficController then sends the actual traffic
let sphinx_message_sender =
Self::start_mix_traffic_controller(gateway_client, shutdown.subscribe());
self.start_real_traffic_controller(
shared_topology_accessor.clone(),
reply_key_storage,
+1 -22
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use clap::{crate_version, Parser};
use logging::setup_logging;
use network_defaults::setup_env;
pub mod client;
@@ -34,25 +35,3 @@ fn banner() -> String {
crate_version!()
)
}
fn setup_logging() {
let mut log_builder = pretty_env_logger::formatted_timed_builder();
if let Ok(s) = ::std::env::var("RUST_LOG") {
log_builder.parse_filters(&s);
} else {
// default to 'Info'
log_builder.filter(None, log::LevelFilter::Info);
}
log_builder
.filter_module("hyper", log::LevelFilter::Warn)
.filter_module("tokio_reactor", log::LevelFilter::Warn)
.filter_module("reqwest", log::LevelFilter::Warn)
.filter_module("mio", log::LevelFilter::Warn)
.filter_module("want", log::LevelFilter::Warn)
.filter_module("tungstenite", log::LevelFilter::Warn)
.filter_module("tokio_tungstenite", log::LevelFilter::Warn)
.filter_module("handlebars", log::LevelFilter::Warn)
.filter_module("sled", log::LevelFilter::Warn)
.init();
}
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.0.2"
version = "1.1.0"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
@@ -31,6 +31,7 @@ completions = { path = "../../common/completions" }
credential-storage = { path = "../../common/credential-storage" }
credentials = { path = "../../common/credentials", optional = true }
crypto = { path = "../../common/crypto" }
logging = { path = "../../common/logging"}
gateway-client = { path = "../../common/client-libs/gateway-client" }
gateway-requests = { path = "../../gateway/gateway-requests" }
network-defaults = { path = "../../common/network-defaults" }
+18 -22
View File
@@ -13,9 +13,7 @@ use client_core::client::inbound_messages::{
InputMessage, InputMessageReceiver, InputMessageSender,
};
use client_core::client::key_manager::KeyManager;
use client_core::client::mix_traffic::{
BatchMixMessageReceiver, BatchMixMessageSender, MixTrafficController,
};
use client_core::client::mix_traffic::{BatchMixMessageSender, MixTrafficController};
use client_core::client::real_messages_control::RealMessagesController;
use client_core::client::received_buffer::{
ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, ReceivedMessagesBufferController,
@@ -36,7 +34,6 @@ use gateway_client::{
use log::*;
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::addressing::nodes::NodeIdentity;
use nymsphinx::params::PacketSize;
use task::{wait_for_signal, ShutdownListener, ShutdownNotifier};
pub mod config;
@@ -103,8 +100,9 @@ impl NymClient {
topology_accessor,
);
if self.config.get_base().get_use_extended_packet_size() {
stream.set_custom_packet_size(PacketSize::ExtendedPacket)
if let Some(size) = self.config.get_base().get_use_extended_packet_size() {
log::debug!("Setting extended packet size: {:?}", size);
stream.set_custom_packet_size(size.into());
}
stream.start_with_shutdown(shutdown);
@@ -132,8 +130,9 @@ impl NymClient {
self.as_mix_recipient(),
);
if self.config.get_base().get_use_extended_packet_size() {
controller_config.set_custom_packet_size(PacketSize::ExtendedPacket)
if let Some(size) = self.config.get_base().get_use_extended_packet_size() {
log::debug!("Setting extended packet size: {:?}", size);
controller_config.set_custom_packet_size(size.into());
}
info!("Starting real traffic stream...");
@@ -263,13 +262,13 @@ impl NymClient {
// over it. Perhaps GatewayClient needs to be thread-shareable or have some channel for
// requests?
fn start_mix_traffic_controller(
&mut self,
mix_rx: BatchMixMessageReceiver,
gateway_client: GatewayClient,
shutdown: ShutdownListener,
) {
) -> BatchMixMessageSender {
info!("Starting mix traffic controller...");
MixTrafficController::new(mix_rx, gateway_client).start_with_shutdown(shutdown);
let (mix_traffic_controller, mix_tx) = MixTrafficController::new(gateway_client);
mix_traffic_controller.start_with_shutdown(shutdown);
mix_tx
}
fn start_socks5_listener(
@@ -345,11 +344,6 @@ impl NymClient {
// rather than creating them here, so say for example the buffer controller would create the request channels
// and would allow anyone to clone the sender channel
// sphinx_message_sender is the transmitter for any component generating sphinx packets that are to be sent to the mixnet
// they are used by cover traffic stream and real traffic stream
// sphinx_message_receiver is the receiver used by MixTrafficController that sends the actual traffic
let (sphinx_message_sender, sphinx_message_receiver) = mpsc::unbounded();
// unwrapped_sphinx_sender is the transmitter of mixnet messages received from the gateway
// unwrapped_sphinx_receiver is the receiver for said messages - used by ReceivedMessagesBuffer
let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded();
@@ -386,11 +380,13 @@ impl NymClient {
.start_gateway_client(mixnet_messages_sender, ack_sender, shutdown.subscribe())
.await;
self.start_mix_traffic_controller(
sphinx_message_receiver,
gateway_client,
shutdown.subscribe(),
);
// The sphinx_message_sender is the transmitter for any component generating sphinx packets
// that are to be sent to the mixnet. They are used by cover traffic stream and real
// traffic stream.
// The MixTrafficController then sends the actual traffic
let sphinx_message_sender =
Self::start_mix_traffic_controller(gateway_client, shutdown.subscribe());
self.start_real_traffic_controller(
shared_topology_accessor.clone(),
reply_key_storage,
+1 -20
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use clap::{crate_version, Parser};
use logging::setup_logging;
use network_defaults::setup_env;
pub mod client;
@@ -34,23 +35,3 @@ fn banner() -> String {
crate_version!()
)
}
fn setup_logging() {
let mut log_builder = pretty_env_logger::formatted_timed_builder();
if let Ok(s) = ::std::env::var("RUST_LOG") {
log_builder.parse_filters(&s);
} else {
// default to 'Info'
log_builder.filter(None, log::LevelFilter::Info);
}
log_builder
.filter_module("hyper", log::LevelFilter::Warn)
.filter_module("tokio_reactor", log::LevelFilter::Warn)
.filter_module("reqwest", log::LevelFilter::Warn)
.filter_module("mio", log::LevelFilter::Warn)
.filter_module("want", log::LevelFilter::Warn)
.filter_module("tungstenite", log::LevelFilter::Warn)
.filter_module("tokio_tungstenite", log::LevelFilter::Warn)
.init();
}
+8 -3
View File
@@ -4,7 +4,7 @@
// due to expansion of #[wasm_bindgen] macro on `Debug` Config struct
#![allow(clippy::drop_non_drop)]
use client_core::config::{Debug as ConfigDebug, GatewayEndpoint};
use client_core::config::{Debug as ConfigDebug, ExtendedPacketSize, GatewayEndpoint};
use std::time::Duration;
use url::Url;
use wasm_bindgen::prelude::*;
@@ -107,6 +107,11 @@ pub struct Debug {
impl From<Debug> for ConfigDebug {
fn from(debug: Debug) -> Self {
// For now we just always use the (older) 32kb extended size
let use_extended_packet_size = debug
.use_extended_packet_size
.then(|| ExtendedPacketSize::Extended32);
ConfigDebug {
average_packet_delay: Duration::from_millis(debug.average_packet_delay_ms),
average_ack_delay: Duration::from_millis(debug.average_ack_delay_ms),
@@ -126,7 +131,7 @@ impl From<Debug> for ConfigDebug {
disable_loop_cover_traffic_stream: debug.disable_loop_cover_traffic_stream,
disable_main_poisson_packet_distribution: debug
.disable_main_poisson_packet_distribution,
use_extended_packet_size: debug.use_extended_packet_size,
use_extended_packet_size,
}
}
}
@@ -148,7 +153,7 @@ impl From<ConfigDebug> for Debug {
disable_loop_cover_traffic_stream: debug.disable_loop_cover_traffic_stream,
disable_main_poisson_packet_distribution: debug
.disable_main_poisson_packet_distribution,
use_extended_packet_size: debug.use_extended_packet_size,
use_extended_packet_size: debug.use_extended_packet_size.is_some(),
}
}
}
+15 -18
View File
@@ -6,7 +6,7 @@ use client_core::client::{
cover_traffic_stream::LoopCoverTrafficStream,
inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender},
key_manager::KeyManager,
mix_traffic::{BatchMixMessageReceiver, BatchMixMessageSender, MixTrafficController},
mix_traffic::{BatchMixMessageSender, MixTrafficController},
real_messages_control::{self, RealMessagesController},
received_buffer::{
ReceivedBufferMessage, ReceivedBufferRequestReceiver, ReceivedBufferRequestSender,
@@ -22,7 +22,6 @@ use gateway_client::{
MixnetMessageSender,
};
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::params::PacketSize;
use rand::rngs::OsRng;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
@@ -110,8 +109,8 @@ impl NymClient {
topology_accessor,
);
if self.config.debug.use_extended_packet_size {
stream.set_custom_packet_size(PacketSize::ExtendedPacket)
if let Some(size) = &self.config.debug.use_extended_packet_size {
stream.set_custom_packet_size(size.clone().into());
}
stream.start();
@@ -135,8 +134,8 @@ impl NymClient {
self.as_mix_recipient(),
);
if self.config.debug.use_extended_packet_size {
controller_config.set_custom_packet_size(PacketSize::ExtendedPacket)
if let Some(size) = &self.config.debug.use_extended_packet_size {
controller_config.set_custom_packet_size(size.clone().into());
}
console_log!("Starting real traffic stream...");
@@ -253,13 +252,11 @@ impl NymClient {
// TODO: if we want to send control messages to gateway_client, this CAN'T take the ownership
// over it. Perhaps GatewayClient needs to be thread-shareable or have some channel for
// requests?
fn start_mix_traffic_controller(
&mut self,
mix_rx: BatchMixMessageReceiver,
gateway_client: GatewayClient,
) {
fn start_mix_traffic_controller(gateway_client: GatewayClient) -> BatchMixMessageSender {
console_log!("Starting mix traffic controller...");
MixTrafficController::new(mix_rx, gateway_client).start();
let (mix_traffic_controller, mix_tx) = MixTrafficController::new(gateway_client);
mix_traffic_controller.start();
mix_tx
}
// TODO: this procedure is extremely overcomplicated, because it's based off native client's behaviour
@@ -307,11 +304,6 @@ impl NymClient {
// rather than creating them here, so say for example the buffer controller would create the request channels
// and would allow anyone to clone the sender channel
// sphinx_message_sender is the transmitter for any component generating sphinx packets that are to be sent to the mixnet
// they are used by cover traffic stream and real traffic stream
// sphinx_message_receiver is the receiver used by MixTrafficController that sends the actual traffic
let (sphinx_message_sender, sphinx_message_receiver) = mpsc::unbounded();
// unwrapped_sphinx_sender is the transmitter of mixnet messages received from the gateway
// unwrapped_sphinx_receiver is the receiver for said messages - used by ReceivedMessagesBuffer
let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded();
@@ -339,7 +331,12 @@ impl NymClient {
.start_gateway_client(mixnet_messages_sender, ack_sender)
.await;
self.start_mix_traffic_controller(sphinx_message_receiver, gateway_client);
// The sphinx_message_sender is the transmitter for any component generating sphinx packets
// that are to be sent to the mixnet. They are used by cover traffic stream and real
// traffic stream.
// The MixTrafficController then sends the actual traffic
let sphinx_message_sender = Self::start_mix_traffic_controller(gateway_client);
self.start_real_traffic_controller(
shared_topology_accessor.clone(),
ack_receiver,
@@ -62,9 +62,19 @@ impl PacketRouter {
trace!("routing regular packet");
received_messages.push(received_packet);
} else if received_packet.len()
== PacketSize::ExtendedPacket.plaintext_size() - ack_overhead
== PacketSize::ExtendedPacket8.plaintext_size() - ack_overhead
{
trace!("routing extended packet");
trace!("routing extended8 packet");
received_messages.push(received_packet);
} else if received_packet.len()
== PacketSize::ExtendedPacket16.plaintext_size() - ack_overhead
{
trace!("routing extended16 packet");
received_messages.push(received_packet);
} else if received_packet.len()
== PacketSize::ExtendedPacket32.plaintext_size() - ack_overhead
{
trace!("routing extended32 packet");
received_messages.push(received_packet);
} else {
// this can happen if other clients are not padding their messages
@@ -23,6 +23,7 @@ pub struct Config {
maximum_reconnection_backoff: Duration,
initial_connection_timeout: Duration,
maximum_connection_buffer_size: usize,
use_legacy_version: bool,
}
impl Config {
@@ -31,12 +32,14 @@ impl Config {
maximum_reconnection_backoff: Duration,
initial_connection_timeout: Duration,
maximum_connection_buffer_size: usize,
use_legacy_version: bool,
) -> Self {
Config {
initial_reconnection_backoff,
maximum_reconnection_backoff,
initial_connection_timeout,
maximum_connection_buffer_size,
use_legacy_version,
}
}
}
@@ -201,7 +204,8 @@ impl SendWithoutResponse for Client {
packet_mode: PacketMode,
) -> io::Result<()> {
trace!("Sending packet to {:?}", address);
let framed_packet = FramedSphinxPacket::new(packet, packet_mode);
let framed_packet =
FramedSphinxPacket::new(packet, packet_mode, self.config.use_legacy_version);
if let Some(sender) = self.conn_new.get_mut(&address) {
if let Err(err) = sender.channel.try_send(framed_packet) {
@@ -259,6 +263,7 @@ mod tests {
maximum_reconnection_backoff: Duration::from_millis(300_000),
initial_connection_timeout: Duration::from_millis(1_500),
maximum_connection_buffer_size: 128,
use_legacy_version: false,
})
}
@@ -24,12 +24,14 @@ impl PacketForwarder {
maximum_reconnection_backoff: Duration,
initial_connection_timeout: Duration,
maximum_connection_buffer_size: usize,
use_legacy_version: bool,
) -> (PacketForwarder, MixForwardingSender) {
let client_config = Config::new(
initial_reconnection_backoff,
maximum_reconnection_backoff,
initial_connection_timeout,
maximum_connection_buffer_size,
use_legacy_version,
);
let (packet_sender, packet_receiver) = mpsc::unbounded();
@@ -13,6 +13,7 @@ colored = "2.0"
cw3 = "0.13.1"
mixnet-contract-common = { path= "../../cosmwasm-smart-contracts/mixnet-contract" }
vesting-contract-common = { path= "../../cosmwasm-smart-contracts/vesting-contract" }
contracts-common = { path = "../../cosmwasm-smart-contracts/contracts-common" }
coconut-bandwidth-contract-common = { path= "../../cosmwasm-smart-contracts/coconut-bandwidth-contract" }
multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" }
vesting-contract = { path = "../../../contracts/vesting" }
@@ -6,8 +6,10 @@ pub use crate::nymd::cosmwasm_client::client::CosmWasmClient;
use crate::nymd::error::NymdError;
use crate::nymd::NymdClient;
use async_trait::async_trait;
use contracts_common::ContractBuildInformation;
use cosmwasm_std::{Coin as CosmWasmCoin, Timestamp};
use mixnet_contract_common::MixId;
use serde::Deserialize;
use vesting_contract::vesting::Account;
use vesting_contract_common::{
messages::QueryMsg as VestingQueryMsg, AllDelegationsResponse, DelegationTimesResponse,
@@ -16,6 +18,15 @@ use vesting_contract_common::{
#[async_trait]
pub trait VestingQueryClient {
async fn query_vesting_contract<T>(&self, query: VestingQueryMsg) -> Result<T, NymdError>
where
for<'a> T: Deserialize<'a>;
async fn get_vesting_contract_version(&self) -> Result<ContractBuildInformation, NymdError> {
self.query_vesting_contract(VestingQueryMsg::GetContractVersion {})
.await
}
async fn locked_coins(
&self,
address: &str,
@@ -107,6 +118,15 @@ pub trait VestingQueryClient {
#[async_trait]
impl<C: CosmWasmClient + Sync + Send> VestingQueryClient for NymdClient<C> {
async fn query_vesting_contract<T>(&self, query: VestingQueryMsg) -> Result<T, NymdError>
where
for<'a> T: Deserialize<'a>,
{
self.client
.query_contract_smart(self.vesting_contract_address(), &query)
.await
}
async fn locked_coins(
&self,
vesting_account_address: &str,
@@ -1,4 +1,5 @@
use thiserror::Error;
use validator_api_requests::models::RequestError;
#[derive(Error, Debug)]
pub enum ValidatorAPIError {
@@ -10,4 +11,7 @@ pub enum ValidatorAPIError {
#[error("Request failed with error message - {0}")]
GenericRequestFailure(String),
#[error("The validator API has failed to resolve our request. It returned status code {status} and additional error message: {}", error.message())]
ApiRequestFailure { status: u16, error: RequestError },
}
@@ -5,6 +5,7 @@ use crate::validator_api::error::ValidatorAPIError;
use crate::validator_api::routes::{CORE_STATUS_COUNT, SINCE_ARG};
use mixnet_contract_common::mixnode::MixNodeDetails;
use mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId};
use reqwest::Response;
use serde::{Deserialize, Serialize};
use url::Url;
use validator_api_requests::coconut::{
@@ -12,9 +13,10 @@ use validator_api_requests::coconut::{
VerifyCredentialBody, VerifyCredentialResponse,
};
use validator_api_requests::models::{
GatewayCoreStatusResponse, InclusionProbabilityResponse, MixNodeBondAnnotated,
MixnodeCoreStatusResponse, MixnodeStatusResponse, RewardEstimationResponse,
StakeSaturationResponse, UptimeResponse,
GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse,
InclusionProbabilityResponse, MixNodeBondAnnotated, MixnodeCoreStatusResponse,
MixnodeStatusReportResponse, MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RequestError,
RewardEstimationResponse, StakeSaturationResponse, UptimeResponse,
};
pub mod error;
@@ -47,6 +49,19 @@ impl Client {
&self.url
}
async fn send_get_request<K, V>(
&self,
path: PathSegments<'_>,
params: Params<'_, K, V>,
) -> Result<Response, ValidatorAPIError>
where
K: AsRef<str>,
V: AsRef<str>,
{
let url = create_api_url(&self.url, path, params);
Ok(self.reqwest_client.get(url).send().await?)
}
async fn query_validator_api<T, K, V>(
&self,
path: PathSegments<'_>,
@@ -57,8 +72,36 @@ impl Client {
K: AsRef<str>,
V: AsRef<str>,
{
let url = create_api_url(&self.url, path, params);
Ok(self.reqwest_client.get(url).send().await?.json().await?)
let res = self.send_get_request(path, params).await?;
if res.status().is_success() {
Ok(res.json().await?)
} else {
Err(ValidatorAPIError::GenericRequestFailure(res.text().await?))
}
}
// This works for endpoints returning Result<Json<T>, ErrorResponse>
async fn query_validator_api_fallible<T, K, V>(
&self,
path: PathSegments<'_>,
params: Params<'_, K, V>,
) -> Result<T, ValidatorAPIError>
where
for<'a> T: Deserialize<'a>,
K: AsRef<str>,
V: AsRef<str>,
{
let res = self.send_get_request(path, params).await?;
let status = res.status();
if res.status().is_success() {
Ok(res.json().await?)
} else {
let request_error: RequestError = res.json().await?;
Err(ValidatorAPIError::ApiRequestFailure {
status: status.as_u16(),
error: request_error,
})
}
}
async fn post_validator_api<B, T, K, V>(
@@ -135,6 +178,74 @@ impl Client {
.await
}
pub async fn get_mixnode_report(
&self,
mix_id: MixId,
) -> Result<MixnodeStatusReportResponse, ValidatorAPIError> {
self.query_validator_api(
&[
routes::API_VERSION,
routes::STATUS,
routes::MIXNODE,
&mix_id.to_string(),
routes::REPORT,
],
NO_PARAMS,
)
.await
}
pub async fn get_gateway_report(
&self,
identity: IdentityKeyRef<'_>,
) -> Result<GatewayStatusReportResponse, ValidatorAPIError> {
self.query_validator_api(
&[
routes::API_VERSION,
routes::STATUS,
routes::GATEWAY,
identity,
routes::REPORT,
],
NO_PARAMS,
)
.await
}
pub async fn get_mixnode_history(
&self,
mix_id: MixId,
) -> Result<MixnodeUptimeHistoryResponse, ValidatorAPIError> {
self.query_validator_api(
&[
routes::API_VERSION,
routes::STATUS,
routes::MIXNODE,
&mix_id.to_string(),
routes::HISTORY,
],
NO_PARAMS,
)
.await
}
pub async fn get_gateway_history(
&self,
identity: IdentityKeyRef<'_>,
) -> Result<GatewayUptimeHistoryResponse, ValidatorAPIError> {
self.query_validator_api(
&[
routes::API_VERSION,
routes::STATUS,
routes::GATEWAY,
identity,
routes::HISTORY,
],
NO_PARAMS,
)
.await
}
pub async fn get_rewarded_mixnodes_detailed(
&self,
) -> Result<Vec<MixNodeBondAnnotated>, ValidatorAPIError> {
@@ -234,7 +345,7 @@ impl Client {
&self,
mix_id: MixId,
) -> Result<RewardEstimationResponse, ValidatorAPIError> {
self.query_validator_api(
self.query_validator_api_fallible(
&[
routes::API_VERSION,
routes::STATUS_ROUTES,
@@ -251,7 +362,7 @@ impl Client {
&self,
mix_id: MixId,
) -> Result<StakeSaturationResponse, ValidatorAPIError> {
self.query_validator_api(
self.query_validator_api_fallible(
&[
routes::API_VERSION,
routes::STATUS_ROUTES,
@@ -268,7 +379,7 @@ impl Client {
&self,
mix_id: MixId,
) -> Result<InclusionProbabilityResponse, ValidatorAPIError> {
self.query_validator_api(
self.query_validator_api_fallible(
&[
routes::API_VERSION,
routes::STATUS_ROUTES,
@@ -285,7 +396,7 @@ impl Client {
&self,
mix_id: MixId,
) -> Result<UptimeResponse, ValidatorAPIError> {
self.query_validator_api(
self.query_validator_api_fallible(
&[
routes::API_VERSION,
routes::STATUS_ROUTES,
@@ -10,7 +10,6 @@ pub const GATEWAYS: &str = "gateways";
pub const DETAILED: &str = "detailed";
pub const ACTIVE: &str = "active";
pub const REWARDED: &str = "rewarded";
pub const COCONUT_ROUTES: &str = "coconut";
pub const BANDWIDTH: &str = "bandwidth";
@@ -28,6 +27,8 @@ pub const CORE_STATUS_COUNT: &str = "core-status-count";
pub const SINCE_ARG: &str = "since";
pub const STATUS: &str = "status";
pub const REPORT: &str = "report";
pub const HISTORY: &str = "history";
pub const REWARD_ESTIMATION: &str = "reward-estimation";
pub const AVG_UPTIME: &str = "avg_uptime";
pub const STAKE_SATURATION: &str = "stake-saturation";
@@ -11,3 +11,6 @@ cosmwasm-std = "1.0.0"
serde = { version = "1.0", features = ["derive"] }
schemars = "0.8"
thiserror = "1"
[dev-dependencies]
serde_json = "1.0.0"
@@ -47,6 +47,14 @@ impl Percent {
self.0 == Decimal::zero()
}
pub fn zero() -> Self {
Self(Decimal::zero())
}
pub fn hundred() -> Self {
Self(Decimal::one())
}
pub fn from_percentage_value(value: u64) -> Result<Self, ContractsCommonError> {
Percent::new(Decimal::percent(value))
}
@@ -144,3 +152,47 @@ pub struct ContractBuildInformation {
/// Provides the rustc version that was used for the build, for example `1.52.0-nightly`.
pub rustc_version: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn percent_serde() {
let valid_value = Percent::from_percentage_value(80).unwrap();
let serialized = serde_json::to_string(&valid_value).unwrap();
let deserialized: Percent = serde_json::from_str(&serialized).unwrap();
assert_eq!(valid_value, deserialized);
let invalid_values = vec!["\"42\"", "\"1.1\"", "\"1.00000001\"", "\"foomp\"", "\"1a\""];
for invalid_value in invalid_values {
assert!(serde_json::from_str::<'_, Percent>(invalid_value).is_err())
}
assert_eq!(
serde_json::from_str::<'_, Percent>("\"0.95\"").unwrap(),
Percent::from_percentage_value(95).unwrap()
)
}
#[test]
fn percent_to_absolute_integer() {
let p = serde_json::from_str::<'_, Percent>("\"0.0001\"").unwrap();
assert_eq!(p.round_to_integer(), 0);
let p = serde_json::from_str::<'_, Percent>("\"0.0099\"").unwrap();
assert_eq!(p.round_to_integer(), 0);
let p = serde_json::from_str::<'_, Percent>("\"0.0199\"").unwrap();
assert_eq!(p.round_to_integer(), 1);
let p = serde_json::from_str::<'_, Percent>("\"0.45123\"").unwrap();
assert_eq!(p.round_to_integer(), 45);
let p = serde_json::from_str::<'_, Percent>("\"0.999999999\"").unwrap();
assert_eq!(p.round_to_integer(), 99);
let p = serde_json::from_str::<'_, Percent>("\"1.00\"").unwrap();
assert_eq!(p.round_to_integer(), 100);
}
}
@@ -73,6 +73,9 @@ pub struct Interval {
// TODO add a better TS type generation
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
#[serde(with = "string_rfc3339_offset_date_time")]
// note: the `ts-rs failed to parse this attribute. It will be ignored.` warning emitted during
// compilation is fine (I guess). `ts-rs` can't handle `with` serde attribute, but that's okay
// since we explicitly specified this field should correspond to typescript's string
current_epoch_start: OffsetDateTime,
current_epoch_id: EpochId,
#[cfg_attr(feature = "generate-ts", ts(type = "{ secs: number; nanos: number; }"))]
@@ -31,6 +31,7 @@ pub struct InitialRewardingParams {
pub initial_reward_pool: Decimal,
pub initial_staking_supply: Decimal,
pub staking_supply_scale_factor: Percent,
pub sybil_resistance: Percent,
pub active_set_work_factor: Decimal,
pub interval_pool_emission: Percent,
@@ -51,6 +52,7 @@ impl InitialRewardingParams {
interval: IntervalRewardParams {
reward_pool: self.initial_reward_pool,
staking_supply: self.initial_staking_supply,
staking_supply_scale_factor: self.staking_supply_scale_factor,
epoch_reward_budget,
stake_saturation_point,
sybil_resistance: self.sybil_resistance,
@@ -26,6 +26,11 @@ pub struct IntervalRewardParams {
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
pub staking_supply: Decimal,
/// Defines the percentage of stake needed to reach saturation for all of the nodes in the rewarded set.
/// Also known as `beta`.
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
pub staking_supply_scale_factor: Percent,
// computed values
/// Current value of the computed reward budget per epoch, per node.
/// It is expected to be constant throughout the interval.
@@ -164,6 +169,10 @@ impl RewardingParams {
self.interval.staking_supply = staking_supply;
}
if let Some(staking_supply_scale_factor) = updates.staking_supply_scale_factor {
self.interval.staking_supply_scale_factor = staking_supply_scale_factor
}
if let Some(sybil_resistance_percent) = updates.sybil_resistance_percent {
self.interval.sybil_resistance = sybil_resistance_percent;
}
@@ -235,6 +244,9 @@ pub struct IntervalRewardingParamsUpdate {
#[cfg_attr(feature = "generate-ts", ts(type = "string | null"))]
pub staking_supply: Option<Decimal>,
#[cfg_attr(feature = "generate-ts", ts(type = "string | null"))]
pub staking_supply_scale_factor: Option<Percent>,
#[cfg_attr(feature = "generate-ts", ts(type = "string | null"))]
pub sybil_resistance_percent: Option<Percent>,
@@ -252,6 +264,7 @@ impl IntervalRewardingParamsUpdate {
// essentially at least a single field has to be a `Some`
self.reward_pool.is_some()
|| self.staking_supply.is_some()
|| self.staking_supply_scale_factor.is_some()
|| self.sybil_resistance_percent.is_some()
|| self.active_set_work_factor.is_some()
|| self.interval_pool_emission.is_some()
@@ -40,7 +40,12 @@ impl Simulator {
if self.interval.current_interval_id() + 1 == updated.current_interval_id() {
let old = self.system_rewarding_params.interval;
let reward_pool = old.reward_pool - self.pending_reward_pool_emission;
let staking_supply = old.staking_supply + self.pending_reward_pool_emission;
let staking_supply = old.staking_supply
+ self
.system_rewarding_params
.interval
.staking_supply_scale_factor
* self.pending_reward_pool_emission;
let epoch_reward_budget = reward_pool
/ Decimal::from_atomics(self.interval.epochs_in_interval(), 0).unwrap()
* old.interval_pool_emission.value();
@@ -51,6 +56,7 @@ impl Simulator {
interval: IntervalRewardParams {
reward_pool,
staking_supply,
staking_supply_scale_factor: old.staking_supply_scale_factor,
epoch_reward_budget,
stake_saturation_point,
sybil_resistance: old.sybil_resistance,
@@ -209,6 +215,7 @@ mod tests {
interval: IntervalRewardParams {
reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens)
staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M
staking_supply_scale_factor: Percent::hundred(),
epoch_reward_budget,
stake_saturation_point,
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
@@ -537,6 +544,7 @@ mod tests {
interval: IntervalRewardParams {
reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(),
staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(),
staking_supply_scale_factor: Percent::hundred(),
epoch_reward_budget,
stake_saturation_point,
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
@@ -122,48 +122,3 @@ pub struct PagedRewardedSetResponse {
pub nodes: Vec<(MixId, RewardedSetNodeStatus)>,
pub start_next_after: Option<MixId>,
}
#[cfg(test)]
mod tests {
use contracts_common::Percent;
#[test]
fn percent_serde() {
let valid_value = Percent::from_percentage_value(80).unwrap();
let serialized = serde_json::to_string(&valid_value).unwrap();
println!("{}", serialized);
let deserialized: Percent = serde_json::from_str(&serialized).unwrap();
assert_eq!(valid_value, deserialized);
let invalid_values = vec!["\"42\"", "\"1.1\"", "\"1.00000001\"", "\"foomp\"", "\"1a\""];
for invalid_value in invalid_values {
assert!(serde_json::from_str::<'_, Percent>(invalid_value).is_err())
}
assert_eq!(
serde_json::from_str::<'_, Percent>("\"0.95\"").unwrap(),
Percent::from_percentage_value(95).unwrap()
)
}
#[test]
fn percent_to_absolute_integer() {
let p = serde_json::from_str::<'_, Percent>("\"0.0001\"").unwrap();
assert_eq!(p.round_to_integer(), 0);
let p = serde_json::from_str::<'_, Percent>("\"0.0099\"").unwrap();
assert_eq!(p.round_to_integer(), 0);
let p = serde_json::from_str::<'_, Percent>("\"0.0199\"").unwrap();
assert_eq!(p.round_to_integer(), 1);
let p = serde_json::from_str::<'_, Percent>("\"0.45123\"").unwrap();
assert_eq!(p.round_to_integer(), 45);
let p = serde_json::from_str::<'_, Percent>("\"0.999999999\"").unwrap();
assert_eq!(p.round_to_integer(), 99);
let p = serde_json::from_str::<'_, Percent>("\"1.00\"").unwrap();
assert_eq!(p.round_to_integer(), 100);
}
}
@@ -9,7 +9,6 @@ mixnet-contract-common = { path = "../mixnet-contract" }
contracts-common = { path = "../contracts-common" }
serde = { version = "1.0", features = ["derive"] }
schemars = "0.8"
log = "0.4"
ts-rs = {version = "6.1.2", optional = true}
[features]
@@ -4,7 +4,6 @@ use std::str::FromStr;
// SPDX-License-Identifier: Apache-2.0
use contracts_common::Percent;
use cosmwasm_std::{Addr, Coin, Timestamp, Uint128};
use log::warn;
use mixnet_contract_common::MixId;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -59,30 +58,18 @@ impl FromStr for PledgeCap {
fn from_str(cap: &str) -> Result<Self, Self::Err> {
let cap = cap.replace('_', "").replace(',', ".");
match Percent::from_str(&cap) {
Ok(p) => {
if p.is_zero() {
warn!("Pledge cap set to 0%, are you sure this is right?")
}
Ok(PledgeCap::Percent(p))
}
Err(_) => {
match cap.parse::<u128>() {
Ok(i) => {
if i < 100_000_000_000 {
warn!("PledgeCap set to less then 100_000 NYM, are you sure this is right?");
}
Ok(PledgeCap::Absolute(Uint128::from(i)))
}
Err(_e) => Err(format!("Could not parse {} as Percent or Uint128", cap)),
}
}
Ok(p) => Ok(PledgeCap::Percent(p)),
Err(_) => match cap.parse::<u128>() {
Ok(i) => Ok(PledgeCap::Absolute(Uint128::from(i))),
Err(_e) => Err(format!("Could not parse {} as Percent or Uint128", cap)),
},
}
}
}
impl Default for PledgeCap {
fn default() -> Self {
PledgeCap::Absolute(Uint128::from(100_000_000_000u128))
PledgeCap::Percent(Percent::from_percentage_value(10).expect("This can never fail!"))
}
}
@@ -160,6 +160,7 @@ impl ExecuteMsg {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
GetContractVersion {},
LockedCoins {
vesting_account_address: String,
block_time: Option<Timestamp>,
+10
View File
@@ -0,0 +1,10 @@
[package]
name = "logging"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4.0"
pretty_env_logger = "0.4.0"
+25
View File
@@ -0,0 +1,25 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// I'd argue we should start transitioning from `log` to `tracing`
pub fn setup_logging() {
let mut log_builder = pretty_env_logger::formatted_timed_builder();
if let Ok(s) = ::std::env::var("RUST_LOG") {
log_builder.parse_filters(&s);
} else {
// default to 'Info'
log_builder.filter(None, log::LevelFilter::Info);
}
log_builder
.filter_module("hyper", log::LevelFilter::Warn)
.filter_module("tokio_reactor", log::LevelFilter::Warn)
.filter_module("reqwest", log::LevelFilter::Warn)
.filter_module("mio", log::LevelFilter::Warn)
.filter_module("want", log::LevelFilter::Warn)
.filter_module("tungstenite", log::LevelFilter::Warn)
.filter_module("tokio_tungstenite", log::LevelFilter::Warn)
.filter_module("handlebars", log::LevelFilter::Warn)
.filter_module("sled", log::LevelFilter::Warn)
.init();
}
@@ -116,7 +116,10 @@ impl SphinxPacketProcessor {
trace!("received an ack packet!");
Ok((None, data))
}
PacketSize::RegularPacket | PacketSize::ExtendedPacket => {
PacketSize::RegularPacket
| PacketSize::ExtendedPacket8
| PacketSize::ExtendedPacket16
| PacketSize::ExtendedPacket32 => {
trace!("received a normal packet!");
let (ack_data, message) = self.split_hop_data_into_ack_and_message(data)?;
let (ack_first_hop, ack_packet) = SurbAck::try_recover_first_hop_packet(&ack_data)?;
+1 -1
View File
@@ -10,7 +10,7 @@ pub const MIX_DENOM: DenomDetails = DenomDetails::new("unym", "nym", 6);
pub const STAKE_DENOM: DenomDetails = DenomDetails::new("unyx", "nyx", 6);
pub(crate) const MIXNET_CONTRACT_ADDRESS: &str =
"n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g";
"n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr";
pub(crate) const VESTING_CONTRACT_ADDRESS: &str =
"n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw";
pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str =
+4 -2
View File
@@ -25,8 +25,10 @@ impl Display for MixPacketFormattingError {
InvalidPacketSize(actual) =>
write!(
f,
"received request had invalid size. (actual: {}, but expected one of: {} (ACK), {} (REGULAR), {} (EXTENDED))",
actual, PacketSize::AckPacket.size(), PacketSize::RegularPacket.size(), PacketSize::ExtendedPacket.size()
"received request had invalid size. (actual: {}, but expected one of: {} (ACK), {} (REGULAR), {}, {}, {} (EXTENDED))",
actual, PacketSize::AckPacket.size(), PacketSize::RegularPacket.size(),
PacketSize::ExtendedPacket8.size(), PacketSize::ExtendedPacket16.size(),
PacketSize::ExtendedPacket32.size()
),
MalformedSphinxPacket => write!(f, "received sphinx packet was malformed"),
InvalidPacketMode => write!(f, "provided packet mode is invalid")
+114 -21
View File
@@ -6,7 +6,6 @@ use bytes::{Buf, BufMut, BytesMut};
use nymsphinx_params::packet_modes::InvalidPacketMode;
use nymsphinx_params::packet_sizes::{InvalidPacketSize, PacketSize};
use nymsphinx_types::SphinxPacket;
use std::convert::TryFrom;
use std::io;
use tokio_util::codec::{Decoder, Encoder};
@@ -75,7 +74,7 @@ impl Decoder for SphinxCodec {
if src.is_empty() {
// can't do anything if we have no bytes, but let's reserve enough for the most
// conservative case, i.e. receiving an ack packet
src.reserve(Header::SIZE + PacketSize::AckPacket.size());
src.reserve(Header::LEGACY_SIZE + PacketSize::AckPacket.size());
return Ok(None);
}
@@ -87,7 +86,7 @@ impl Decoder for SphinxCodec {
};
let sphinx_packet_size = header.packet_size.size();
let frame_len = Header::SIZE + sphinx_packet_size;
let frame_len = header.size() + sphinx_packet_size;
if src.len() < frame_len {
// we don't have enough bytes to read the rest of frame
@@ -96,7 +95,7 @@ impl Decoder for SphinxCodec {
}
// advance buffer past the header - at this point we have enough bytes
src.advance(Header::SIZE);
src.advance(header.size());
let sphinx_packet_bytes = src.split_to(sphinx_packet_size);
let sphinx_packet = match SphinxPacket::from_bytes(&sphinx_packet_bytes) {
Ok(sphinx_packet) => sphinx_packet,
@@ -115,21 +114,27 @@ impl Decoder for SphinxCodec {
// has appropriate capacity in anticipation of future calls to decode.
// Failing to do so leads to inefficiency.
// if we have at least one more byte available, we can reserve enough bytes for
// if we have enough bytes to decode the header of the next packet, we can reserve enough bytes for
// the entire next frame, if not, we assume the next frame is an ack packet and
// reserve for that.
// we also assume the next packet coming from the same client will use exactly the same versioning
// as the current packet
let mut allocate_for_next_packet = header.size() + PacketSize::AckPacket.size();
if !src.is_empty() {
let next_packet_len = match PacketSize::try_from(src[0]) {
Ok(next_packet_len) => next_packet_len,
match Header::decode(src) {
Ok(Some(next_header)) => {
allocate_for_next_packet = next_header.size() + next_header.packet_size.size();
}
Ok(None) => {
// we don't have enough information to know how much to reserve, fallback to the ack case
}
// the next frame will be malformed but let's leave handling the error to the next
// call to 'decode', as presumably, the current sphinx packet is still valid
Err(_) => return Ok(Some(nymsphinx_packet)),
};
let next_frame_len = next_packet_len.size() + Header::SIZE;
src.reserve(next_frame_len - 1);
} else {
src.reserve(Header::SIZE + PacketSize::AckPacket.size());
}
src.reserve(allocate_for_next_packet);
Ok(Some(nymsphinx_packet))
}
@@ -199,6 +204,8 @@ mod packet_encoding {
#[cfg(test)]
mod decode_will_allocate_enough_bytes_for_next_call {
use super::*;
use nymsphinx_params::packet_version::PacketVersion;
use nymsphinx_params::PacketMode;
#[test]
fn for_empty_bytes() {
@@ -207,20 +214,23 @@ mod packet_encoding {
assert!(SphinxCodec.decode(&mut empty_bytes).unwrap().is_none());
assert_eq!(
empty_bytes.capacity(),
Header::SIZE + PacketSize::AckPacket.size()
Header::LEGACY_SIZE + PacketSize::AckPacket.size()
);
}
#[test]
fn for_bytes_with_header() {
fn for_bytes_with_legacy_header() {
// if header gets decoded there should be enough bytes for the entire frame
let packet_sizes = vec![
PacketSize::AckPacket,
PacketSize::RegularPacket,
PacketSize::ExtendedPacket,
PacketSize::ExtendedPacket8,
PacketSize::ExtendedPacket16,
PacketSize::ExtendedPacket32,
];
for packet_size in packet_sizes {
let header = Header {
packet_version: PacketVersion::Legacy,
packet_size,
packet_mode: Default::default(),
};
@@ -228,12 +238,60 @@ mod packet_encoding {
header.encode(&mut bytes);
assert!(SphinxCodec.decode(&mut bytes).unwrap().is_none());
assert_eq!(bytes.capacity(), Header::SIZE + packet_size.size())
assert_eq!(bytes.capacity(), Header::LEGACY_SIZE + packet_size.size())
}
}
#[test]
fn for_full_frame() {
fn for_bytes_with_versioned_header() {
// if header gets decoded there should be enough bytes for the entire frame
let packet_sizes = vec![
PacketSize::AckPacket,
PacketSize::RegularPacket,
PacketSize::ExtendedPacket8,
PacketSize::ExtendedPacket16,
PacketSize::ExtendedPacket32,
];
for packet_size in packet_sizes {
let header = Header {
packet_version: PacketVersion::Versioned(123),
packet_size,
packet_mode: Default::default(),
};
let mut bytes = BytesMut::new();
header.encode(&mut bytes);
assert!(SphinxCodec.decode(&mut bytes).unwrap().is_none());
assert_eq!(
bytes.capacity(),
Header::VERSIONED_SIZE + packet_size.size()
)
}
}
#[test]
fn for_full_frame_with_legacy_header() {
// if full frame is used exactly, there should be enough space for header + ack packet
let packet = FramedSphinxPacket {
header: Header {
packet_version: PacketVersion::Legacy,
packet_size: Default::default(),
packet_mode: Default::default(),
},
packet: make_valid_sphinx_packet(Default::default()),
};
let mut bytes = BytesMut::new();
SphinxCodec.encode(packet, &mut bytes).unwrap();
assert!(SphinxCodec.decode(&mut bytes).unwrap().is_some());
assert_eq!(
bytes.capacity(),
Header::LEGACY_SIZE + PacketSize::AckPacket.size()
);
}
#[test]
fn for_full_frame_with_versioned_header() {
// if full frame is used exactly, there should be enough space for header + ack packet
let packet = FramedSphinxPacket {
header: Header::default(),
@@ -245,17 +303,50 @@ mod packet_encoding {
assert!(SphinxCodec.decode(&mut bytes).unwrap().is_some());
assert_eq!(
bytes.capacity(),
Header::SIZE + PacketSize::AckPacket.size()
Header::VERSIONED_SIZE + PacketSize::AckPacket.size()
);
}
#[test]
fn for_full_frame_with_extra_byte() {
// if there was at least 1 byte left, there should be enough space for entire next frame
fn for_full_frame_with_extra_bytes_with_legacy_header() {
// if there was at least 2 byte left, there should be enough space for entire next frame
let packet_sizes = vec![
PacketSize::AckPacket,
PacketSize::RegularPacket,
PacketSize::ExtendedPacket,
PacketSize::ExtendedPacket8,
PacketSize::ExtendedPacket16,
PacketSize::ExtendedPacket32,
];
for packet_size in packet_sizes {
let first_packet = FramedSphinxPacket {
header: Header {
packet_version: PacketVersion::Legacy,
packet_size: Default::default(),
packet_mode: Default::default(),
},
packet: make_valid_sphinx_packet(Default::default()),
};
let mut bytes = BytesMut::new();
SphinxCodec.encode(first_packet, &mut bytes).unwrap();
bytes.put_u8(packet_size as u8);
bytes.put_u8(PacketMode::default() as u8);
assert!(SphinxCodec.decode(&mut bytes).unwrap().is_some());
assert!(bytes.capacity() >= Header::LEGACY_SIZE + packet_size.size())
}
}
#[test]
fn for_full_frame_with_extra_bytes_with_versioned_header() {
// if there was at least 3 byte left, there should be enough space for entire next frame
let packet_sizes = vec![
PacketSize::AckPacket,
PacketSize::RegularPacket,
PacketSize::ExtendedPacket8,
PacketSize::ExtendedPacket16,
PacketSize::ExtendedPacket32,
];
for packet_size in packet_sizes {
@@ -266,10 +357,12 @@ mod packet_encoding {
let mut bytes = BytesMut::new();
SphinxCodec.encode(first_packet, &mut bytes).unwrap();
bytes.put_u8(PacketVersion::new_versioned(123).as_u8().unwrap());
bytes.put_u8(packet_size as u8);
bytes.put_u8(PacketMode::default() as u8);
assert!(SphinxCodec.decode(&mut bytes).unwrap().is_some());
assert!(bytes.capacity() >= Header::SIZE + packet_size.size())
assert!(bytes.capacity() >= Header::VERSIONED_SIZE + packet_size.size())
}
}
}
+81 -14
View File
@@ -4,6 +4,7 @@
use crate::codec::SphinxCodecError;
use bytes::{BufMut, BytesMut};
use nymsphinx_params::packet_sizes::PacketSize;
use nymsphinx_params::packet_version::PacketVersion;
use nymsphinx_params::PacketMode;
use nymsphinx_types::SphinxPacket;
use std::convert::TryFrom;
@@ -17,12 +18,14 @@ pub struct FramedSphinxPacket {
}
impl FramedSphinxPacket {
pub fn new(packet: SphinxPacket, packet_mode: PacketMode) -> Self {
pub fn new(packet: SphinxPacket, packet_mode: PacketMode, use_legacy_version: bool) -> Self {
// If this fails somebody is using the library in a super incorrect way, because they
// already managed to somehow create a sphinx packet
let packet_size = PacketSize::get_type(packet.len()).unwrap();
FramedSphinxPacket {
header: Header {
packet_version: PacketVersion::new(use_legacy_version),
packet_size,
packet_mode,
},
@@ -48,6 +51,9 @@ impl FramedSphinxPacket {
// but would that really be worth it?
#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)]
pub struct Header {
/// Represents the wire format version used to construct this packet.
pub(crate) packet_version: PacketVersion,
/// Represents type and consequently size of the included SphinxPacket.
pub(crate) packet_size: PacketSize,
@@ -64,11 +70,25 @@ pub struct Header {
}
impl Header {
pub(crate) const SIZE: usize = 2;
pub(crate) const LEGACY_SIZE: usize = 2;
pub(crate) const VERSIONED_SIZE: usize = 3;
pub(crate) fn size(&self) -> usize {
if self.packet_version.is_legacy() {
Self::LEGACY_SIZE
} else {
Self::VERSIONED_SIZE
}
}
pub(crate) fn encode(&self, dst: &mut BytesMut) {
// we reserve one byte for `packet_size` and the other for `mode`
dst.reserve(Self::SIZE);
dst.reserve(Self::LEGACY_SIZE);
if let Some(version) = self.packet_version.as_u8() {
dst.reserve(Self::VERSIONED_SIZE);
dst.put_u8(version)
}
dst.put_u8(self.packet_size as u8);
dst.put_u8(self.packet_mode as u8);
// reserve bytes for the actual packet
@@ -76,16 +96,30 @@ impl Header {
}
pub(crate) fn decode(src: &mut BytesMut) -> Result<Option<Self>, SphinxCodecError> {
if src.len() < Self::SIZE {
if src.len() < Self::LEGACY_SIZE {
// can't do anything if we don't have enough bytes - but reserve enough for the next call
src.reserve(Self::SIZE);
src.reserve(Self::LEGACY_SIZE);
return Ok(None);
}
Ok(Some(Header {
packet_size: PacketSize::try_from(src[0])?,
packet_mode: PacketMode::try_from(src[1])?,
}))
let packet_version = PacketVersion::from(src[0]);
if packet_version.is_legacy() {
Ok(Some(Header {
packet_version,
packet_size: PacketSize::try_from(src[0])?,
packet_mode: PacketMode::try_from(src[1])?,
}))
} else if src.len() < Self::VERSIONED_SIZE {
// we're missing that 1 byte to read the full header...
src.reserve(Self::VERSIONED_SIZE);
Ok(None)
} else {
Ok(Some(Header {
packet_version,
packet_size: PacketSize::try_from(src[1])?,
packet_mode: PacketMode::try_from(src[2])?,
}))
}
}
}
@@ -108,7 +142,16 @@ mod header_encoding {
// make sure this is still 'unknown' for if we make changes in the future
assert!(PacketSize::try_from(unknown_packet_size).is_err());
let mut bytes = BytesMut::from([unknown_packet_size, PacketMode::default() as u8].as_ref());
// unfortunately this will only work for the 'versioned' variant
// due to the hack used to get legacy mode compatibility
let mut bytes = BytesMut::from(
[
PacketVersion::new_versioned(123).as_u8().unwrap(),
unknown_packet_size,
PacketMode::default() as u8,
]
.as_ref(),
);
assert!(Header::decode(&mut bytes).is_err())
}
@@ -127,23 +170,47 @@ mod header_encoding {
let mut empty_bytes = BytesMut::new();
let decode_attempt_1 = Header::decode(&mut empty_bytes).unwrap();
assert!(decode_attempt_1.is_none());
assert!(empty_bytes.capacity() > Header::SIZE);
assert!(empty_bytes.capacity() > Header::LEGACY_SIZE);
let mut empty_bytes = BytesMut::with_capacity(1);
let decode_attempt_2 = Header::decode(&mut empty_bytes).unwrap();
assert!(decode_attempt_2.is_none());
assert!(empty_bytes.capacity() > Header::SIZE);
assert!(empty_bytes.capacity() > Header::LEGACY_SIZE);
}
#[test]
fn header_encoding_reserves_enough_bytes_for_full_sphinx_packet() {
fn header_encoding_reserves_enough_bytes_for_full_sphinx_packet_in_legacy_mode() {
let packet_sizes = vec![
PacketSize::AckPacket,
PacketSize::RegularPacket,
PacketSize::ExtendedPacket,
PacketSize::ExtendedPacket8,
PacketSize::ExtendedPacket16,
PacketSize::ExtendedPacket32,
];
for packet_size in packet_sizes {
let header = Header {
packet_version: PacketVersion::Legacy,
packet_size,
packet_mode: Default::default(),
};
let mut bytes = BytesMut::new();
header.encode(&mut bytes);
assert_eq!(bytes.capacity(), bytes.len() + packet_size.size())
}
}
#[test]
fn header_encoding_reserves_enough_bytes_for_full_sphinx_packet_in_versioned_mode() {
let packet_sizes = vec![
PacketSize::AckPacket,
PacketSize::RegularPacket,
PacketSize::ExtendedPacket8,
PacketSize::ExtendedPacket16,
PacketSize::ExtendedPacket32,
];
for packet_size in packet_sizes {
let header = Header {
packet_version: PacketVersion::Versioned(123),
packet_size,
packet_mode: Default::default(),
};
+12
View File
@@ -13,6 +13,7 @@ pub use packet_sizes::PacketSize;
pub mod packet_modes;
pub mod packet_sizes;
pub mod packet_version;
// If somebody can provide an argument why it might be reasonable to have more than 255 mix hops,
// I will change this to [`usize`]
@@ -24,6 +25,17 @@ pub const DEFAULT_NUM_MIX_HOPS: u8 = 3;
pub const FRAG_ID_LEN: usize = 5;
pub type SerializedFragmentIdentifier = [u8; FRAG_ID_LEN];
// wait, wait, but why are we starting with version 7?
// when packet header gets serialized, the following bytes (in that order) are put onto the wire:
// - packet_version (starting with v1.1.0)
// - packet_size indicator
// - packet_mode
// it also just so happens that the only valid values for packet_size indicator include values 1-6
// therefore if we receive byte `7` (or larger than that) we'll know we received a versioned packet,
// otherwise we should treat it as legacy
/// Increment it whenever we perform any breaking change in the wire format!
const CURRENT_PACKET_VERSION_NUMBER: u8 = 7;
// TODO: ask @AP about the choice of below algorithms
/// Hashing algorithm used during hkdf for ephemeral shared key generation per sphinx packet payload.
+58 -6
View File
@@ -5,6 +5,7 @@ use crate::FRAG_ID_LEN;
use nymsphinx_types::header::HEADER_SIZE;
use nymsphinx_types::PAYLOAD_OVERHEAD_SIZE;
use std::convert::TryFrom;
use std::str::FromStr;
// it's up to the smart people to figure those values out : )
const REGULAR_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 2 * 1024;
@@ -15,11 +16,16 @@ const REGULAR_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 2 * 102
const ACK_IV_SIZE: usize = 16;
const ACK_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + ACK_IV_SIZE + FRAG_ID_LEN;
const EXTENDED_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 32 * 1024;
const EXTENDED_PACKET_SIZE_8: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 8 * 1024;
const EXTENDED_PACKET_SIZE_16: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 16 * 1024;
const EXTENDED_PACKET_SIZE_32: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 32 * 1024;
#[derive(Debug)]
pub struct InvalidPacketSize;
#[derive(Debug)]
pub struct InvalidExtendedPacketSize;
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PacketSize {
@@ -30,7 +36,28 @@ pub enum PacketSize {
AckPacket = 2,
// for example for streaming fast and furious in uncompressed 10bit 4K HDR quality
ExtendedPacket = 3,
ExtendedPacket32 = 3,
// for example for streaming fast and furious in heavily compressed lossy RealPlayer quality
ExtendedPacket8 = 4,
// for example for streaming fast and furious in compressed XviD quality
ExtendedPacket16 = 5,
}
impl FromStr for PacketSize {
type Err = InvalidPacketSize;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"regular" => Ok(Self::RegularPacket),
"ack" => Ok(Self::AckPacket),
"extended8" => Ok(Self::ExtendedPacket8),
"extended16" => Ok(Self::ExtendedPacket16),
"extended32" => Ok(Self::ExtendedPacket32),
_ => Err(InvalidPacketSize),
}
}
}
impl TryFrom<u8> for PacketSize {
@@ -40,7 +67,9 @@ impl TryFrom<u8> for PacketSize {
match value {
_ if value == (PacketSize::RegularPacket as u8) => Ok(Self::RegularPacket),
_ if value == (PacketSize::AckPacket as u8) => Ok(Self::AckPacket),
_ if value == (PacketSize::ExtendedPacket as u8) => Ok(Self::ExtendedPacket),
_ if value == (PacketSize::ExtendedPacket8 as u8) => Ok(Self::ExtendedPacket8),
_ if value == (PacketSize::ExtendedPacket16 as u8) => Ok(Self::ExtendedPacket16),
_ if value == (PacketSize::ExtendedPacket32 as u8) => Ok(Self::ExtendedPacket32),
_ => Err(InvalidPacketSize),
}
}
@@ -51,7 +80,9 @@ impl PacketSize {
match self {
PacketSize::RegularPacket => REGULAR_PACKET_SIZE,
PacketSize::AckPacket => ACK_PACKET_SIZE,
PacketSize::ExtendedPacket => EXTENDED_PACKET_SIZE,
PacketSize::ExtendedPacket8 => EXTENDED_PACKET_SIZE_8,
PacketSize::ExtendedPacket16 => EXTENDED_PACKET_SIZE_16,
PacketSize::ExtendedPacket32 => EXTENDED_PACKET_SIZE_32,
}
}
@@ -68,12 +99,33 @@ impl PacketSize {
Ok(PacketSize::RegularPacket)
} else if PacketSize::AckPacket.size() == size {
Ok(PacketSize::AckPacket)
} else if PacketSize::ExtendedPacket.size() == size {
Ok(PacketSize::ExtendedPacket)
} else if PacketSize::ExtendedPacket8.size() == size {
Ok(PacketSize::ExtendedPacket8)
} else if PacketSize::ExtendedPacket16.size() == size {
Ok(PacketSize::ExtendedPacket16)
} else if PacketSize::ExtendedPacket32.size() == size {
Ok(PacketSize::ExtendedPacket32)
} else {
Err(InvalidPacketSize)
}
}
pub fn is_extended_size(&self) -> bool {
match self {
PacketSize::RegularPacket | PacketSize::AckPacket => false,
PacketSize::ExtendedPacket8
| PacketSize::ExtendedPacket16
| PacketSize::ExtendedPacket32 => true,
}
}
pub fn as_extended_size(self) -> Option<Self> {
if self.is_extended_size() {
Some(self)
} else {
None
}
}
}
impl Default for PacketSize {
@@ -0,0 +1,59 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{PacketSize, CURRENT_PACKET_VERSION_NUMBER};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PacketVersion {
// this will allow updated mixnodes to still understand packets from before the update
Legacy,
Versioned(u8),
}
impl PacketVersion {
pub fn new(use_legacy: bool) -> Self {
if use_legacy {
Self::new_legacy()
} else {
Self::new_versioned(CURRENT_PACKET_VERSION_NUMBER)
}
}
pub fn new_legacy() -> Self {
PacketVersion::Legacy
}
pub fn new_versioned(version: u8) -> Self {
PacketVersion::Versioned(version)
}
pub fn is_legacy(&self) -> bool {
matches!(self, PacketVersion::Legacy)
}
pub fn as_u8(&self) -> Option<u8> {
match self {
PacketVersion::Legacy => None,
PacketVersion::Versioned(version) => Some(*version),
}
}
}
impl From<u8> for PacketVersion {
fn from(v: u8) -> Self {
match v {
n if n == PacketSize::RegularPacket as u8 => PacketVersion::Legacy,
n if n == PacketSize::AckPacket as u8 => PacketVersion::Legacy,
n if n == PacketSize::ExtendedPacket8 as u8 => PacketVersion::Legacy,
n if n == PacketSize::ExtendedPacket16 as u8 => PacketVersion::Legacy,
n if n == PacketSize::ExtendedPacket32 as u8 => PacketVersion::Legacy,
n => PacketVersion::Versioned(n),
}
}
}
impl Default for PacketVersion {
fn default() -> Self {
PacketVersion::Versioned(CURRENT_PACKET_VERSION_NUMBER)
}
}
+23 -10
View File
@@ -13,6 +13,13 @@ pub struct OrderedMessageBuffer {
messages: HashMap<u64, OrderedMessage>,
}
/// Data returned from `OrderedMessageBuffer` on a successful read of gapless ordered data.
#[derive(Debug, PartialEq, Eq)]
pub struct ReadContiguousData {
pub data: Vec<u8>,
pub last_index: u64,
}
impl OrderedMessageBuffer {
pub fn new() -> OrderedMessageBuffer {
OrderedMessageBuffer {
@@ -42,7 +49,7 @@ impl OrderedMessageBuffer {
/// a read will return the bytes of messages 0, 1, 2. Subsequent reads will
/// return `None` until message 3 comes in, at which point 3, 4, and any
/// further contiguous messages which have arrived will be returned.
pub fn read(&mut self) -> Option<Vec<u8>> {
pub fn read(&mut self) -> Option<ReadContiguousData> {
if !self.messages.contains_key(&self.next_index) {
return None;
}
@@ -66,7 +73,10 @@ impl OrderedMessageBuffer {
.collect();
trace!("Returning {} bytes from ordered message buffer", data.len());
Some(data)
Some(ReadContiguousData {
data,
last_index: index,
})
}
}
@@ -102,11 +112,11 @@ mod test_chunking_and_reassembling {
};
buffer.write(first_message);
let first_read = buffer.read().unwrap();
let first_read = buffer.read().unwrap().data;
assert_eq!(vec![1, 2, 3, 4], first_read);
buffer.write(second_message);
let second_read = buffer.read().unwrap();
let second_read = buffer.read().unwrap().data;
assert_eq!(vec![5, 6, 7, 8], second_read);
assert_eq!(None, buffer.read()); // second read on fully ordered result set is empty
@@ -128,7 +138,7 @@ mod test_chunking_and_reassembling {
buffer.write(first_message);
buffer.write(second_message);
let second_read = buffer.read();
assert_eq!(vec![1, 2, 3, 4, 5, 6, 7, 8], second_read.unwrap());
assert_eq!(vec![1, 2, 3, 4, 5, 6, 7, 8], second_read.unwrap().data);
assert_eq!(None, buffer.read()); // second read on fully ordered result set is empty
}
@@ -147,8 +157,8 @@ mod test_chunking_and_reassembling {
buffer.write(second_message);
buffer.write(first_message);
let read = buffer.read();
assert_eq!(vec![1, 2, 3, 4, 5, 6, 7, 8], read.unwrap());
let read = buffer.read().unwrap().data;
assert_eq!(vec![1, 2, 3, 4, 5, 6, 7, 8], read);
assert_eq!(None, buffer.read()); // second read on fully ordered result set is empty
}
}
@@ -182,7 +192,7 @@ mod test_chunking_and_reassembling {
#[test]
fn everything_up_to_the_indexing_gap_is_returned() {
let mut buffer = setup();
let ordered_bytes = buffer.read().unwrap();
let ordered_bytes = buffer.read().unwrap().data;
assert_eq!([0, 0, 0, 0, 1, 1, 1, 1].to_vec(), ordered_bytes);
// we shouldn't get any more from a second attempt if nothing is added
@@ -208,7 +218,7 @@ mod test_chunking_and_reassembling {
};
buffer.write(two_message);
let more_ordered_bytes = buffer.read().unwrap();
let more_ordered_bytes = buffer.read().unwrap().data;
assert_eq!([2, 2, 2, 2, 3, 3, 3, 3].to_vec(), more_ordered_bytes);
// let's add another message
@@ -227,7 +237,10 @@ mod test_chunking_and_reassembling {
};
buffer.write(four_message);
assert_eq!([4, 4, 4, 4, 5, 5, 5, 5].to_vec(), buffer.read().unwrap());
assert_eq!(
[4, 4, 4, 4, 5, 5, 5, 5].to_vec(),
buffer.read().unwrap().data
);
// at this point we should again get back nothing if we try a read
assert_eq!(None, buffer.read());
+1 -1
View File
@@ -2,7 +2,7 @@ mod buffer;
mod message;
mod sender;
pub use buffer::OrderedMessageBuffer;
pub use buffer::{OrderedMessageBuffer, ReadContiguousData};
pub use message::MessageError;
pub use message::OrderedMessage;
pub use sender::OrderedMessageSender;
@@ -4,7 +4,7 @@
use futures::channel::mpsc;
use futures::StreamExt;
use log::*;
use ordered_buffer::{OrderedMessage, OrderedMessageBuffer};
use ordered_buffer::{OrderedMessage, OrderedMessageBuffer, ReadContiguousData};
use socks5_requests::ConnectionId;
use std::collections::{HashMap, HashSet};
use task::ShutdownListener;
@@ -38,12 +38,13 @@ pub enum ControllerCommand {
struct ActiveConnection {
is_closed: bool,
closed_at_index: Option<u64>,
connection_sender: Option<ConnectionSender>,
ordered_buffer: OrderedMessageBuffer,
}
impl ActiveConnection {
fn write_to_buf(&mut self, payload: Vec<u8>) {
fn write_to_buf(&mut self, payload: Vec<u8>, is_closed: bool) {
let ordered_message = match OrderedMessage::try_from_bytes(payload) {
Ok(msg) => msg,
Err(err) => {
@@ -51,10 +52,13 @@ impl ActiveConnection {
return;
}
};
if is_closed {
self.closed_at_index = Some(ordered_message.index);
}
self.ordered_buffer.write(ordered_message);
}
fn read_from_buf(&mut self) -> Option<Vec<u8>> {
fn read_from_buf(&mut self) -> Option<ReadContiguousData> {
self.ordered_buffer.read()
}
}
@@ -99,6 +103,7 @@ impl Controller {
is_closed: false,
connection_sender: Some(connection_sender),
ordered_buffer: OrderedMessageBuffer::new(),
closed_at_index: None,
};
if let Some(_active_conn) = self.active_connections.insert(conn_id, active_connection) {
error!("Received a duplicate 'Connect'!")
@@ -127,21 +132,23 @@ impl Controller {
fn send_to_connection(&mut self, conn_id: ConnectionId, payload: Vec<u8>, is_closed: bool) {
if let Some(active_connection) = self.active_connections.get_mut(&conn_id) {
if !payload.is_empty() {
active_connection.write_to_buf(payload);
active_connection.write_to_buf(payload, is_closed);
} else if !is_closed {
error!("Tried to write an empty message to a not-closing connection. Please let us know if you see this message");
}
// if messages get unordered, make sure we don't lose information about
// remote socket getting closed!
active_connection.is_closed |= is_closed;
if let Some(payload) = active_connection.read_from_buf() {
if let Some(closed_at_index) = active_connection.closed_at_index {
if payload.last_index > closed_at_index {
active_connection.is_closed = true;
}
}
if let Err(err) = active_connection
.connection_sender
.as_mut()
.unwrap()
.unbounded_send(ConnectionMessage {
payload,
payload: payload.data,
socket_closed: active_connection.is_closed,
})
{
+1
View File
@@ -62,6 +62,7 @@ pub struct DelegationWithEverything {
// DEPRECATED, IF POSSIBLE TRY TO DISCONTINUE USE OF IT!
pub pending_events: Vec<DelegationEvent>,
pub mixnode_is_unbonding: Option<bool>,
}
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
+35 -4
View File
@@ -1,14 +1,46 @@
## [nym-contracts-v1.1.0](https://github.com/nymtech/nym/tree/nym-contracts-v1.1.0) (2022-11-09)
### Changed
- mixnet-contract: rework of rewarding ([#1472]), which includes, but is not limited to:
- internal reward accounting was modified to be similar to the ideas presented in Cosmos' F1 paper, which results in throughput gains and no storage or gas cost bloat over time,
- introduced internal queues for pending epoch and interval events that only get resolved once relevant epoch/interval rolls over
- the contract no longer stores any historical information regarding past epochs/parameters/stake state for the purposes of rewarding
- a lot of queries got renamed to keep naming more consistent,
- introduced new utility-based queries such as a query for reward estimation for the current epoch,
- mixnodes are now identified by a monotonously increasing `mix_id`
- bonding now results in getting fresh `mix_id` and thus if given node decides to unbond and rebond, it will lose all its delegations,
- mixnode operators are now allowed to set their operating costs as opposed to having fixed value of 40nym/interval
- rewarding parameters are now correctly updated at an **interval** end
- rewarding parameters now include a staking supply scale factor attribute (beta in the tokenomics paper)
- node performance can now be more granular with internal `Decimal` representation as opposed to an `u8`
- node profit margin can now be more granular with internal `Decimal` representation as opposed to an `u8`
- mixnode operators are now allowed to change their configuration options, such as port information, without having to unbond
- mixnode unbonding is no longer instantaneous, instead it happens once an epoch rolls over
- it is now possible to query for operator and node history to see how often (and with what parameters) they rebonded
- other minor bugfixes and changes
- ...
- new exciting bugs to find and squash
- vesting-contract: optional locked token pledge cap per account ([#1687]), defaults to 10%
- vesting-contract: updated internal delegation storage due to mixnet contract revamp ([#1472])
### Added
- vesting-contract: added query for obtaining contract build information ([#1726])
[#1472]: https://github.com/nymtech/nym/pull/1472
[#1687]: https://github.com/nymtech/nym/pull/1687
[#1726]: https://github.com/nymtech/nym/pull/1726
## [nym-contracts-v1.0.2](https://github.com/nymtech/nym/tree/nym-contracts-v1.0.2) (2022-09-13)
### Added
- vesting-contract: added queries for delegation timestamps and paged query for all vesting delegations in the contract ([#1569])
- all binaries: added shell completion and [Fig](fig.io) spec generation ([#1638])
### Changed
- mixnet-contract: compounding delegator rewards now happens instantaneously as opposed to having to wait for the current epoch to finish ([#1571])
- network-requester: updated CLI to use `clap` macros ([#1638])
### Fixed
@@ -18,9 +50,8 @@
[#1544]: https://github.com/nymtech/nym/pull/1544
[#1569]: https://github.com/nymtech/nym/pull/1569
[#1569]: https://github.com/nymtech/nym/pull/1571
[#1571]: https://github.com/nymtech/nym/pull/1571
[#1613]: https://github.com/nymtech/nym/pull/1613
[#1638]: https://github.com/nymtech/nym/pull/1638
## [nym-contracts-v1.0.1](https://github.com/nymtech/nym/tree/nym-contracts-v1.0.1) (2022-06-22)
+2 -2
View File
@@ -930,7 +930,7 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
name = "mixnet-contract"
version = "1.0.2"
version = "1.1.0"
dependencies = [
"bs58",
"cosmwasm-schema",
@@ -1615,6 +1615,7 @@ dependencies = [
"schemars",
"serde",
"thiserror",
"vergen",
"vesting-contract-common",
]
@@ -1624,7 +1625,6 @@ version = "0.1.0"
dependencies = [
"contracts-common",
"cosmwasm-std",
"log",
"mixnet-contract-common",
"schemars",
"serde",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "mixnet-contract"
version = "1.0.2"
version = "1.1.0"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
edition = "2021"
+2
View File
@@ -497,6 +497,7 @@ mod tests {
initial_rewarding_params: InitialRewardingParams {
initial_reward_pool: Decimal::from_atomics(100_000_000_000_000u128, 0).unwrap(),
initial_staking_supply: Decimal::from_atomics(123_456_000_000_000u128, 0).unwrap(),
staking_supply_scale_factor: Percent::hundred(),
sybil_resistance: Percent::from_percentage_value(23).unwrap(),
active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(),
interval_pool_emission: Percent::from_percentage_value(1).unwrap(),
@@ -535,6 +536,7 @@ mod tests {
interval: IntervalRewardParams {
reward_pool: Decimal::from_atomics(100_000_000_000_000u128, 0).unwrap(),
staking_supply: Decimal::from_atomics(123_456_000_000_000u128, 0).unwrap(),
staking_supply_scale_factor: Percent::hundred(),
epoch_reward_budget: expected_epoch_reward_budget,
stake_saturation_point: expected_stake_saturation_point,
sybil_resistance: Percent::from_percentage_value(23).unwrap(),
@@ -1220,6 +1220,7 @@ mod tests {
let update = IntervalRewardingParamsUpdate {
reward_pool: Some(before.interval.reward_pool / two),
staking_supply: Some(before.interval.staking_supply * four),
staking_supply_scale_factor: None,
sybil_resistance_percent: Some(Percent::from_percentage_value(42).unwrap()),
active_set_work_factor: None,
interval_pool_emission: None,
+2 -1
View File
@@ -20,7 +20,8 @@ pub(crate) fn apply_reward_pool_changes(
let reward_pool = rewarding_params.interval.reward_pool - pending_pool_change.removed
+ pending_pool_change.added;
let staking_supply = rewarding_params.interval.staking_supply + pending_pool_change.removed;
let staking_supply = rewarding_params.interval.staking_supply
+ rewarding_params.interval.staking_supply_scale_factor * pending_pool_change.removed;
let epoch_reward_budget = reward_pool
/ Decimal::from_atomics(interval.epochs_in_interval(), 0).unwrap()
* rewarding_params.interval.interval_pool_emission;
@@ -1710,6 +1710,7 @@ pub mod tests {
let update = IntervalRewardingParamsUpdate {
reward_pool: None,
staking_supply: None,
staking_supply_scale_factor: None,
sybil_resistance_percent: None,
active_set_work_factor: None,
interval_pool_emission: None,
@@ -1742,6 +1743,7 @@ pub mod tests {
let empty_update = IntervalRewardingParamsUpdate {
reward_pool: None,
staking_supply: None,
staking_supply_scale_factor: None,
sybil_resistance_percent: None,
active_set_work_factor: None,
interval_pool_emission: None,
@@ -1761,6 +1763,7 @@ pub mod tests {
let update = IntervalRewardingParamsUpdate {
reward_pool: None,
staking_supply: None,
staking_supply_scale_factor: None,
sybil_resistance_percent: None,
active_set_work_factor: None,
interval_pool_emission: None,
@@ -1795,6 +1798,7 @@ pub mod tests {
let update = IntervalRewardingParamsUpdate {
reward_pool: None,
staking_supply: None,
staking_supply_scale_factor: None,
sybil_resistance_percent: None,
active_set_work_factor: None,
interval_pool_emission: None,
@@ -1819,6 +1823,7 @@ pub mod tests {
let update = IntervalRewardingParamsUpdate {
reward_pool: None,
staking_supply: None,
staking_supply_scale_factor: None,
sybil_resistance_percent: None,
active_set_work_factor: None,
interval_pool_emission: None,
@@ -1859,6 +1864,7 @@ pub mod tests {
let update = IntervalRewardingParamsUpdate {
reward_pool: Some(old.interval.reward_pool / two),
staking_supply: Some(old.interval.staking_supply * four),
staking_supply_scale_factor: None,
sybil_resistance_percent: None,
active_set_work_factor: None,
interval_pool_emission: None,
@@ -768,6 +768,7 @@ pub mod test_helpers {
InitialRewardingParams {
initial_reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens)
initial_staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M
staking_supply_scale_factor: Percent::hundred(),
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(),
interval_pool_emission: Percent::from_percentage_value(2).unwrap(),
+4 -1
View File
@@ -23,4 +23,7 @@ cw-storage-plus = { version = "0.13.4", features = ["iterator"] }
schemars = "0.8"
serde = { version = "1.0", default-features = false, features = ["derive"] }
thiserror = { version = "1.0" }
thiserror = { version = "1.0" }
[build-dependencies]
vergen = { version = "5", default-features = false, features = ["build", "git", "rustc"] }
+8
View File
@@ -0,0 +1,8 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use vergen::{vergen, Config};
fn main() {
vergen(Config::default()).expect("failed to extract build metadata")
}
+20 -3
View File
@@ -1,13 +1,14 @@
use crate::errors::ContractError;
use crate::queued_migrations::migrate_to_v2_mixnet_contract;
use crate::storage::{
account_from_address, BlockTimestampSecs, ADMIN, DELEGATIONS, MIXNET_CONTRACT_ADDRESS,
MIX_DENOM,
account_from_address, save_account, BlockTimestampSecs, ADMIN, DELEGATIONS,
MIXNET_CONTRACT_ADDRESS, MIX_DENOM,
};
use crate::traits::{
DelegatingAccount, GatewayBondingAccount, MixnodeBondingAccount, VestingAccount,
};
use crate::vesting::{populate_vesting_periods, Account};
use contracts_common::ContractBuildInformation;
use cosmwasm_std::{
coin, entry_point, to_binary, BankMsg, Coin, Deps, DepsMut, Env, MessageInfo, Order,
QueryResponse, Response, StdResult, Timestamp, Uint128,
@@ -157,7 +158,7 @@ pub fn try_update_locked_pledge_cap(
let mut account = account_from_address(&address, deps.storage, deps.api)?;
account.pledge_cap = Some(cap);
// update_locked_pledge_cap(amount, deps.storage)?;
save_account(&account, deps.storage)?;
Ok(Response::default())
}
@@ -500,6 +501,7 @@ fn try_create_periodic_vesting_account(
#[entry_point]
pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result<QueryResponse, ContractError> {
let query_res = match msg {
QueryMsg::GetContractVersion {} => to_binary(&get_contract_version()),
QueryMsg::LockedCoins {
vesting_account_address,
block_time,
@@ -606,6 +608,21 @@ pub fn try_get_account(address: &str, deps: Deps<'_>) -> Result<Account, Contrac
account_from_address(address, deps.storage, deps.api)
}
/// Gets build information of this contract.
pub fn get_contract_version() -> ContractBuildInformation {
// as per docs
// env! macro will expand to the value of the named environment variable at
// compile time, yielding an expression of type `&'static str`
ContractBuildInformation {
build_timestamp: env!("VERGEN_BUILD_TIMESTAMP").to_string(),
build_version: env!("VERGEN_BUILD_SEMVER").to_string(),
commit_sha: env!("VERGEN_GIT_SHA").to_string(),
commit_timestamp: env!("VERGEN_GIT_COMMIT_TIMESTAMP").to_string(),
commit_branch: env!("VERGEN_GIT_BRANCH").to_string(),
rustc_version: env!("VERGEN_RUSTC_SEMVER").to_string(),
}
}
/// Gets currently locked coins, see [crate::traits::VestingAccount::locked_coins]
pub fn try_get_locked_coins(
vesting_account_address: &str,
@@ -105,7 +105,7 @@ impl VestingAccount for Account {
}
fn get_end_time(&self) -> Timestamp {
self.periods[(self.num_vesting_periods() - 1) as usize].end_time()
self.periods[(self.num_vesting_periods() - 1)].end_time()
}
fn get_original_vesting(&self) -> OriginalVestingResponse {
@@ -133,7 +133,7 @@ impl VestingAccount for Account {
let start_time = match period {
Period::Before => 0,
Period::After => u64::MAX,
Period::In(idx) => self.periods[idx as usize].start_time,
Period::In(idx) => self.periods[idx].start_time,
};
let coin = self.total_delegations_at_timestamp(storage, start_time)?;
@@ -179,7 +179,7 @@ impl VestingAccount for Account {
let start_time = match period {
Period::Before => 0,
Period::After => u64::MAX,
Period::In(idx) => self.periods[idx as usize].start_time,
Period::In(idx) => self.periods[idx].start_time,
};
let amount = if let Some(bond) = self
+2 -2
View File
@@ -15,7 +15,7 @@ pub struct VestingPeriod {
impl VestingPeriod {
pub fn end_time(&self) -> Timestamp {
Timestamp::from_seconds(self.start_time + self.period_seconds as u64)
Timestamp::from_seconds(self.start_time + self.period_seconds)
}
}
@@ -26,7 +26,7 @@ pub fn populate_vesting_periods(
let mut periods = Vec::with_capacity(vesting_spec.num_periods() as usize);
for i in 0..vesting_spec.num_periods() {
let period = VestingPeriod {
start_time: start_time + i as u64 * vesting_spec.period_seconds(),
start_time: start_time + i * vesting_spec.period_seconds(),
period_seconds: vesting_spec.period_seconds(),
};
periods.push(period);
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "explorer-api"
version = "1.0.1"
version = "1.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -29,5 +29,6 @@ dotenv = "0.15.0"
mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" }
contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" }
network-defaults = { path = "../common/network-defaults" }
logging = { path = "../common/logging"}
task = { path = "../common/task" }
validator-client = { path = "../common/client-libs/validator-client", features=["nymd-client"] }
@@ -58,7 +58,10 @@ impl GeoLocateTask {
continue;
}
match geo_ip.query(&cache_item.mix_node().host) {
match geo_ip.query(
&cache_item.mix_node().host,
Some(cache_item.mix_node().mix_port),
) {
Ok(opt) => match opt {
Some(location) => {
let location = Location::new(location);
+27 -5
View File
@@ -4,9 +4,14 @@
use isocountry::CountryCode;
use log::warn;
use maxminddb::{geoip2::Country, MaxMindDBError, Reader};
use std::{net::IpAddr, str::FromStr, sync::Arc};
use std::{
net::{IpAddr, ToSocketAddrs},
str::FromStr,
sync::Arc,
};
const DEFAULT_DATABASE_PATH: &str = "./geo_ip/GeoLite2-Country.mmdb";
const FAKE_PORT: u16 = 1234;
#[derive(Debug)]
pub enum GeoIpError {
@@ -52,10 +57,27 @@ impl GeoIp {
GeoIp { db: reader }
}
pub fn query(&self, address: &str) -> Result<Option<Location>, GeoIpError> {
let ip: IpAddr = FromStr::from_str(address).map_err(|e| {
error!("Fail to create IpAddr from {}: {}", &address, e);
GeoIpError::NoValidIP
pub fn query(&self, address: &str, port: Option<u16>) -> Result<Option<Location>, GeoIpError> {
let ip: IpAddr = FromStr::from_str(address).or_else(|_| {
debug!(
"Fail to create IpAddr from {}. Trying using internal lookup...",
&address
);
let p = port.unwrap_or(FAKE_PORT);
let socket_addr = (address, p)
.to_socket_addrs()
.map_err(|e| {
error!("Fail to resolve IP address from {}:{}: {}", &address, p, e);
GeoIpError::NoValidIP
})?
.next()
.ok_or_else(|| {
error!("Fail to resolve IP address from {}:{}", &address, p);
GeoIpError::NoValidIP
})?;
let ip = socket_addr.ip();
debug!("Internal lookup succeed, resolved ip: {}", ip);
Ok(ip)
})?;
let result = self
.db
+1 -1
View File
@@ -39,7 +39,7 @@ fn find_location(request: &Request<'_>) -> Result<Location, (Status, LocationErr
let location = geo_ip
.0
.clone()
.query(&ip)
.query(&ip, None)
.map_err(|e| match e {
GeoIpError::NoValidIP => (Status::Forbidden, LocationError::NoIP),
GeoIpError::InternalError => {
+1 -15
View File
@@ -6,6 +6,7 @@ extern crate rocket_okapi;
use clap::Parser;
use dotenv::dotenv;
use log::info;
use logging::setup_logging;
use network_defaults::setup_env;
use task::ShutdownNotifier;
@@ -115,18 +116,3 @@ async fn wait_for_signal() {
},
}
}
fn setup_logging() {
let mut log_builder = pretty_env_logger::formatted_timed_builder();
if let Ok(s) = ::std::env::var("RUST_LOG") {
log_builder.parse_filters(&s);
} else {
// default to 'Info'
log_builder.filter(None, log::LevelFilter::Info);
}
log_builder
.filter_module("tokio_reactor", log::LevelFilter::Warn)
.filter_module("reqwest", log::LevelFilter::Warn)
.init();
}
+2
View File
@@ -41,6 +41,8 @@ pub(crate) async fn retrieve_mixnode_econ_stats(
Some(EconomicDynamicsStats {
stake_saturation: best_effort_small_dec_to_f64(stake_saturation.saturation) as f32,
uncapped_saturation: best_effort_small_dec_to_f64(stake_saturation.uncapped_saturation)
as f32,
active_set_inclusion_probability: inclusion_probability.in_active,
reserve_set_inclusion_probability: inclusion_probability.in_reserve,
// drop precision for compatibility sake
+6
View File
@@ -3,6 +3,7 @@
use crate::cache::Cache;
use crate::mix_nodes::location::Location;
use contracts_common::Percent;
use mixnet_contract_common::Delegation;
use mixnet_contract_common::{Addr, Coin, Layer, MixId, MixNode};
use serde::Deserialize;
@@ -24,6 +25,7 @@ pub(crate) enum MixnodeStatus {
pub(crate) struct PrettyDetailedMixNodeBond {
// I leave this to @MS to refactor this type as a lot of things here are redundant thanks to
// the existence of `MixNodeDetails`
pub mix_id: MixId,
pub location: Option<Location>,
pub status: MixnodeStatus,
pub pledge_amount: Coin,
@@ -32,9 +34,12 @@ pub(crate) struct PrettyDetailedMixNodeBond {
pub layer: Layer,
pub mix_node: MixNode,
pub stake_saturation: f32,
pub uncapped_saturation: f32,
pub avg_uptime: u8,
pub estimated_operator_apy: f64,
pub estimated_delegators_apy: f64,
pub operating_cost: Coin,
pub profit_margin_percent: Percent,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)]
@@ -149,6 +154,7 @@ pub(crate) struct NodeStats {
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
pub(crate) struct EconomicDynamicsStats {
pub(crate) stake_saturation: f32,
pub(crate) uncapped_saturation: f32,
pub(crate) active_set_inclusion_probability: SelectionChance,
pub(crate) reserve_set_inclusion_probability: SelectionChance,
+5 -13
View File
@@ -126,19 +126,6 @@ impl ThreadsafeMixNodesCache {
self.mixnodes.read().await.get_mixnode(mix_id)
}
pub(crate) async fn get_mixnode_by_identity(
&self,
pubkey: &str,
) -> Option<MixNodeBondAnnotated> {
let all_nodes = self.get_mixnodes().await?;
for (_, node) in all_nodes {
if node.mix_node().identity_key == pubkey {
return Some(node);
}
}
None
}
pub(crate) async fn get_mixnodes(&self) -> Option<HashMap<MixId, MixNodeBondAnnotated>> {
self.mixnodes.read().await.get_mixnodes()
}
@@ -154,6 +141,7 @@ impl ThreadsafeMixNodesCache {
let rewarding_info = &node.mixnode_details.rewarding_details;
PrettyDetailedMixNodeBond {
mix_id,
location: location.and_then(|l| l.location.clone()),
status: mixnodes_guard.determine_node_status(mix_id),
pledge_amount: truncate_reward(rewarding_info.operator, denom),
@@ -163,8 +151,12 @@ impl ThreadsafeMixNodesCache {
mix_node: node.mixnode_details.bond_information.mix_node.clone(),
avg_uptime: node.performance.round_to_integer(),
stake_saturation: best_effort_small_dec_to_f64(node.stake_saturation) as f32,
uncapped_saturation: best_effort_small_dec_to_f64(node.uncapped_stake_saturation)
as f32,
estimated_operator_apy: best_effort_small_dec_to_f64(node.estimated_operator_apy),
estimated_delegators_apy: best_effort_small_dec_to_f64(node.estimated_delegators_apy),
operating_cost: rewarding_info.cost_params.interval_operating_cost.clone(),
profit_margin_percent: rewarding_info.cost_params.profit_margin_percent,
}
}
+11 -13
View File
@@ -1,7 +1,7 @@
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use mixnet_contract_common::MixNode;
use mixnet_contract_common::{MixId, MixNode};
use rocket::serde::json::Json;
use rocket::{Route, State};
use rocket_okapi::okapi::openapi3::OpenApi;
@@ -20,41 +20,39 @@ pub fn ping_make_default_routes(settings: &OpenApiSettings) -> (Vec<Route>, Open
openapi_get_routes_spec![settings: index]
}
// TODO: I'm not deprecating this one explicitly since we don't have
// a decision on whether nodes should be accessed (as in using URL) by id or identity key
#[openapi(tag = "ping")]
#[get("/<pubkey>")]
#[get("/<mix_id>")]
pub(crate) async fn index(
pubkey: &str,
mix_id: MixId,
state: &State<ExplorerApiStateContext>,
) -> Option<Json<PingResponse>> {
match state.inner.ping.clone().get(pubkey).await {
match state.inner.ping.clone().get(mix_id).await {
Some(cache_value) => {
trace!("Returning cached value for {}", pubkey);
trace!("Returning cached value for {}", mix_id);
Some(Json(PingResponse {
pending: cache_value.pending,
ports: cache_value.ports,
}))
}
None => {
trace!("No cache value for {}", pubkey);
trace!("No cache value for {}", mix_id);
match state.inner.get_mix_node_by_pubkey(pubkey).await {
match state.inner.get_mix_node(mix_id).await {
Some(node) => {
// set status to pending, so that any HTTP requests are pending
state.inner.ping.set_pending(pubkey).await;
state.inner.ping.set_pending(mix_id).await;
// do the check
let ports = Some(port_check(node.mix_node()).await);
trace!("Tested mix node {}: {:?}", pubkey, ports);
trace!("Tested mix node {}: {:?}", mix_id, ports);
let response = PingResponse {
ports,
pending: false,
};
// cache for 1 min
trace!("Caching value for {}", pubkey);
state.inner.ping.set(pubkey, response.clone()).await;
trace!("Caching value for {}", mix_id);
state.inner.ping.set(mix_id, response.clone()).await;
// return response
Some(Json(response))
+8 -7
View File
@@ -2,11 +2,12 @@ use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use mixnet_contract_common::MixId;
use serde::Deserialize;
use serde::Serialize;
use tokio::sync::RwLock;
pub(crate) type PingCache = HashMap<String, PingCacheItem>;
pub(crate) type PingCache = HashMap<MixId, PingCacheItem>;
const PING_TTL: Duration = Duration::from_secs(60 * 5); // 5 mins, before port check will be re-tried (only while pending)
const CACHE_TTL: Duration = Duration::from_secs(60 * 60); // 1 hour, to cache result from port check
@@ -23,11 +24,11 @@ impl ThreadsafePingCache {
}
}
pub(crate) async fn get(&self, identity_key: &str) -> Option<PingResponse> {
pub(crate) async fn get(&self, mix_id: MixId) -> Option<PingResponse> {
self.inner
.read()
.await
.get(identity_key)
.get(&mix_id)
.filter(|cache_item| cache_item.valid_until > SystemTime::now())
.map(|cache_item| {
if cache_item.pending {
@@ -43,9 +44,9 @@ impl ThreadsafePingCache {
})
}
pub(crate) async fn set_pending(&self, identity_key: &str) {
pub(crate) async fn set_pending(&self, mix_id: MixId) {
self.inner.write().await.insert(
identity_key.to_string(),
mix_id,
PingCacheItem {
pending: true,
valid_until: SystemTime::now() + PING_TTL,
@@ -54,9 +55,9 @@ impl ThreadsafePingCache {
);
}
pub(crate) async fn set(&self, identity_key: &str, item: PingResponse) {
pub(crate) async fn set(&self, mix_id: MixId, item: PingResponse) {
self.inner.write().await.insert(
identity_key.to_string(),
mix_id,
PingCacheItem {
pending: false,
valid_until: SystemTime::now() + CACHE_TTL,
+1 -8
View File
@@ -3,7 +3,7 @@ use std::path::Path;
use chrono::{DateTime, Utc};
use log::info;
use mixnet_contract_common::{IdentityKeyRef, MixId};
use mixnet_contract_common::MixId;
use serde::{Deserialize, Serialize};
use crate::client::ThreadsafeValidatorClient;
@@ -41,13 +41,6 @@ impl ExplorerApiState {
pub(crate) async fn get_mix_node(&self, mix_id: MixId) -> Option<MixNodeBondAnnotated> {
self.mixnodes.get_mixnode(mix_id).await
}
pub(crate) async fn get_mix_node_by_pubkey(
&self,
pubkey: IdentityKeyRef<'_>,
) -> Option<MixNodeBondAnnotated> {
self.mixnodes.get_mixnode_by_identity(pubkey).await
}
}
#[derive(Debug, Serialize, Deserialize)]
+2 -1
View File
@@ -26,7 +26,8 @@
"react-router-dom": "6",
"react-simple-maps": "^2.3.0",
"react-tooltip": "^4.2.21",
"use-clipboard-copy": "^0.2.0"
"use-clipboard-copy": "^0.2.0",
"big.js": "^6.2.1"
},
"devDependencies": {
"@babel/core": "^7.15.0",
+1
View File
@@ -14,6 +14,7 @@ export const VALIDATORS_API = `${VALIDATOR_BASE_URL}/validators`;
export const BLOCK_API = `${VALIDATOR_API_BASE_URL}/block`;
export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`;
export const UPTIME_STORY_API = `${VALIDATOR_API_BASE_URL}/api/v1/status/mixnode`; // add ID then '/history' to this.
export const UPTIME_STORY_API_GATEWAY = `${VALIDATOR_API_BASE_URL}/api/v1/status/gateway`; // add ID then '/history' or '/report' to this.
// errors
export const MIXNODE_API_ERROR = "We're having trouble finding that record, please try again or Contact Us.";
+9 -1
View File
@@ -2,6 +2,7 @@ import {
BLOCK_API,
COUNTRY_DATA_API,
GATEWAYS_API,
UPTIME_STORY_API_GATEWAY,
MIXNODE_API,
MIXNODE_PING,
MIXNODES_API,
@@ -15,6 +16,8 @@ import {
DelegationsResponse,
UniqDelegationsResponse,
GatewayResponse,
GatewayReportResponse,
UptimeStoryResponse,
MixNodeDescriptionResponse,
MixNodeResponse,
MixNodeResponseItem,
@@ -23,7 +26,6 @@ import {
StatsResponse,
StatusResponse,
SummaryOverviewResponse,
UptimeStoryResponse,
ValidatorsResponse,
} from '../typeDefs/explorer-api';
@@ -92,6 +94,12 @@ export class Api {
return res.json();
};
static fetchGatewayUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> =>
(await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/history`)).json();
static fetchGatewayReportById = async (id: string): Promise<GatewayReportResponse> =>
(await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/report`)).json();
static fetchValidators = async (): Promise<ValidatorsResponse> => {
const res = await fetch(VALIDATORS_API);
const json = await res.json();
+57 -36
View File
@@ -1,9 +1,12 @@
import * as React from 'react';
import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { Tooltip } from '@nymproject/react/tooltip/Tooltip';
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard';
import { Box } from '@mui/system';
import { cellStyles } from './Universal-DataGrid';
import { currencyToString } from '../utils/currency';
import { GatewayEnrichedRowType } from './Gateways';
import { MixnodeRowType } from './MixNodes';
export type ColumnsType = {
@@ -43,42 +46,60 @@ function formatCellValues(val: string | number, field: string) {
export const DetailTable: React.FC<{
tableName: string;
columnsData: ColumnsType[];
rows: MixnodeRowType[];
}> = ({ tableName, columnsData, rows }: UniversalTableProps) => (
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label={tableName}>
<TableHead>
<TableRow>
{columnsData?.map(({ field, title, flex }) => (
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, flex }}>
{title}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{rows.map((eachRow) => (
<TableRow key={eachRow.id} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
{columnsData?.map((_, index) => (
<TableCell
key={_.title}
component="th"
scope="row"
variant="body"
sx={{
...cellStyles,
padding: 2,
width: 200,
fontSize: 14,
}}
data-testid={`${_.title.replace(/ /g, '-')}-value`}
>
{formatCellValues(eachRow[columnsData[index].field], columnsData[index].field)}
rows: MixnodeRowType[] | GatewayEnrichedRowType[];
}> = ({ tableName, columnsData, rows }: UniversalTableProps) => {
const theme = useTheme();
return (
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label={tableName}>
<TableHead>
<TableRow>
{columnsData?.map(({ field, title, flex, tooltipInfo }) => (
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, flex }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{tooltipInfo && (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Tooltip
title={tooltipInfo}
id={field}
placement="top-start"
textColor={theme.palette.nym.networkExplorer.tooltip.color}
bgColor={theme.palette.nym.networkExplorer.tooltip.background}
maxWidth={230}
arrow
/>
</Box>
)}
{title}
</Box>
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
</TableHead>
<TableBody>
{rows.map((eachRow) => (
<TableRow key={eachRow.id} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
{columnsData?.map((data, index) => (
<TableCell
key={data.title}
component="th"
scope="row"
variant="body"
sx={{
...cellStyles,
padding: 2,
width: 200,
fontSize: 14,
}}
data-testid={`${data.title.replace(/ /g, '-')}-value`}
>
{formatCellValues(eachRow[columnsData[index].field], columnsData[index].field)}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};
+28 -3
View File
@@ -1,23 +1,48 @@
import { GatewayResponse } from '../typeDefs/explorer-api';
import { GatewayResponse, GatewayResponseItem, GatewayReportResponse } from '../typeDefs/explorer-api';
export type GatewayRowType = {
id: string;
owner: string;
identity_key: string;
identityKey: string;
bond: number;
host: string;
location: string;
};
export type GatewayEnrichedRowType = GatewayRowType & {
routingScore: string;
avgUptime: string;
clientsPort: number;
mixPort: number;
};
export function gatewayToGridRow(arrayOfGateways: GatewayResponse): GatewayRowType[] {
return !arrayOfGateways
? []
: arrayOfGateways.map((gw) => ({
id: gw.owner,
owner: gw.owner,
identity_key: gw.gateway.identity_key || '',
identityKey: gw.gateway.identity_key || '',
location: gw?.gateway?.location || '',
bond: gw.pledge_amount.amount || 0,
host: gw.gateway.host || '',
}));
}
export function gatewayEnrichedToGridRow(
gateway: GatewayResponseItem,
report: GatewayReportResponse,
): GatewayEnrichedRowType {
return {
id: gateway.owner,
owner: gateway.owner,
identityKey: gateway.gateway.identity_key || '',
location: gateway?.gateway?.location || '',
bond: gateway.pledge_amount.amount || 0,
host: gateway.gateway.host || '',
clientsPort: gateway.gateway.clients_port || 0,
mixPort: gateway.gateway.mix_port || 0,
routingScore: `${report.most_recent}%`,
avgUptime: `${report.last_day || report.last_hour}%`,
};
}
@@ -6,14 +6,16 @@ export const EconomicsInfoColumns: ColumnsType[] = [
title: 'Estimated Total Reward',
flex: 1,
headerAlign: 'left',
tooltipInfo: 'Estimated reward per epoch for this profit margin if your node is selected in the active set.',
tooltipInfo:
'Estimated node reward (total for the operator and delegators) in the current epoch. There are roughly 24 epochs in a day.',
},
{
field: 'estimatedOperatorReward',
title: 'Estimated Operator Reward',
flex: 1,
headerAlign: 'left',
tooltipInfo: 'Estimated reward per epoch for this profit margin if your node is selected in the active set.',
tooltipInfo:
"Estimated operator's reward (including PM and Operating Cost) in the current epoch. There are roughly 24 epochs in a day.",
},
{
field: 'selectionChance',
@@ -29,7 +31,7 @@ export const EconomicsInfoColumns: ColumnsType[] = [
flex: 1,
headerAlign: 'left',
tooltipInfo:
'Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is: 1 million NYM, computed as S/K where S is total amount of tokens available to stakeholders and K is the number of nodes in the reward set.',
'Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is: 750k NYM, computed as S/K where S is total amount of tokens available to stakeholders and K is the number of nodes in the reward set.',
},
{
field: 'profitMargin',
@@ -37,7 +39,15 @@ export const EconomicsInfoColumns: ColumnsType[] = [
flex: 1,
headerAlign: 'left',
tooltipInfo:
'Percentage of the delegates rewards that the operator takes as fee before rewards are distributed to the delegates.',
'Percentage of the delegators rewards that the operator takes as fee before rewards are distributed to the delegators.',
},
{
field: 'operatingCost',
title: 'Operating Cost',
flex: 1,
headerAlign: 'left',
tooltipInfo:
'Monthly operational cost of running this node. This cost is set by the operator and it influences how the rewards are split between the operator and delegators.',
},
{
field: 'avgUptime',
@@ -26,6 +26,9 @@ const row: EconomicsInfoRowWithIndex = {
profitMargin: {
value: '10 %',
},
operatingCost: {
value: '11121 NYM',
},
stakeSaturation: {
value: '80 %',
progressBarValue: 80,
@@ -64,6 +67,9 @@ const emptyRow: EconomicsInfoRowWithIndex = {
profitMargin: {
value: '-',
},
operatingCost: {
value: '-',
},
stakeSaturation: {
value: '-',
progressBarValue: 0,
@@ -1,25 +1,11 @@
import { currencyToString } from '../../../utils/currency';
import { currencyToString, unymToNym } from '../../../utils/currency';
import { useMixnodeContext } from '../../../context/mixnode';
import { ApiState, MixNodeEconomicDynamicsStatsResponse } from '../../../typeDefs/explorer-api';
import { EconomicsInfoRowWithIndex } from './types';
import { toPercentIntegerString } from '../../../utils';
const selectionChance = (economicDynamicsStats: ApiState<MixNodeEconomicDynamicsStatsResponse> | undefined) => {
const inclusionProbability = economicDynamicsStats?.data?.active_set_inclusion_probability;
// TODO: when v2 will be deployed, remove cases: VeryHigh, Moderate and VeryLow
switch (inclusionProbability) {
case 'VeryLow':
return 'Very Low';
case 'VeryHigh':
return 'Very High';
case 'High':
case 'Good':
case 'Low':
case 'Moderate':
return inclusionProbability;
default:
return '-';
}
};
const selectionChance = (economicDynamicsStats: ApiState<MixNodeEconomicDynamicsStatsResponse> | undefined) =>
economicDynamicsStats?.data?.active_set_inclusion_probability || '-';
export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => {
const { economicDynamicsStats, mixNode } = useMixnodeContext();
@@ -28,10 +14,14 @@ export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => {
currencyToString((economicDynamicsStats?.data?.estimated_total_node_reward || '').toString()) || '-';
const estimatedOperatorRewards =
currencyToString((economicDynamicsStats?.data?.estimated_operator_reward || '').toString()) || '-';
const stakeSaturation = economicDynamicsStats?.data?.stake_saturation || '-';
const profitMargin = mixNode?.data?.mix_node.profit_margin_percent || '-';
const stakeSaturation = economicDynamicsStats?.data?.uncapped_saturation || '-';
const profitMargin = mixNode?.data?.profit_margin_percent
? toPercentIntegerString(mixNode?.data?.profit_margin_percent)
: '-';
const avgUptime = economicDynamicsStats?.data?.current_interval_uptime;
const opCost = mixNode?.data?.operating_cost;
return {
id: 1,
estimatedTotalReward: {
@@ -50,6 +40,9 @@ export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => {
profitMargin: {
value: profitMargin ? `${profitMargin} %` : '-',
},
operatingCost: {
value: opCost ? `${unymToNym(opCost.amount, 6)} NYM` : '-',
},
avgUptime: {
value: avgUptime ? `${avgUptime} %` : '-',
},
@@ -10,6 +10,7 @@ export interface EconomicsInfoRow {
stakeSaturation: EconomicsRowsType;
profitMargin: EconomicsRowsType;
avgUptime: EconomicsRowsType;
operatingCost: EconomicsRowsType;
}
export type EconomicsInfoRowWithIndex = EconomicsInfoRow & { id: number };
+10 -3
View File
@@ -1,7 +1,10 @@
/* eslint-disable camelcase */
import { MixNodeResponse, MixNodeResponseItem, MixnodeStatus } from '../../typeDefs/explorer-api';
import { toPercentIntegerString } from '../../utils';
import { unymToNym } from '../../utils/currency';
export type MixnodeRowType = {
mix_id: number;
id: string;
status: MixnodeStatus;
owner: string;
@@ -15,6 +18,7 @@ export type MixnodeRowType = {
profit_percentage: string;
avg_uptime: string;
stake_saturation: number;
operating_cost: string;
};
export function mixnodeToGridRow(arrayOfMixnodes?: MixNodeResponse): MixnodeRowType[] {
@@ -26,9 +30,11 @@ export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem):
const delegations = Number(item.total_delegation.amount) || 0;
const totalBond = pledge + delegations;
const selfPercentage = ((pledge * 100) / totalBond).toFixed(2);
const profitPercentage = item.mix_node.profit_margin_percent || 0;
const stakeSaturation = typeof item.stake_saturation === 'number' ? item.stake_saturation * 100 : 0;
const profitPercentage = toPercentIntegerString(item.profit_margin_percent) || 0;
const uncappedSaturation = typeof item.uncapped_saturation === 'number' ? item.uncapped_saturation * 100 : 0;
return {
mix_id: item.mix_id,
id: item.owner,
status: item.status,
owner: item.owner,
@@ -41,6 +47,7 @@ export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem):
layer: item?.layer || '',
profit_percentage: `${profitPercentage}%`,
avg_uptime: `${item.avg_uptime}%` || '-',
stake_saturation: stakeSaturation,
stake_saturation: uncappedSaturation,
operating_cost: `${unymToNym(item.operating_cost?.amount, 6)} NYM`,
};
}
+36
View File
@@ -0,0 +1,36 @@
import React, { ReactElement } from 'react';
import { Tooltip as MUITooltip, TooltipComponentsPropsOverrides, TooltipProps } from '@mui/material';
type ValueType<T> = T[keyof T];
type Props = {
text: string;
id: string;
placement?: ValueType<Pick<TooltipProps, 'placement'>>;
tooltipSx?: TooltipComponentsPropsOverrides;
children: React.ReactNode;
};
export const Tooltip = ({ text, id, placement, tooltipSx, children }: Props) => (
<MUITooltip
title={text}
id={id}
placement={placement || 'top-start'}
componentsProps={{
tooltip: {
sx: {
maxWidth: 200,
background: (t) => t.palette.nym.networkExplorer.tooltip.background,
color: (t) => t.palette.nym.networkExplorer.tooltip.color,
'& .MuiTooltip-arrow': {
color: (t) => t.palette.nym.networkExplorer.tooltip.background,
},
},
...tooltipSx,
},
}}
arrow
>
{children as ReactElement<any, any>}
</MUITooltip>
);
+59
View File
@@ -0,0 +1,59 @@
import * as React from 'react';
import { ApiState, GatewayReportResponse, UptimeStoryResponse } from '../typeDefs/explorer-api';
import { Api } from '../api';
import { useApiState } from './hooks';
/**
* This context provides the state for a single gateway by identity key.
*/
interface GatewayState {
uptimeReport?: ApiState<GatewayReportResponse>;
uptimeStory?: ApiState<UptimeStoryResponse>;
}
export const GatewayContext = React.createContext<GatewayState>({});
export const useGatewayContext = (): React.ContextType<typeof GatewayContext> =>
React.useContext<GatewayState>(GatewayContext);
/**
* Provides a state context for a gateway by identity
* @param gatewayIdentityKey The identity key of the gateway
*/
export const GatewayContextProvider = ({
gatewayIdentityKey,
children,
}: {
gatewayIdentityKey: string;
children: JSX.Element;
}) => {
const [uptimeReport, fetchUptimeReportById, clearUptimeReportById] = useApiState<GatewayReportResponse>(
gatewayIdentityKey,
Api.fetchGatewayReportById,
'Failed to fetch gateway uptime report by id',
);
const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] = useApiState<UptimeStoryResponse>(
gatewayIdentityKey,
Api.fetchGatewayUptimeStoryById,
'Failed to fetch gateway uptime history',
);
React.useEffect(() => {
// when the identity key changes, remove all previous data
clearUptimeReportById();
clearUptimeHistory();
Promise.all([fetchUptimeReportById(), fetchUptimeHistory()]);
}, [gatewayIdentityKey]);
const state = React.useMemo<GatewayState>(
() => ({
uptimeReport,
uptimeStory,
}),
[uptimeReport, uptimeStory],
);
return <GatewayContext.Provider value={state}>{children}</GatewayContext.Provider>;
};
+32 -15
View File
@@ -1,30 +1,47 @@
import * as React from 'react';
import { ApiState } from '../typeDefs/explorer-api';
type WrappedApiFn<T> = () => Promise<ApiState<T>>;
/**
* Custom hook to get data from the API by passing an id to a delegate method that fetches the data asynchronously
* @param id The id to fetch
* @param fn Delegate the fetching to this method (must take `(id: string)` as a parameter)
* @param errorMessage A static error message, to use when no dynamic error message is returned
*/
export const useApiState = <T>(
// eslint-disable-next-line @typescript-eslint/ban-types
fn: Function,
id: string,
fn: (argId: string) => Promise<T>,
errorMessage: string,
): [ApiState<T>, WrappedApiFn<T>] => {
): [ApiState<T> | undefined, () => Promise<ApiState<T>>, () => void] => {
// stores the state
const [value, setValue] = React.useState<ApiState<T>>();
const wrappedFn = React.useCallback(async () => {
// clear the value
const clearValueFn = () => setValue(undefined);
// this provides a method to trigger the delegate to fetch data
const wrappedFetchFn = React.useCallback(async () => {
setValue({ isLoading: true });
try {
// keep previous state and set to loading
setValue((prevState) => ({ ...prevState, isLoading: true }));
const data = await fn();
setValue({
// delegate to user function to get data and set if successful
const data = await fn(id);
const newValue: ApiState<T> = {
isLoading: false,
data,
});
return data;
};
setValue(newValue);
return newValue;
} catch (error) {
setValue({
// return the caught error or create a new error with the static error message
const newValue: ApiState<T> = {
error: error instanceof Error ? error : new Error(errorMessage),
isLoading: false,
});
return undefined;
};
setValue(newValue);
return newValue;
}
}, [setValue, fn]);
return [value || { isLoading: true }, wrappedFn];
}, [setValue, fn, id, errorMessage]);
return [value, wrappedFetchFn, clearValueFn];
};
+5 -2
View File
@@ -96,19 +96,22 @@ export const MainContextProvider: React.FC = ({ children }) => {
const filterMixnodes = async (filters: { [key in EnumFilterKey]: number[] }, status?: MixnodeStatus) => {
setMixnodes((d) => ({ ...d, isLoading: true }));
const mxns = status ? await Api.fetchMixnodesActiveSetByStatus(status) : await Api.fetchMixnodes();
const filtered = mxns?.filter(
(m) =>
m.mix_node.profit_margin_percent >= filters.profitMargin[0] &&
m.mix_node.profit_margin_percent <= filters.profitMargin[1] &&
+m.profit_margin_percent >= filters.profitMargin[0] / 100 &&
+m.profit_margin_percent <= filters.profitMargin[1] / 100 &&
m.stake_saturation >= filters.stakeSaturation[0] &&
m.stake_saturation <= filters.stakeSaturation[1] &&
m.avg_uptime >= filters.routingScore[0] &&
m.avg_uptime <= filters.routingScore[1],
);
setMixnodes({ data: filtered, isLoading: false });
};
const fetchGateways = async () => {
setGateways((d) => ({ ...d, isLoading: true }));
try {
const data = await Api.fetchGateways();
setGateways({ data, isLoading: false });
+1 -44
View File
@@ -11,6 +11,7 @@ import {
UptimeStoryResponse,
} from '../typeDefs/explorer-api';
import { Api } from '../api';
import { useApiState } from './hooks';
import { mixNodeResponseItemToMixnodeRowType, MixnodeRowType } from '../components/MixNodes';
/**
@@ -153,47 +154,3 @@ export const MixnodeContextProvider: React.FC<MixnodeContextProviderProps> = ({
return <MixnodeContext.Provider value={state}>{children}</MixnodeContext.Provider>;
};
/**
* Custom hook to get data from the API by passing an id to a delegate method that fetches the data asynchronously
* @param id The id to fetch
* @param fn Delegate the fetching to this method (must take `(id: string)` as a parameter)
* @param errorMessage A static error message, to use when no dynamic error message is returned
*/
function useApiState<T>(
id: string,
fn: (argId: string) => Promise<T>,
errorMessage: string,
): [ApiState<T> | undefined, () => Promise<ApiState<T>>, () => void] {
// stores the state
const [value, setValue] = React.useState<ApiState<T> | undefined>();
// clear the value
const clearValueFn = () => setValue(undefined);
// this provides a method to trigger the delegate to fetch data
const wrappedFetchFn = React.useCallback(async () => {
try {
// keep previous state and set to loading
setValue((prevState) => ({ ...prevState, isLoading: true }));
// delegate to user function to get data and set if successful
const data = await fn(id);
const newValue: ApiState<T> = {
isLoading: false,
data,
};
setValue(newValue);
return newValue;
} catch (error) {
// return the caught error or create a new error with the static error message
const newValue: ApiState<T> = {
error: error instanceof Error ? error : new Error(errorMessage),
isLoading: false,
};
setValue(newValue);
return newValue;
}
}, [setValue, fn]);
return [value || { isLoading: true }, wrappedFetchFn, clearValueFn];
}
+179
View File
@@ -0,0 +1,179 @@
import * as React from 'react';
import { Alert, AlertTitle, Box, CircularProgress, Grid } from '@mui/material';
import { useParams } from 'react-router-dom';
import { GatewayResponseItem } from '../../typeDefs/explorer-api';
import { ColumnsType, DetailTable } from '../../components/DetailTable';
import { gatewayEnrichedToGridRow, GatewayEnrichedRowType } from '../../components/Gateways';
import { ComponentError } from '../../components/ComponentError';
import { ContentCard } from '../../components/ContentCard';
import { TwoColSmallTable } from '../../components/TwoColSmallTable';
import { UptimeChart } from '../../components/UptimeChart';
import { GatewayContextProvider, useGatewayContext } from '../../context/gateway';
import { useMainContext } from '../../context/main';
import { Title } from '../../components/Title';
const columns: ColumnsType[] = [
{
field: 'identityKey',
title: 'Identity Key',
headerAlign: 'left',
width: 230,
},
{
field: 'bond',
title: 'Bond',
flex: 1,
headerAlign: 'left',
},
{
field: 'routingScore',
title: 'Routing Score',
flex: 1,
headerAlign: 'left',
tooltipInfo:
'Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test.',
},
{
field: 'avgUptime',
title: 'Avg. Score',
flex: 1,
headerAlign: 'left',
tooltipInfo: 'Is the average routing score in the last 24 hours',
},
{
field: 'host',
title: 'IP',
headerAlign: 'left',
width: 99,
},
{
field: 'location',
title: 'Location',
headerAlign: 'left',
flex: 1,
},
{
field: 'owner',
title: 'Owner',
headerAlign: 'left',
flex: 1,
},
];
/**
* Shows gateway details
*/
const PageGatewayDetailsWithState = ({ selectedGateway }: { selectedGateway: GatewayResponseItem | undefined }) => {
const [enrichGateway, setEnrichGateway] = React.useState<GatewayEnrichedRowType>();
const [status, setStatus] = React.useState<number[] | undefined>();
const { uptimeReport, uptimeStory } = useGatewayContext();
React.useEffect(() => {
if (uptimeReport?.data && selectedGateway) {
setEnrichGateway(gatewayEnrichedToGridRow(selectedGateway, uptimeReport.data));
}
}, [uptimeReport, selectedGateway]);
React.useEffect(() => {
if (enrichGateway) {
setStatus([enrichGateway.mixPort, enrichGateway.clientsPort]);
}
}, [enrichGateway]);
return (
<Box component="main">
<Title text="Gateway Detail" />
<Grid container>
<Grid item xs={12}>
<DetailTable
columnsData={columns}
tableName="Gateway detail table"
rows={enrichGateway ? [enrichGateway] : []}
/>
</Grid>
</Grid>
<Grid container spacing={2} mt={0}>
<Grid item xs={12} md={4}>
{status && (
<ContentCard title="Gateway Status">
<TwoColSmallTable
loading={false}
keys={['Mix port', 'Client WS API Port']}
values={status.map((each) => each)}
icons={status.map((elem) => !!elem)}
/>
</ContentCard>
)}
</Grid>
<Grid item xs={12} md={8}>
{uptimeStory && (
<ContentCard title="Routing Score">
{uptimeStory.error && <ComponentError text="There was a problem retrieving routing score." />}
<UptimeChart loading={uptimeStory.isLoading} xLabel="date" uptimeStory={uptimeStory} />
</ContentCard>
)}
</Grid>
</Grid>
</Box>
);
};
/**
* Guard component to handle loading and not found states
*/
const PageGatewayDetailGuard: React.FC = () => {
const [selectedGateway, setSelectedGateway] = React.useState<GatewayResponseItem | undefined>();
const { gateways } = useMainContext();
const { id } = useParams<{ id: string | undefined }>();
React.useEffect(() => {
if (gateways?.data) {
setSelectedGateway(gateways.data.find((gateway) => gateway.gateway.identity_key === id));
}
}, [gateways, id]);
if (gateways?.isLoading) {
return <CircularProgress />;
}
if (gateways?.error) {
// eslint-disable-next-line no-console
console.error(gateways?.error);
return (
<Alert severity="error">
Oh no! Could not load mixnode <code>{id || ''}</code>
</Alert>
);
}
// loaded, but not found
if (gateways && !gateways.isLoading && !gateways.data) {
return (
<Alert severity="warning">
<AlertTitle>Gateway not found</AlertTitle>
Sorry, we could not find a mixnode with id <code>{id || ''}</code>
</Alert>
);
}
return <PageGatewayDetailsWithState selectedGateway={selectedGateway} />;
};
/**
* Wrapper component that adds the mixnode content based on the `id` in the address URL
*/
export const PageGatewayDetail: React.FC = () => {
const { id } = useParams<{ id: string | undefined }>();
if (!id) {
return <Alert severity="error">Oh no! No mixnode identity key specified</Alert>;
}
return (
<GatewayContextProvider gatewayIdentityKey={id}>
<PageGatewayDetailGuard />
</GatewayContextProvider>
);
};
+63 -25
View File
@@ -1,5 +1,6 @@
import * as React from 'react';
import { Button, Card, Grid, Typography } from '@mui/material';
import { Link as RRDLink } from 'react-router-dom';
import { Box, Button, Card, Grid, Link as MuiLink } from '@mui/material';
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
import { SelectChangeEvent } from '@mui/material/Select';
import { useMainContext } from '../../context/main';
@@ -10,6 +11,9 @@ import { CustomColumnHeading } from '../../components/CustomColumnHeading';
import { Title } from '../../components/Title';
import { cellStyles, UniversalDataGrid } from '../../components/Universal-DataGrid';
import { currencyToString } from '../../utils/currency';
import { Tooltip } from '../../components/Tooltip';
import { BIG_DIPPER } from '../../api/constants';
import { splice } from '../../utils';
export const PageGateways: React.FC = () => {
const { gateways } = useMainContext();
@@ -43,29 +47,20 @@ export const PageGateways: React.FC = () => {
const columns: GridColDef[] = [
{
field: 'owner',
headerName: 'Owner',
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
width: 380,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<Typography sx={cellStyles} data-testid="owner">
{params.value}
</Typography>
),
},
{
field: 'identity_key',
field: 'identityKey',
headerName: 'Identity Key',
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
headerClassName: 'MuiDataGrid-header-override',
width: 380,
headerAlign: 'left',
renderCell: (params: GridRenderCellParams) => (
<Typography sx={cellStyles} data-testid="identity-key">
<MuiLink
sx={{ ...cellStyles }}
component={RRDLink}
to={`/network-components/gateway/${params.row.identityKey}`}
>
{params.value}
</Typography>
</MuiLink>
),
},
{
@@ -76,27 +71,37 @@ export const PageGateways: React.FC = () => {
headerClassName: 'MuiDataGrid-header-override',
headerAlign: 'left',
renderCell: (params: GridRenderCellParams) => (
<Typography sx={cellStyles} data-testid="pledge-amount">
<MuiLink
sx={{ ...cellStyles }}
component={RRDLink}
to={`/network-components/gateway/${params.row.identityKey}`}
data-testid="pledge-amount"
>
{currencyToString(params.value)}
</Typography>
</MuiLink>
),
},
{
field: 'host',
renderHeader: () => <CustomColumnHeading headingTitle="IP:Port" />,
width: 110,
width: 180,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<Typography sx={cellStyles} data-testid="host">
<MuiLink
sx={{ ...cellStyles }}
component={RRDLink}
to={`/network-components/gateway/${params.row.identityKey}`}
data-testid="host"
>
{params.value}
</Typography>
</MuiLink>
),
},
{
field: 'location',
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
width: 150,
width: 180,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
@@ -105,10 +110,38 @@ export const PageGateways: React.FC = () => {
sx={{ ...cellStyles, justifyContent: 'flex-start' }}
data-testid="location-button"
>
{params.value}
<Tooltip text={params.value} id="gateway-location-text">
<Box
sx={{
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
>
{params.value}
</Box>
</Tooltip>
</Button>
),
},
{
field: 'owner',
headerName: 'Owner',
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
width: 380,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
sx={{ ...cellStyles }}
href={`${BIG_DIPPER}/account/${params.value}`}
target="_blank"
data-testid="owner"
>
{splice(7, 29, params.value)}
</MuiLink>
),
},
];
const handlePageSize = (event: SelectChangeEvent<string>) => {
@@ -133,7 +166,12 @@ export const PageGateways: React.FC = () => {
pageSize={pageSize}
searchTerm={searchTerm}
/>
<UniversalDataGrid rows={gatewayToGridRow(filteredGateways)} columns={columns} pageSize={pageSize} />
<UniversalDataGrid
pagination
rows={gatewayToGridRow(filteredGateways)}
columns={columns}
pageSize={pageSize}
/>
</Card>
</Grid>
</Grid>
+1 -1
View File
@@ -35,7 +35,7 @@ const columns: ColumnsType[] = [
},
{
field: 'self_percentage',
title: 'Self %',
title: 'Bond %',
headerAlign: 'left',
width: 99,
},
+62 -11
View File
@@ -1,6 +1,6 @@
import * as React from 'react';
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
import { Button, Card, Grid, Link as MuiLink } from '@mui/material';
import { Button, Card, Grid, Link as MuiLink, Box } from '@mui/material';
import { Link as RRDLink, useParams, useNavigate } from 'react-router-dom';
import { SelectChangeEvent } from '@mui/material/Select';
import { SxProps } from '@mui/system';
@@ -18,6 +18,7 @@ import { currencyToString } from '../../utils/currency';
import { splice } from '../../utils';
import { getMixNodeStatusColor } from '../../components/MixNodes/Status';
import { MixNodeStatusDropdown } from '../../components/MixNodes/StatusDropdown';
import { Tooltip } from '../../components/Tooltip';
const getCellFontStyle = (theme: Theme, row: MixnodeRowType, textColor?: string) => {
const color = textColor || getMixNodeStatusColor(theme, row.status);
@@ -81,6 +82,24 @@ export const PageMixnodes: React.FC = () => {
};
const columns: GridColDef[] = [
{
field: 'mix_id',
headerName: 'Mix ID',
renderHeader: () => <CustomColumnHeading headingTitle="Mix ID" />,
headerClassName: 'MuiDataGrid-header-override',
width: 100,
headerAlign: 'left',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
sx={getCellStyles(theme, params.row)}
component={RRDLink}
to={`/network-components/mixnode/${params.value}`}
data-testid="mix-id"
>
{params.value}
</MuiLink>
),
},
{
field: 'identity_key',
headerName: 'Identity Key',
@@ -98,7 +117,7 @@ export const PageMixnodes: React.FC = () => {
<MuiLink
sx={getCellStyles(theme, params.row)}
component={RRDLink}
to={`/network-components/mixnode/${params.value}`}
to={`/network-components/mixnode/${params.row.mix_id}`}
data-testid="identity-link"
>
{splice(7, 29, params.value)}
@@ -118,7 +137,7 @@ export const PageMixnodes: React.FC = () => {
<MuiLink
sx={getCellStyles(theme, params.row)}
component={RRDLink}
to={`/network-components/mixnode/${params.row.identity_key}`}
to={`/network-components/mixnode/${params.row.mix_id}`}
>
{currencyToString(params.value)}
</MuiLink>
@@ -130,7 +149,7 @@ export const PageMixnodes: React.FC = () => {
renderHeader: () => (
<CustomColumnHeading
headingTitle="Stake Saturation"
tooltipInfo="Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is: 1 million NYM, computed as S/K where S is total amount of tokens available to stakeholders and K is the number of nodes in the reward set."
tooltipInfo="Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is: 750k NYM, computed as S/K where S is total amount of tokens available to stakeholders and K is the number of nodes in the reward set."
/>
),
headerClassName: 'MuiDataGrid-header-override',
@@ -143,7 +162,7 @@ export const PageMixnodes: React.FC = () => {
...getCellStyles(theme, params.row, params.value > 100 ? 'theme.palette.warning.main' : undefined),
}}
component={RRDLink}
to={`/network-components/mixnode/${params.row.identity_key}`}
to={`/network-components/mixnode/${params.row.mix_id}`}
>
{`${params.value.toFixed(2)} %`}
</MuiLink>
@@ -161,7 +180,7 @@ export const PageMixnodes: React.FC = () => {
<MuiLink
sx={getCellStyles(theme, params.row)}
component={RRDLink}
to={`/network-components/mixnode/${params.row.identity_key}`}
to={`/network-components/mixnode/${params.row.mix_id}`}
>
{currencyToString(params.value)}
</MuiLink>
@@ -173,7 +192,7 @@ export const PageMixnodes: React.FC = () => {
renderHeader: () => (
<CustomColumnHeading
headingTitle="Profit Margin"
tooltipInfo="Percentage of the delegates rewards that the operator takes as fee before rewards are distributed to the delegates."
tooltipInfo="Percentage of the delegators rewards that the operator takes as fee before rewards are distributed to the delegators."
/>
),
headerClassName: 'MuiDataGrid-header-override',
@@ -183,7 +202,29 @@ export const PageMixnodes: React.FC = () => {
<MuiLink
sx={{ ...getCellStyles(theme, params.row), textAlign: 'left' }}
component={RRDLink}
to={`/network-components/mixnode/${params.row.identity_key}`}
to={`/network-components/mixnode/${params.row.mix_id}`}
>
{params.value}
</MuiLink>
),
},
{
field: 'operating_cost',
headerName: 'Operating Cost',
renderHeader: () => (
<CustomColumnHeading
headingTitle="Operating Cost"
tooltipInfo="Monthly operational cost of running this node. This cost is set by the operator and it influences how the rewards are split between the operator and delegators."
/>
),
headerClassName: 'MuiDataGrid-header-override',
width: 170,
headerAlign: 'left',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
sx={{ ...getCellStyles(theme, params.row), textAlign: 'left' }}
component={RRDLink}
to={`/network-components/mixnode/${params.row.mix_id}`}
>
{params.value}
</MuiLink>
@@ -205,7 +246,7 @@ export const PageMixnodes: React.FC = () => {
<MuiLink
sx={{ ...getCellStyles(theme, params.row), textAlign: 'left' }}
component={RRDLink}
to={`/network-components/mixnode/${params.row.identity_key}`}
to={`/network-components/mixnode/${params.row.mix_id}`}
>
{params.value}
</MuiLink>
@@ -244,7 +285,17 @@ export const PageMixnodes: React.FC = () => {
justifyContent: 'flex-start',
}}
>
{params.value}
<Tooltip text={params.value} id="mixnode-location-text">
<Box
sx={{
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
>
{params.value}
</Box>
</Tooltip>
</Button>
),
},
@@ -259,7 +310,7 @@ export const PageMixnodes: React.FC = () => {
<MuiLink
sx={getCellStyles(theme, params.row)}
component={RRDLink}
to={`/network-components/mixnode/${params.row.identity_key}`}
to={`/network-components/mixnode/${params.row.mix_id}`}
>
{params.value}
</MuiLink>
@@ -2,6 +2,7 @@ import * as React from 'react';
import { Routes as ReactRouterRoutes, Route, useNavigate } from 'react-router-dom';
import { BIG_DIPPER } from '../api/constants';
import { PageGateways } from '../pages/Gateways';
import { PageGatewayDetail } from '../pages/GatewayDetail';
import { PageMixnodeDetail } from '../pages/MixnodeDetail';
import { PageMixnodes } from '../pages/Mixnodes';
@@ -18,6 +19,7 @@ export const NetworkComponentsRoutes: React.FC = () => (
<Route path="mixnodes" element={<PageMixnodes />} />
<Route path="mixnode/:id" element={<PageMixnodeDetail />} />
<Route path="gateways" element={<PageGateways />} />
<Route path="gateway/:id" element={<PageGatewayDetail />} />
<Route path="validators" element={<ValidatorRoute />} />
<Route path="gateways/:id" element={<h1> Specific Gateways ID</h1>} />
</ReactRouterRoutes>
+9 -8
View File
@@ -30,7 +30,6 @@ export interface MixNode {
sphinx_key: string;
identity_key: string;
version: string;
profit_margin_percent: number;
location: string;
}
@@ -70,6 +69,7 @@ export const toMixnodeStatus = (status?: MixnodeStatusWithAll): MixnodeStatus |
};
export interface MixNodeResponseItem {
mix_id: number;
pledge_amount: Amount;
total_delegation: Amount;
owner: string;
@@ -85,6 +85,9 @@ export interface MixNodeResponseItem {
mix_node: MixNode;
avg_uptime: number;
stake_saturation: number;
uncapped_saturation: number;
operating_cost: Amount;
profit_margin_percent: string;
}
export type MixNodeResponse = MixNodeResponseItem[];
@@ -126,12 +129,9 @@ export type GatewayResponse = GatewayResponseItem[];
export interface GatewayReportResponse {
identity: string;
owner: string;
most_recent_ipv4: boolean;
most_recent_ipv6: boolean;
last_hour_ipv4: number;
last_hour_ipv6: number;
last_day_ipv4: number;
last_day_ipv6: number;
most_recent: number;
last_hour: number;
last_day: number;
}
export type GatewayHistoryResponse = StatsResponse;
@@ -215,8 +215,9 @@ export type UptimeStoryResponse = {
export type MixNodeEconomicDynamicsStatsResponse = {
stake_saturation: number;
uncapped_saturation: number;
// TODO: when v2 will be deployed, remove cases: VeryHigh, Moderate and VeryLow
active_set_inclusion_probability: 'High' | 'Good' | 'Low' | 'VeryLow' | 'Moderate' | 'VeryHigh';
active_set_inclusion_probability: 'High' | 'Good' | 'Low';
reserve_set_inclusion_probability: 'High' | 'Good' | 'Low';
estimated_total_node_reward: number;
estimated_operator_reward: number;
+36
View File
@@ -1,4 +1,5 @@
import { printableCoin } from '@nymproject/nym-validator-client';
import Big from 'big.js';
const DENOM = process.env.CURRENCY_DENOM || 'unym';
const DENOM_STAKING = process.env.CURRENCY_STAKING_DENOM || 'unyx';
@@ -14,3 +15,38 @@ export const stakingCurrencyToString = (amount: string, denom: string = DENOM_ST
amount,
denom,
});
/**
* Converts a decimal number to a pretty representation
* with fixed decimal places.
*
* @param val - a decimal number of string form
* @param dp - number of decimal places (4 by default ie. 0.0000)
* @returns A prettyfied decimal number
*/
export const toDisplay = (val: string | number | Big, dp = 4) => {
let displayValue;
try {
displayValue = Big(val).toFixed(dp);
} catch (e: any) {
console.warn(`${displayValue} not a valid decimal number: ${e}`);
}
return displayValue;
};
/**
* Converts a decimal number of μNYM (micro NYM) to NYM.
*
* @param unym - a decimal number of μNYM
* @param dp - number of decimal places (4 by default ie. 0.0000)
* @returns The corresponding decimal number in NYM
*/
export const unymToNym = (unym: string | number | Big, dp = 4) => {
let nym;
try {
nym = Big(unym).div(1_000_000).toFixed(dp);
} catch (e: any) {
console.warn(`${unym} not a valid decimal number: ${e}`);
}
return nym;
};
+8
View File
@@ -45,3 +45,11 @@ export const splice = (start: number, deleteCount: number, address?: string): st
}
return '';
};
/**
* Converts a stringified percentage float (0.0-1.0) to a stringified integer (0-100).
*
* @param value - the percentage to convert
* @returns A stringified integer
*/
export const toPercentIntegerString = (value: string) => Math.round(Number(value) * 100).toString();
+2 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-gateway"
version = "1.0.2"
version = "1.1.0"
authors = [
"Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
"Jędrzej Stuczyński <andrew@nymtech.net>",
@@ -56,6 +56,7 @@ credentials = { path = "../common/credentials" }
config = { path = "../common/config" }
crypto = { path = "../common/crypto" }
completions = { path = "../common/completions" }
logging = { path = "../common/logging"}
gateway-requests = { path = "gateway-requests" }
mixnet-client = { path = "../common/client-libs/mixnet-client" }
mixnode-common = { path = "../common/mixnode-common" }
+4 -2
View File
@@ -82,8 +82,10 @@ impl fmt::Display for GatewayRequestsError {
RequestOfInvalidSize(actual) =>
write!(
f,
"received request had invalid size. (actual: {}, but expected one of: {} (ACK), {} (REGULAR), {} (EXTENDED))",
actual, PacketSize::AckPacket.size(), PacketSize::RegularPacket.size(), PacketSize::ExtendedPacket.size()
"received request had invalid size. (actual: {}, but expected one of: {} (ACK), {} (REGULAR), {}, {}, {} (EXTENDED))",
actual, PacketSize::AckPacket.size(), PacketSize::RegularPacket.size(),
PacketSize::ExtendedPacket8.size(), PacketSize::ExtendedPacket16.size(),
PacketSize::ExtendedPacket32.size()
),
MalformedSphinxPacket => write!(f, "received sphinx packet was malformed"),
MalformedEncryption => write!(f, "the received encrypted data was malformed"),
+12
View File
@@ -289,6 +289,10 @@ impl Config {
self.debug.maximum_connection_buffer_size
}
pub fn get_use_legacy_sphinx_framing(&self) -> bool {
self.debug.use_legacy_framed_packet_version
}
pub fn get_message_retrieval_limit(&self) -> i64 {
self.debug.message_retrieval_limit
}
@@ -456,6 +460,12 @@ struct Debug {
/// Number of messages from offline client that can be pulled at once from the storage.
message_retrieval_limit: i64,
/// Specifies whether the mixnode should be using the legacy framing for the sphinx packets.
// it's set to true by default. The reason for that decision is to preserve compatibility with the
// existing nodes whilst everyone else is upgrading and getting the code for handling the new field.
// It shall be disabled in the subsequent releases.
use_legacy_framed_packet_version: bool,
}
impl Default for Debug {
@@ -468,6 +478,8 @@ impl Default for Debug {
maximum_connection_buffer_size: DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE,
stored_messages_filename_length: DEFAULT_STORED_MESSAGE_FILENAME_LENGTH,
message_retrieval_limit: DEFAULT_MESSAGE_RETRIEVAL_LIMIT,
// TODO: remember to change it in one of future releases!!
use_legacy_framed_packet_version: true,
}
}
}
+1 -21
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use clap::{crate_version, Parser};
use logging::setup_logging;
use network_defaults::setup_env;
use once_cell::sync::OnceCell;
@@ -88,27 +89,6 @@ fn long_version() -> String {
)
}
fn setup_logging() {
let mut log_builder = pretty_env_logger::formatted_timed_builder();
if let Ok(s) = ::std::env::var("RUST_LOG") {
log_builder.parse_filters(&s);
} else {
// default to 'Info'
log_builder.filter(None, log::LevelFilter::Info);
}
log_builder
.filter_module("hyper", log::LevelFilter::Warn)
.filter_module("tokio_reactor", log::LevelFilter::Warn)
.filter_module("reqwest", log::LevelFilter::Warn)
.filter_module("mio", log::LevelFilter::Warn)
.filter_module("want", log::LevelFilter::Warn)
.filter_module("sled", log::LevelFilter::Warn)
.filter_module("tungstenite", log::LevelFilter::Warn)
.filter_module("tokio_tungstenite", log::LevelFilter::Warn)
.init();
}
#[cfg(test)]
mod tests {
use super::*;

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