Compare commits

...

179 Commits

Author SHA1 Message Date
durch 84e7bbd8ea Expose transfer ownership to rust client 2022-11-21 10:43:56 +01:00
Mark Sinclair e30fd270a1 Publish @nymproject/sdk v1.1.4 2022-11-18 15:32:57 +00:00
Mark Sinclair e3cda93919 WASM client: update crate version so that the client works with mainnet 2022-11-18 13:06:03 +00:00
benedetta davico 87fb4daeda Release/v1.1.1 nym wallet (#1775)
* fix undelegating with vesting tokens

* update version number

* update tauri conf version

* fix(wallet): explorer links

* refactor(explorer): rename mixnodeidentitykey to mixid

* fix(wallet): broken explorer links

Co-authored-by: fmtabbara <fmtabbara@hotmail.co.uk>
Co-authored-by: pierre <dommerc.pierre@gmail.com>
2022-11-18 12:38:16 +00:00
Dave Hrycyszyn 9f56796bf6 Removing unused nym-chitchat experiment 2022-11-18 12:28:21 +00:00
Jon Häggblad 09b51226c2 client: add LaneQueueLength to ServerResponse (#1772)
* client: add LaneQueueLength to ServerResponse

* fix test compilation

* changlog: add note
2022-11-18 10:24:28 +01:00
Jędrzej Stuczyński 6946151b25 Made vesting_contract_address argument peroperly optional (#1769)
* Made vesting_contract_address argument peroperly optional

* Updated changelog
2022-11-17 17:13:58 +00:00
Jon Häggblad a4aee465fa Merge pull request #1758 from nymtech/jon/fix/deserialize-length-check
Fix deserialize length check
2022-11-17 16:09:54 +01:00
Jon Häggblad 0d71ac5e75 websocket-requests: fix lints in file 2022-11-17 15:32:12 +01:00
Jon Häggblad ce14e40968 websocket-requests: fix deserialize length check 2022-11-17 15:32:12 +01:00
Pierre Dommerc d108edb424 fix(nym-cli): typo in sign command json output (#1768) 2022-11-17 15:15:45 +01:00
Tommy Verrall 18f5623b05 Merge pull request #1759 from nymtech/feature/mixnet-contract-migratemsg
Added migration code for updating vesting contract address
2022-11-17 12:48:57 +01:00
Mark Sinclair 8e92801929 Reduce logging in explorer api 2022-11-16 16:45:59 +00:00
Gala 3fc1bc4e7c Merge pull request #1764 from nymtech/no-display-maintenance
Update banner
2022-11-16 17:25:40 +01:00
Gala 72de726762 another text update 2022-11-16 16:08:47 +00:00
Gala cb9dfa8188 update banner text 2022-11-16 16:03:27 +00:00
Mark Sinclair f102ed53a7 Merge remote-tracking branch 'origin/release/v1.1.0' into develop
# Conflicts:
#	.github/workflows/network-explorer-api.yml
2022-11-16 15:54:08 +00:00
Raphaël Walther a803c7f25e Set output 2022-11-16 16:06:03 +01:00
Gala f20b620cbb turn maintenance banner display false 2022-11-16 15:04:24 +00:00
Raphaël Walther d771d15959 Corrected reference 2022-11-16 15:50:53 +01:00
Raphaël Walther 49e6f387ff Hardcoded branch name 2022-11-16 15:12:50 +01:00
Raphaël Walther 9568c0ba1d Upgraded checkout action 2022-11-16 14:50:46 +01:00
Gala 0b7b705e56 Merge pull request #1602 from nymtech/327-nym-connect-colours
Nym Connect: Various ui updates
2022-11-16 14:30:41 +01:00
Raphaël Walther 5daea675e7 Sent errors through the pipe 2022-11-16 14:28:45 +01:00
Gala ebd18586a8 Merge branch '327-nym-connect-colours' of github.com:nymtech/nym into 327-nym-connect-colours 2022-11-16 13:14:40 +00:00
Gala 585610295f Merge branch 'develop' into 327-nym-connect-colours 2022-11-16 13:14:17 +00:00
Raphaël Walther 91653d13c6 Added echo 2022-11-16 14:03:34 +01:00
Mark Sinclair b84486c0f4 GitHub Actions: install packages needed for build 2022-11-16 11:56:26 +00:00
Mark Sinclair b6a765481a GitHub Actions: add workflow to build network explorer api 2022-11-16 11:43:53 +00:00
Mark Sinclair 8f52f34bc4 GitHub Actions: add workflow to build network explorer api 2022-11-16 11:43:17 +00:00
Raphaël Walther 39798de1e8 Fixed dependency 2022-11-16 11:40:28 +01:00
Raphaël Walther c650587e4c Fixed typo 2022-11-16 11:35:06 +01:00
Raphaël Walther 660d5d8b05 Added missing property 2022-11-16 11:18:12 +01:00
Raphaël Walther 79f9db91ae Fixed typo 2022-11-16 11:16:07 +01:00
Raphaël Walther 43822f27a8 Switched to job outputs 2022-11-16 11:13:36 +01:00
Raphaël Walther e500d154dd Fixed issue with environment variable 2022-11-16 10:37:28 +01:00
Raphaël Walther 3ceb00fae1 Added matrix 2022-11-16 10:24:49 +01:00
Raphaël Walther d019343fd9 Added nightly build on latest release 2022-11-16 10:10:59 +01:00
Raphaël Walther f55a55b784 Cleaned workflows 2022-11-16 10:10:14 +01:00
Raphaël Walther 0ea8da79c8 Enabled yanked crates warning 2022-11-16 09:24:58 +01:00
Jon Häggblad 0e12251773 Update some deps suggested by cargo deny (#1761)
* Update yanked cpufeatures dependency

* Update yanked textwrap version

* Updated yanked crossbeam-channel version

* Update client-core dep to 1.1.0
2022-11-16 09:10:40 +01:00
Jon Häggblad f886326014 wallet_storage: fix clippy (#1757) 2022-11-16 07:43:49 +01:00
Jess c73c2beb33 Update CHANGELOG.md 2022-11-15 19:04:38 +00:00
benedettadavico b7aa84cd5a updating mainnet mixnet conract address 2022-11-15 19:54:28 +01:00
Raphaël Walther b6b40163c6 Added security audit for whole tree 2022-11-15 17:43:51 +01:00
Jędrzej Stuczyński f46c0142e7 Updated changelog 2022-11-15 16:28:15 +00:00
Jędrzej Stuczyński 8774b22d84 Added migration code for updating vesting contract address 2022-11-15 16:06:03 +00:00
Mark Sinclair c74a880838 Update README for SDK 2022-11-14 15:58:00 +00:00
Mark Sinclair ccbb254b1a Add wildcard glob for all files to SDK npm package.json 2022-11-14 15:36:30 +00:00
Dave Hrycyszyn 2bd0cfc870 Moving towards a publishable npm sdk package 2022-11-14 15:08:58 +00:00
Gala aeaf31ed59 Merge pull request #1756 from nymtech/feature-457-banner
Feature 457 banner
2022-11-14 14:04:07 +01:00
Gala 05820cfca7 mantenance banner text update 2022-11-14 12:58:49 +00:00
Gala 0469d5b602 adding a comment and different alignment 2022-11-11 11:53:49 +01:00
Jon Häggblad d912844543 Client: multiplex connection data streams (#1720)
* WIP: QA network details

* Initial implementation to multiplex socks5-client sends

* Introduce TransmissionLane enum

* WIP

* WIP: client requests connection id

* WIP

* mulitplex somewhat done

* Remove closed lanes

* WIP: connection handling over ws

* Remove unused published active connections shared data

* Start on status timer

* Max number of connections, and prune

* Some tidy

* Remove commented out code and tweak log

* Tidy

* Tweak log output

* Rename to TransmissionBuffer

* Use number of msg sent instead of time to rank age of lanes

* Create client-connections crate

* Remove waker call that probably are not needed

* Extract out some types from real traffic stream module

* Revert to develop qa.env

* Tweak comments, tidy for getting ready to merge

* Update changelog

* wasm client compile fixes

* rustfmt
2022-11-11 11:04:49 +01:00
Gala 64757ebc83 adding missing spaces 2022-11-11 07:16:13 +01:00
Gala ba55affe0a reducing ne banner height 2022-11-10 20:43:09 +01:00
Gala e9f826e705 adding a mintenance banner 2022-11-10 20:11:59 +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
Mark Sinclair b68fb4f5dd Merge branch 'release/v1.1.0' into develop 2022-11-10 15:06:36 +00:00
Dave Hrycyszyn 7461fe88d0 Merge remote-tracking branch 'origin/feature/nym-sdk' into develop 2022-11-10 14:09:49 +00:00
Jon Häggblad 510d0333a1 Add -c for --config-env-file (#1748)
* Add -c for --config-env-file

* changelog: add note
2022-11-10 13:38:55 +01:00
Jon Häggblad cea4887080 Fix clippy in beta toolchain (#1749) 2022-11-10 13:38:40 +01:00
Jon Häggblad 8f1cb67bf7 Update client-core to 1.1.0 2022-11-10 09:44:20 +01: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 89925e49e8 GH Actions: Install same dependencies as build..yml 2022-11-09 18:20:45 +00:00
Mark Sinclair 96fc7208a2 Merge branch 'release/v1.1.0' into develop 2022-11-09 15:27:56 +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 b8cb683da0 Add qwerty qa net env file 2022-11-09 11:27:28 +01: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
Jon Häggblad d79c25861b Merge pull request #1740 from nymtech/develop-with-release-1.1.0-merged-in
Merge release/v1.1.0 into develop
2022-11-09 11:02:12 +01:00
Jon Häggblad b89ec2e0be clippy 2022-11-09 10:43:53 +01:00
Jon Häggblad 269f50bdd4 rustfmt 2022-11-09 10:25:39 +01:00
durch e3c02dc80a Actually save updated pledge cap 2022-11-09 10:22:14 +01:00
Drazen Urch 65a9320d35 Set default pledge cap to 10% (#1739)
* Set default pledge cap to 10%

* fix clippy beta lints
2022-11-09 10:22:08 +01:00
durch cf268ffcd5 Actually save updated pledge cap 2022-11-09 10:19:21 +01:00
Mark Sinclair bdcfe42a1e Add docs and diagrams 2022-11-08 17:47:10 +00:00
Mark Sinclair 51e30b2a89 Add "attach file" to chat demo
Uses custom binary payload, that includes headers with the file and mime-type, so the receiving browser can save the file correctly.
2022-11-08 17:47:10 +00:00
Mark Sinclair 76d4d0e7cb Add makefile 2022-11-08 17:47:10 +00:00
Mark Sinclair 9df432b8a2 Add arbitrary text headers to binary payload helpers 2022-11-08 17:47:10 +00:00
Mark Sinclair 4021059e76 Send all chat messages with new payload 2022-11-08 17:47:10 +00:00
Mark Sinclair 43ef098aad Add utilities to handle text and binary payloads for mixnet messages 2022-11-08 17:47:10 +00:00
Mark Sinclair bf1d2a12bc Fix html file 2022-11-08 17:47:10 +00:00
Mark Sinclair c3f214ffad Change to sync build 2022-11-08 17:47:10 +00:00
Mark Sinclair 324fb6afe7 Build SDK in dependencies 2022-11-08 17:47:10 +00:00
Mark Sinclair ace020b5cf Upgrade favicons (to upgrade sharp) 2022-11-08 17:47:10 +00:00
Mark Sinclair 4b8fa4805e Tweak dependency build script 2022-11-08 17:47:10 +00:00
Mark Sinclair eb18b49f3e Add a typescript version of the old js-example with basic HTML 2022-11-08 17:47:10 +00:00
Mark Sinclair 2dc45fda1e Tweak dependency build script 2022-11-08 17:47:10 +00:00
Mark Sinclair c2a113f1b3 Remove bootstrap async load, as it isn't needed when loading the wasm from a worker 2022-11-08 17:47:10 +00:00
Mark Sinclair f805eebce7 UI tweaks 2022-11-08 17:47:10 +00:00
Mark Sinclair ad81160760 Build dependencies 2022-11-08 17:47:10 +00:00
Mark Sinclair 0931236a98 Add README and example structure 2022-11-08 17:47:05 +00: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
Jon Häggblad 9b14e00653 Fix merge error 2022-11-07 13:55:36 +01:00
Jon Häggblad ec8b5e6e9d Merge remote-tracking branch 'origin/release/v1.1.0' into develop 2022-11-07 10:13:33 +01:00
Raphaël Walther d4584c305a Set cron 2022-11-04 15:50:08 +01:00
Raphaël Walther afc53d4379 Added audit notification 2022-11-04 15:12:29 +01:00
Raphaël Walther 4278e88d3c Added audit notification 2022-11-04 14:55:47 +01:00
Raphaël Walther e12a34ce6b Added audit notification 2022-11-04 11:58:23 +01:00
Raphaël Walther 1de64f7b52 Added audit notification 2022-11-04 11:42:31 +01:00
Raphaël Walther 66dbe09e66 Added audit notification 2022-11-04 11:22:54 +01:00
Raphaël Walther dcce269921 Added audit notification 2022-11-04 09:56:00 +01:00
Jędrzej Stuczyński c043f0096a Notify about sent packet after actually pushing it through mix_tx (#1735) 2022-11-03 15:11:58 +00: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
Jędrzej Stuczyński fe6da046dc Fixed beta clippy warnings (#1736) 2022-11-03 10:13:16 +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
Jon Häggblad 99b30c2570 client: additional error handling in client + socks5-client + network-requester (#1713)
* client: add error type to native client, and start handling them

* client: handle two more error cases

* changelog: add note

* socks5: add error type and start handle run errors

* network-requester: add some error types

* rustfmt

* changelog: update note

* network-requester: remove unused import
2022-10-27 16:00:26 +02:00
Jon Häggblad 2c5d31e685 client: make channel to mix traffic controller bounded and add backpressure handling (#1703)
* 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
2022-10-27 15:23:25 +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
Jon Häggblad cf65bc1295 socks5: wait to close buffer (#1702)
* 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
2022-10-27 12:42:01 +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
Jędrzej Stuczyński 2d5f851252 Workaround for clippy #9612 issue 2022-10-27 10:47:13 +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
Jędrzej Stuczyński 5ce087dafe Chore/remove unwraps (#1707)
* Disallowing the use of unwraps and expects in vesting and mixnet contracts

* Removed dodgy unwraps from the mixnet contract

* Removed dodgy unwraps from the vesting contract

* Removed unwraps/expects from common contracts crate

* ...but adding the unwraps in tests
2022-10-26 16:48:06 +01:00
Dave Hrycyszyn caf03a09c8 Adding in wss fix for web version 2022-10-26 16:34:09 +01: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
Dave Hrycyszyn 5db47b8931 Optimizing for fat wasm 2022-10-26 15:40:03 +01:00
Dave Hrycyszyn 27c1b29615 Removing accidental yarn lockfile 2022-10-26 15:38:54 +01:00
Dave Hrycyszyn c80c8ef899 Bumping version number of wasm client package 2022-10-26 14:36:33 +01: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
pierre d0cd22c4da Revert "fix(explorer-api): geoip, ip address from domain"
This reverts commit a721e97c06.
2022-10-26 11:57:04 +02:00
pierre a721e97c06 fix(explorer-api): geoip, ip address from domain 2022-10-26 11:52:54 +02:00
Drazen Urch f4f98027a0 Add per account pledge caps (#1687)
* Add per account pledge caps

* Address PR comments

* Update CHANGELOG

* No cap if no locked

* Fail account creation if taking account already exists

* Delegated free should be counted from vesting period start
2022-10-26 10:51:40 +02:00
Dave Hrycyszyn dee27e805d Adding a README for the nym-api 2022-10-25 12:25:09 +01:00
Dave Hrycyszyn 6f7dc36e5c Removing unused attribute 2022-10-25 11:45:02 +01:00
Dave Hrycyszyn ef50f361ba Adding funding notice 2022-10-25 11:45:02 +01:00
Pierre Dommerc 3c55b28e69 feat(explorer-api): auto update geoip database (#1684)
* feat(explorer-api): auto update geoip database

* feat(explorer-api): read dotenv file

* fix(explorer-api): gitignore

* feat: move geoipupdate service to root compose file
2022-10-24 18:17:50 +02:00
Jędrzej Stuczyński f1624e658e Feature/adjusting epoch events (#1696)
* Added (not yet using) source height for pending events

* Fixed existing unit tests

* Emitting source height for resolved pending events

* Removed unused attribute keys

* Emitting initial total unit reward at time of delegation

* Updated ts types

* regenerated corresponding typescript types

* missed changes

* Piggybacking on breaking change to remove serde aliases
2022-10-24 09:30:51 +01:00
Jon Häggblad fc44f2fe1c Fix typo in Makefile 2022-10-21 22:11:04 +02:00
Fouad cc26e4043c only display general settings for mixnodes (#1700)
* only display general settings for mixnodes
2022-10-21 12:21:55 +01:00
Jon Häggblad bb242080cf Update qa.env mixnet contract address 2022-10-21 12:34:44 +02:00
Jon Häggblad 3ebaf48aa3 Makefile: add wasm-client (#1699) 2022-10-21 11:35:20 +02:00
Fouad 2d7a55daba fix duplicate delegations (#1697) 2022-10-21 09:55:58 +01:00
Fouad 5f36742ce6 fix up operating cost (#1698) 2022-10-21 09:51:19 +01:00
Fouad 8547e770da Display interval timings (#1690)
* make reusable Alert component

* get current interval

* display next interval and epoch times

* display pending events
2022-10-20 17:11:02 +01:00
Gala 862178c9c5 Merge pull request #1688 from nymtech/ne-changes
Network Explorer: narrowing columns and re-orden them
2022-10-20 17:24:16 +02:00
Jędrzej Stuczyński 33a339ae2c Feature/multi node simulator (#1692)
* simple multi-node simulator

* Extending simulator with multi-node feature + testing against known good values

* Mixnet contract test fixes

* comment explaining the epsilon choice
2022-10-19 17:52:00 +01:00
Gala e0dd9b533e create theme variables 2022-10-17 17:17:20 +02:00
Gala 5ab3f95b8f cleaning 2022-10-17 15:36:42 +02:00
Gala 46097c80fe Merge branch 'develop' into ne-changes 2022-10-17 14:30:59 +02:00
Gala ab0eb35906 columns re-order and narrowing 2022-10-17 14:28:41 +02:00
Gala 8bb3b066ba nav width 2022-10-17 14:28:00 +02:00
Gala 2a04234c26 wip scroll bar styles 2022-10-03 14:45:01 +02:00
Gala ef8f6ed07b some ui changes at the navigation 2022-09-29 17:54:31 +03:00
Gala 6eb482fc4b CR: use a parameter instead of cardcoded value 2022-09-22 16:54:07 +02:00
Gala f9be735d4f various ui updates 2022-09-22 16:54:06 +02:00
Gala 0fd178a304 Merge branch 'develop' into 327-nym-connect-colours 2022-09-22 10:37:06 +02:00
Gala 16ccbd9e48 CR: use a parameter instead of cardcoded value 2022-09-20 13:30:09 +02:00
Gala bd0ea45f35 Merge branch 'develop' into 327-nym-connect-colours 2022-09-20 13:17:15 +02:00
Gala 28cc772d7b various ui updates 2022-09-08 11:43:59 +02:00
376 changed files with 9836 additions and 7816 deletions
+18
View File
@@ -3,3 +3,21 @@
RUST_LOG=info
RUST_BACKTRACE=1
#########################################
# geoipupdate (needed for explorer-api) #
#########################################
# MaxMind account ID (change it to a valid account ID)
GEOIPUPDATE_ACCOUNT_ID=xxx
# MaxMind license key (change it to a valid license key)
GEOIPUPDATE_LICENSE_KEY=xxx
# List of space-separated database edition IDs. Edition IDs may
# consist of letters, digits, and dashes. For example, GeoIP2-City
# would download the GeoIP2 City database (GeoIP2-City).
GEOIPUPDATE_EDITION_IDS=GeoLite2-Country
# The number of hours between geoipupdate runs. If this is not set
# or is set to 0, geoipupdate will run once and exit.
GEOIPUPDATE_FREQUENCY=72
# The path to the directory where geoipupdate will download the
# database.
GEOIP_DB_DIRECTORY=./explorer-api/geo_ip
+30 -14
View File
@@ -1,36 +1,52 @@
name: Daily security audit
on: workflow_dispatch
on:
schedule:
- cron: '5 9 * * *'
jobs:
security_audit:
cargo-deny:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/audit-check@v1
- name: Checkout repository code
uses: actions/checkout@v2
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
toolchain: stable
- name: Install cargo deny
run: cargo install --locked cargo-deny
- name: Run cargo deny
run: |
find . -name Cargo.toml -exec cargo deny --manifest-path {} check \
advisories -A advisory-not-detected --hide-inclusion-graph \; &> \
>(uniq &> .github/workflows/support-files/notifications/deny.message )
- uses: actions/upload-artifact@v3
with:
name: report
path: .github/workflows/support-files/notifications/deny.message
notification:
if: ${{ failure() }}
needs: security_audit
needs: cargo-deny
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v2
- name: Download report from previous job
uses: actions/download-artifact@v3
with:
name: report
path: .github/workflows/support-files/notifications
- name: Keybase - Node Install
run: npm install
working-directory: .github/workflows/support-files
- name: Keybase - Send Notification
env:
NYM_NOTIFICATION_KIND: nightly
NYM_PROJECT_NAME: "Nym daily audit"
NYM_NOTIFICATION_KIND: security
NYM_PROJECT_NAME: "Daily security report"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
GIT_BRANCH: "${GITHUB_REF##*/}"
KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}"
KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}"
KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBTECH_TEAM }}"
KEYBASE_NYM_CHANNEL: "test"
KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}"
KEYBASE_NYM_CHANNEL: "security"
uses: docker://keybaseio/client:stable-node
with:
args: .github/workflows/support-files/notifications/entry_point.sh
-72
View File
@@ -1,72 +0,0 @@
name: Continuous integration on dispatch
on: workflow_dispatch
jobs:
build:
runs-on: [ self-hosted, custom-linux ]
# Enable sccache via environment variable
env:
RUSTC_WRAPPER: /home/ubuntu/.cargo/bin/sccache
steps:
- 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 squashfs-tools
- name: Check out repository code
uses: actions/checkout@v2
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: rustfmt, clippy
- name: Build all binaries
uses: actions-rs/cargo@v1
with:
command: build
args: --workspace
- name: Run all tests
uses: actions-rs/cargo@v1
with:
command: test
args: --workspace --all-features
- name: Check formatting
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- uses: actions-rs/clippy-check@v1
name: Clippy checks
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features
- name: Run clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: --workspace -- -D warnings
- name: Build all binaries with coconut enabled
uses: actions-rs/cargo@v1
with:
command: build
args: --workspace --features=coconut
- name: Run all tests with coconut enabled
uses: actions-rs/cargo@v1
with:
command: test
args: --workspace --features=coconut
- name: Run clippy with coconut enabled
uses: actions-rs/cargo@v1
with:
command: clippy
args: --features=coconut -- -D warnings
@@ -0,0 +1,56 @@
name: CI for Network Explorer API
on:
workflow_dispatch:
release:
types: [created]
env:
NETWORK: mainnet
jobs:
publish-nym:
strategy:
fail-fast: false
matrix:
platform: [ubuntu-latest]
runs-on: ${{ matrix.platform }}
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-explorer-api-`
if: startsWith(github.ref, 'refs/tags/nym-explorer-api-') == false && github.event_name != 'workflow_dispatch'
uses: actions/github-script@v3
with:
script: |
core.setFailed('Release tag did not start with nym-explorer-api-...')
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Build all explorer-api
uses: actions-rs/cargo@v1
with:
command: build
args: --manifest-path explorer-api/Cargo.toml --workspace --release
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: my-artifact
path: |
target/release/explorer-api
retention-days: 30
- name: Upload to release based on tag name
uses: softprops/action-gh-release@v1
if: github.event_name == 'release'
with:
files: |
target/release/explorer-api
@@ -1,4 +1,4 @@
name: Nightly builds on dispatch
name: Nightly builds on latest release
on: workflow_dispatch
jobs:
@@ -7,26 +7,40 @@ jobs:
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
# creates the matrix strategy from nightly_build_matrix_includes.json
- uses: actions/checkout@v2
# creates the matrix strategy from nightly_build_release_matrix.json
- uses: actions/checkout@v3
- id: set-matrix
uses: JoshuaTheMiller/conditional-build-matrix@main
with:
inputFile: '.github/workflows/nightly_build_matrix_on_dispatch.json'
inputFile: '.github/workflows/nightly_build_release_matrix.json'
filter: '[?runOnEvent==`${{ github.event_name }}` || runOnEvent==`always`]'
build:
get_release:
runs-on: ubuntu-latest
needs: matrix_prep
outputs:
output1: ${{ steps.step2.outputs.lastest_release }}
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Set output variable to latest release branch
id: step2
run: echo "latest_release=$(git branch -r | grep -E 'release/v[0-9]+\.[0-9]+\.[0-9]+' | tail -n 1 | sed 's/origin\///')" >> $GITHUB_OUTPUT
build:
needs: [get_release,matrix_prep]
strategy:
matrix: ${{fromJson(needs.matrix_prep.outputs.matrix)}}
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.rust == 'nightly' || matrix.rust == 'beta' || matrix.rust == 'stable' }}
steps:
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools
run: sudo apt-get update && sudo apt-get install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools
if: matrix.os == 'ubuntu-latest'
- name: Check out repository code
uses: actions/checkout@v2
- name: Check out latest release branch
uses: actions/checkout@v3
with:
ref: ${{needs.get_release.outputs.output1}}
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
@@ -42,6 +56,12 @@ jobs:
command: build
args: --workspace
- name: Reclaim some disk space (because Windows is being annoying)
uses: actions-rs/cargo@v1
if: ${{ matrix.os == 'windows-latest' }}
with:
command: clean
- name: Run all tests
uses: actions-rs/cargo@v1
with:
@@ -99,6 +119,12 @@ jobs:
command: build
args: --workspace --features=coconut
- name: Reclaim some disk space (because Windows is being annoying)
uses: actions-rs/cargo@v1
if: ${{ matrix.os == 'windows-latest' }}
with:
command: clean
- name: Run all tests with coconut enabled
uses: actions-rs/cargo@v1
with:
@@ -151,7 +177,7 @@ jobs:
- name: Collect jobs status
uses: technote-space/workflow-conclusion-action@v2
- name: Check out repository code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Keybase - Node Install
if: env.WORKFLOW_CONCLUSION == 'failure'
run: npm install
@@ -160,14 +186,14 @@ jobs:
if: env.WORKFLOW_CONCLUSION == 'failure'
env:
NYM_NOTIFICATION_KIND: nightly
NYM_PROJECT_NAME: "Nym nightly build"
NYM_PROJECT_NAME: "Nym nightly build on latest release"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
GIT_BRANCH: "${GITHUB_REF##*/}"
KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}"
KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}"
KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMTECH_TEAM }}"
KEYBASE_NYM_CHANNEL: "${{ secrets.KEYBASE_CHANNEL_DEV_CORE_ID }}"
KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}"
KEYBASE_NYM_CHANNEL: "ci-nightly-release"
IS_SUCCESS: "${{ env.WORKFLOW_CONCLUSION == 'success' }}"
uses: docker://keybaseio/client:stable-node
with:
@@ -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
@@ -3,7 +3,7 @@ require('dotenv').config();
const Bot = require('keybase-bot');
let context = {
kinds: ['nym-wallet', 'ts-packages', 'network-explorer', 'nightly', 'nym-connect'],
kinds: ['nym-wallet', 'ts-packages', 'network-explorer', 'nightly', 'nym-connect','security'],
};
/**
@@ -0,0 +1,24 @@
const Handlebars = require('handlebars');
const fs = require('fs');
const path = require('path');
const { Octokit, App } = require('octokit');
async function addToContextAndValidate(context) {
return
}
async function getMessageBody(context) {
try {
const source = fs
.readFileSync("deny.message").toString();
return source;
} catch (error) {
console.error(error);
}
}
module.exports = {
addToContextAndValidate,
getMessageBody,
};
+47 -17
View File
@@ -6,27 +6,50 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
### 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))
- 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])
- 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
### Fixed
- validator-api, mixnode, gateway should now prefer values in config.toml over mainnet defaults ([#1645])
- binaries: add `-c` shortform for `--config-env-file`
- websocket-requests: add server response signalling current packet queue length in the client
### 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])
- gateway-client: will attempt to read now as many as 8 websocket messages at once, assuming they're already available on the socket ([#1669])
- clients: add concept of transmission lanes to better handle multiple data streams ([#1720])
[#1720]: https://github.com/nymtech/nym/pull/1720
## [v1.1.0](https://github.com/nymtech/nym/tree/v1.1.0) (2022-11-09)
### Added
- 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/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])
### Fixed
- 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
- 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 `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
@@ -34,11 +57,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
[#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
+53 -12
View File
@@ -577,9 +577,17 @@ dependencies = [
]
[[package]]
name = "client-core"
version = "1.0.1"
name = "client-connections"
version = "0.1.0"
dependencies = [
"futures",
]
[[package]]
name = "client-core"
version = "1.1.0"
dependencies = [
"client-connections",
"config",
"crypto",
"dirs",
@@ -737,7 +745,10 @@ name = "contracts-common"
version = "0.1.0"
dependencies = [
"cosmwasm-std",
"schemars",
"serde",
"serde_json",
"thiserror",
]
[[package]]
@@ -847,9 +858,9 @@ dependencies = [
[[package]]
name = "cpufeatures"
version = "0.2.2"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
dependencies = [
"libc",
]
@@ -1579,14 +1590,17 @@ dependencies = [
[[package]]
name = "explorer-api"
version = "1.0.1"
version = "1.1.0"
dependencies = [
"chrono",
"clap 3.2.8",
"contracts-common",
"dotenv",
"humantime-serde",
"isocountry",
"itertools",
"log",
"logging",
"maxminddb",
"mixnet-contract-common",
"network-defaults",
@@ -2725,6 +2739,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"
@@ -3056,7 +3078,7 @@ dependencies = [
[[package]]
name = "nym-cli"
version = "1.0.0"
version = "1.1.0"
dependencies = [
"anyhow",
"base64",
@@ -3067,6 +3089,7 @@ dependencies = [
"clap_complete_fig",
"dotenv",
"log",
"logging",
"network-defaults",
"nym-cli-commands",
"pretty_env_logger",
@@ -3107,9 +3130,10 @@ dependencies = [
[[package]]
name = "nym-client"
version = "1.0.2"
version = "1.1.0"
dependencies = [
"clap 3.2.8",
"client-connections",
"client-core",
"coconut-interface",
"completions",
@@ -3122,6 +3146,7 @@ dependencies = [
"gateway-client",
"gateway-requests",
"log",
"logging",
"network-defaults",
"nymsphinx",
"pemstore",
@@ -3131,6 +3156,7 @@ dependencies = [
"serde_json",
"sled",
"task",
"thiserror",
"tokio",
"tokio-tungstenite 0.14.0",
"topology",
@@ -3143,7 +3169,7 @@ dependencies = [
[[package]]
name = "nym-gateway"
version = "1.0.2"
version = "1.1.0"
dependencies = [
"anyhow",
"async-trait",
@@ -3163,6 +3189,7 @@ dependencies = [
"gateway-requests",
"humantime-serde",
"log",
"logging",
"mixnet-client",
"mixnode-common",
"network-defaults",
@@ -3189,7 +3216,7 @@ dependencies = [
[[package]]
name = "nym-mixnode"
version = "1.0.2"
version = "1.1.0"
dependencies = [
"anyhow",
"bs58",
@@ -3205,6 +3232,7 @@ dependencies = [
"humantime-serde",
"lazy_static",
"log",
"logging",
"mixnet-client",
"mixnode-common",
"nonexhaustive-delayqueue",
@@ -3230,15 +3258,17 @@ dependencies = [
[[package]]
name = "nym-network-requester"
version = "1.0.2"
version = "1.1.0"
dependencies = [
"async-trait",
"clap 3.2.8",
"client-connections",
"completions",
"dirs",
"futures",
"ipnetwork 0.20.0",
"log",
"logging",
"network-defaults",
"nymsphinx",
"ordered-buffer",
@@ -3264,6 +3294,7 @@ version = "1.0.2"
dependencies = [
"dirs",
"log",
"logging",
"pretty_env_logger",
"rocket",
"serde",
@@ -3275,9 +3306,10 @@ dependencies = [
[[package]]
name = "nym-socks5-client"
version = "1.0.2"
version = "1.1.0"
dependencies = [
"clap 3.2.8",
"client-connections",
"client-core",
"coconut-interface",
"completions",
@@ -3290,6 +3322,7 @@ dependencies = [
"gateway-client",
"gateway-requests",
"log",
"logging",
"network-defaults",
"nymsphinx",
"ordered-buffer",
@@ -3302,6 +3335,7 @@ dependencies = [
"snafu 0.6.10",
"socks5-requests",
"task",
"thiserror",
"tokio",
"topology",
"url",
@@ -3338,7 +3372,7 @@ dependencies = [
[[package]]
name = "nym-validator-api"
version = "1.0.2"
version = "1.1.0"
dependencies = [
"anyhow",
"async-trait",
@@ -3348,6 +3382,7 @@ dependencies = [
"coconut-interface",
"config",
"console-subscriber",
"contracts-common",
"cosmwasm-std",
"credential-storage",
"credentials",
@@ -3362,6 +3397,7 @@ dependencies = [
"humantime-serde",
"inclusion-probability",
"log",
"logging",
"mixnet-contract-common",
"multisig-contract-common",
"nymcoconut",
@@ -4098,6 +4134,7 @@ name = "proxy-helpers"
version = "0.1.0"
dependencies = [
"bytes",
"client-connections",
"futures",
"log",
"ordered-buffer",
@@ -6385,6 +6422,7 @@ dependencies = [
"coconut-interface",
"colored",
"config",
"contracts-common",
"cosmrs",
"cosmwasm-std",
"cw3",
@@ -6473,12 +6511,14 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
name = "vesting-contract"
version = "1.1.0"
dependencies = [
"contracts-common",
"cosmwasm-std",
"cw-storage-plus",
"mixnet-contract-common",
"schemars",
"serde",
"thiserror",
"vergen 5.1.17",
"vesting-contract-common",
]
@@ -6486,6 +6526,7 @@ dependencies = [
name = "vesting-contract-common"
version = "0.1.0"
dependencies = [
"contracts-common",
"cosmwasm-std",
"mixnet-contract-common",
"schemars",
+2
View File
@@ -26,6 +26,7 @@ members = [
"common/client-libs/gateway-client",
"common/client-libs/mixnet-client",
"common/client-libs/validator-client",
"common/client-connections",
"common/coconut-interface",
"common/commands",
"common/config",
@@ -41,6 +42,7 @@ members = [
"common/execute",
"common/inclusion-probability",
"common/ledger",
"common/logging",
"common/mixnode-common",
"common/network-defaults",
"common/nonexhaustive-delayqueue",
+19 -4
View File
@@ -2,12 +2,12 @@ test: clippy-all cargo-test wasm fmt
test-all: test cargo-test-expensive
no-clippy: build cargo-test wasm fmt
happy: fmt clippy-happy test
clippy-all: clippy-main clippy-coconut clippy-all-contracts clippy-all-wallet clippy-all-connect
clippy-all: clippy-main clippy-coconut clippy-all-contracts clippy-all-wallet clippy-all-connect clippy-all-wasm-client
clippy-happy: clippy-happy-main clippy-happy-contracts clippy-happy-wallet clippy-happy-connect
cargo-test: test-main test-contracts test-wallet test-connect test-coconut
cargo-test: test-main test-contracts test-wallet test-connect test-coconut test-wasm-client
cargo-test-expensive: test-main-expensive test-contracts-expensive test-wallet-expensive test-connect-expensive test-coconut-expensive
build: build-contracts build-wallet build-main build-connect
fmt: fmt-main fmt-contracts fmt-wallet fmt-connect
build: build-contracts build-wallet build-main build-connect build-wasm-client
fmt: fmt-main fmt-contracts fmt-wallet fmt-connect fmt-wasm-client
clippy-happy-main:
cargo clippy
@@ -40,6 +40,9 @@ clippy-all-wallet:
clippy-all-connect:
cargo clippy --workspace --manifest-path nym-connect/Cargo.toml --all-features -- -D warnings
clippy-all-wasm-client:
cargo clippy --workspace --manifest-path clients/webassembly/Cargo.toml --all-features --target wasm32-unknown-unknown -- -D warnings
test-main:
cargo test --workspace
@@ -68,6 +71,9 @@ test-wallet:
test-wallet-expensive:
cargo test --manifest-path nym-wallet/Cargo.toml --all-features -- --ignored
test-wasm-client:
cargo test --workspace --manifest-path clients/webassembly/Cargo.toml --all-features
test-connect:
cargo test --manifest-path nym-connect/Cargo.toml --all-features
@@ -86,6 +92,12 @@ build-wallet:
build-connect:
cargo build --manifest-path nym-connect/Cargo.toml --workspace
build-explorer-api:
cargo build --manifest-path explorer-api/Cargo.toml --workspace
build-wasm-client:
cargo build --manifest-path clients/webassembly/Cargo.toml --workspace --target wasm32-unknown-unknown
build-nym-cli:
cargo build --release --manifest-path tools/nym-cli/Cargo.toml
@@ -101,6 +113,9 @@ fmt-wallet:
fmt-connect:
cargo fmt --manifest-path nym-connect/Cargo.toml --all
fmt-wasm-client:
cargo fmt --manifest-path clients/webassembly/Cargo.toml --all
wasm:
RUSTFLAGS='-C link-arg=-s' cargo build --manifest-path contracts/Cargo.toml --release --target wasm32-unknown-unknown
+5 -3
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"
@@ -14,11 +14,13 @@ log = "0.4"
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
serde = { version = "1.0", features = ["derive"] }
sled = { version = "0.34", optional = true }
tap = "1.0.1"
thiserror = "1.0.34"
url = { version ="2.2", features = ["serde"] }
# internal
config = { path = "../../common/config" }
client-connections = { path = "../../common/client-connections" }
crypto = { path = "../../common/crypto" }
gateway-client = { path = "../../common/client-libs/gateway-client" }
#gateway-client = { path = "../../common/client-libs/gateway-client", default-features = false, features = ["wasm", "coconut"] }
@@ -28,7 +30,6 @@ nymsphinx = { path = "../../common/nymsphinx" }
pemstore = { path = "../../common/pemstore" }
topology = { path = "../../common/topology" }
validator-client = { path = "../../common/client-libs/validator-client", default-features = false }
tap = "1.0.1"
tokio = { version = "1.21.2", features = ["time", "macros"]}
@@ -56,4 +57,5 @@ tempfile = "3.1.0"
default = ["reply-surb"]
wasm = ["gateway-client/wasm"]
coconut = ["gateway-client/coconut", "gateway-requests/coconut"]
reply-surb = ["sled"]
reply-surb = ["sled"]
@@ -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,22 @@ 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");
// However it's still useful to alert the user that the gateway or the link to
// the gateway can't keep up. Either due to insufficient bandwidth on the
// client side, or that the gateway is overloaded.
log::warn!("Failed to send: gateway appears to not keep up");
}
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
@@ -1,3 +1,4 @@
use client_connections::TransmissionLane;
use futures::channel::mpsc;
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::anonymous_replies::ReplySurb;
@@ -11,6 +12,7 @@ pub enum InputMessage {
recipient: Recipient,
data: Vec<u8>,
with_reply_surb: bool,
lane: TransmissionLane,
},
Reply {
reply_surb: ReplySurb,
@@ -19,11 +21,17 @@ pub enum InputMessage {
}
impl InputMessage {
pub fn new_fresh(recipient: Recipient, data: Vec<u8>, with_reply_surb: bool) -> Self {
pub fn new_fresh(
recipient: Recipient,
data: Vec<u8>,
with_reply_surb: bool,
lane: TransmissionLane,
) -> Self {
InputMessage::Fresh {
recipient,
data,
with_reply_surb,
lane,
}
}
+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;
}
})
@@ -33,7 +33,7 @@ impl AcknowledgementListener {
}
async fn on_ack(&mut self, ack_content: Vec<u8>) {
debug!("Received an ack");
trace!("Received an ack");
let frag_id = match recover_identifier(&self.ack_key, &ack_content)
.map(FragmentIdentifier::try_from_bytes)
{
@@ -8,6 +8,7 @@ use crate::client::{
real_messages_control::real_traffic_stream::{BatchRealMessageSender, RealMessage},
topology_control::TopologyAccessor,
};
use client_connections::TransmissionLane;
use futures::StreamExt;
use log::*;
use nymsphinx::anonymous_replies::ReplySurb;
@@ -104,6 +105,7 @@ where
content: Vec<u8>,
with_reply_surb: bool,
) -> Option<Vec<RealMessage>> {
log::trace!("handling msg size: {}", content.len());
let topology_permit = self.topology_access.get_read_permit().await;
let topology = match topology_permit
.try_get_valid_topology_ref(&self.ack_recipient, Some(&recipient))
@@ -164,26 +166,30 @@ where
}
async fn on_input_message(&mut self, msg: InputMessage) {
let real_messages = match msg {
let (real_messages, lane) = match msg {
InputMessage::Fresh {
recipient,
data,
with_reply_surb,
} => {
lane,
} => (
self.handle_fresh_message(recipient, data, with_reply_surb)
.await,
lane,
),
InputMessage::Reply { reply_surb, data } => (
self.handle_reply(reply_surb, data)
.await
}
InputMessage::Reply { reply_surb, data } => self
.handle_reply(reply_surb, data)
.await
.map(|message| vec![message]),
.map(|message| vec![message]),
TransmissionLane::Reply,
),
};
// there's no point in trying to send nothing
if let Some(real_messages) = real_messages {
// tells real message sender (with the poisson timer) to send this to the mix network
self.real_message_sender
.unbounded_send(real_messages)
.unbounded_send((real_messages, lane))
.unwrap();
}
}
@@ -1,17 +1,21 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::action_controller::{Action, ActionSender};
use super::PendingAcknowledgement;
use super::RetransmissionRequestReceiver;
use super::{
action_controller::{Action, ActionSender},
PendingAcknowledgement, RetransmissionRequestReceiver,
};
use crate::client::{
real_messages_control::real_traffic_stream::{BatchRealMessageSender, RealMessage},
topology_control::TopologyAccessor,
};
use client_connections::TransmissionLane;
use futures::StreamExt;
use log::*;
use nymsphinx::preparer::MessagePreparer;
use nymsphinx::{acknowledgements::AckKey, addressing::clients::Recipient};
use nymsphinx::{
acknowledgements::AckKey, addressing::clients::Recipient, preparer::MessagePreparer,
};
use rand::{CryptoRng, Rng};
use std::sync::{Arc, Weak};
@@ -113,10 +117,10 @@ where
// send to `OutQueueControl` to eventually send to the mix network
self.real_message_sender
.unbounded_send(vec![RealMessage::new(
prepared_fragment.mix_packet,
frag_id,
)])
.unbounded_send((
vec![RealMessage::new(prepared_fragment.mix_packet, frag_id)],
TransmissionLane::Retransmission,
))
.unwrap();
}
@@ -14,6 +14,7 @@ use crate::client::{
topology_control::TopologyAccessor,
};
use crate::spawn_future;
use client_connections::ClosedConnectionReceiver;
use futures::channel::mpsc;
use gateway_client::AcknowledgementReceiver;
use log::*;
@@ -110,6 +111,7 @@ impl RealMessagesController<OsRng> {
mix_sender: BatchMixMessageSender,
topology_access: TopologyAccessor,
#[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage,
closed_connection_rx: ClosedConnectionReceiver,
) -> Self {
let rng = OsRng;
@@ -159,6 +161,7 @@ impl RealMessagesController<OsRng> {
rng,
config.self_recipient,
topology_access,
closed_connection_rx,
);
RealMessagesController {
@@ -4,6 +4,7 @@
use crate::client::mix_traffic::BatchMixMessageSender;
use crate::client::real_messages_control::acknowledgement_control::SentPacketNotificationSender;
use crate::client::topology_control::TopologyAccessor;
use client_connections::{ClosedConnectionReceiver, ConnectionId, TransmissionLane};
use futures::channel::mpsc;
use futures::task::{Context, Poll};
use futures::{Future, Stream, StreamExt};
@@ -16,7 +17,6 @@ use nymsphinx::forwarding::packet::MixPacket;
use nymsphinx::params::PacketSize;
use nymsphinx::utils::sample_poisson_duration;
use rand::{CryptoRng, Rng};
use std::collections::VecDeque;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
@@ -27,6 +27,23 @@ use tokio::time;
#[cfg(target_arch = "wasm32")]
use wasm_timer;
use self::{
sending_delay_controller::SendingDelayController, transmission_buffer::TransmissionBuffer,
};
mod sending_delay_controller;
mod transmission_buffer;
#[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()
}
/// Configurable parameters of the `OutQueueControl`
pub(crate) struct Config {
/// Average delay an acknowledgement packet is going to get delay at a single mixnode.
@@ -89,6 +106,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_delay_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,
@@ -106,8 +127,13 @@ where
/// Accessor to the common instance of network topology.
topology_access: TopologyAccessor,
/// Buffer containing all real messages received. It is first exhausted before more are pulled.
received_buffer: VecDeque<RealMessage>,
/// Buffer containing all incoming real messages keyed by transmission lane, that we will send
/// out to the mixnet.
transmission_buffer: TransmissionBuffer,
/// Incoming channel for being notified of closed connections, so that we can close lanes
/// corresponding to connections. To avoid sending traffic unnecessary
closed_connection_rx: ClosedConnectionReceiver,
}
pub(crate) struct RealMessage {
@@ -126,8 +152,9 @@ impl RealMessage {
// messages are already prepared, etc. the real point of it is to forward it to mix_traffic
// after sufficient delay
pub(crate) type BatchRealMessageSender = mpsc::UnboundedSender<Vec<RealMessage>>;
type BatchRealMessageReceiver = mpsc::UnboundedReceiver<Vec<RealMessage>>;
pub(crate) type BatchRealMessageSender =
mpsc::UnboundedSender<(Vec<RealMessage>, TransmissionLane)>;
type BatchRealMessageReceiver = mpsc::UnboundedReceiver<(Vec<RealMessage>, TransmissionLane)>;
pub(crate) enum StreamMessage {
Cover,
@@ -150,18 +177,21 @@ where
rng: R,
our_full_destination: Recipient,
topology_access: TopologyAccessor,
closed_connection_rx: ClosedConnectionReceiver,
) -> Self {
OutQueueControl {
config,
ack_key,
sent_notifier,
next_delay: None,
sending_delay_controller: Default::default(),
mix_tx,
real_receiver,
our_full_destination,
rng,
topology_access,
received_buffer: VecDeque::with_capacity(0), // we won't be putting any data into this guy directly
transmission_buffer: Default::default(),
closed_connection_rx,
}
}
@@ -176,7 +206,7 @@ where
async fn on_message(&mut self, next_message: StreamMessage) {
trace!("created new message");
let next_message = match next_message {
let (next_message, fragment_id) = match next_message {
StreamMessage::Cover => {
// TODO for way down the line: in very rare cases (during topology update) we might have
// to wait a really tiny bit before actually obtaining the permit hence messing with our
@@ -195,34 +225,41 @@ where
}
let topology_ref = topology_ref_option.unwrap();
generate_loop_cover_packet(
&mut self.rng,
topology_ref,
&self.ack_key,
&self.our_full_destination,
self.config.average_ack_delay,
self.config.average_packet_delay,
self.config.cover_packet_size,
(
generate_loop_cover_packet(
&mut self.rng,
topology_ref,
&self.ack_key,
&self.our_full_destination,
self.config.average_ack_delay,
self.config.average_packet_delay,
self.config.cover_packet_size,
)
.expect(
"Somehow failed to generate a loop cover message with a valid topology",
),
None,
)
.expect("Somehow failed to generate a loop cover message with a valid topology")
}
StreamMessage::Real(real_message) => {
self.sent_notify(real_message.fragment_id);
real_message.mix_packet
(real_message.mix_packet, Some(real_message.fragment_id))
}
};
// 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);
}
// notify ack controller about sending our message only after we actually managed to push it
// through the channel
if let Some(fragment_id) = fragment_id {
self.sent_notify(fragment_id);
}
// In addition to closing connections on receiving messages throught closed_connection_rx,
// also close connections when sufficiently stale.
self.transmission_buffer.prune_stale_connections();
// JS: Not entirely sure why or how it fixes stuff, but without the yield call,
// the UnboundedReceiver [of mix_rx] will not get a chance to read anything
// JS2: Basically it was the case that with high enough rate, the stream had already a next value
@@ -234,7 +271,57 @@ where
tokio::task::yield_now().await;
}
fn on_close_connection(&mut self, connection_id: ConnectionId) {
log::debug!("Removing lane for connection: {connection_id}");
self.transmission_buffer
.remove(&TransmissionLane::ConnectionId(connection_id));
}
fn current_average_message_sending_delay(&self) -> Duration {
self.config.average_message_sending_delay
* self.sending_delay_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_delay_controller.current_multiplier()
);
// Even just a single used slot is enough to signal backpressure
if used_slots > 0 {
log::trace!("Backpressure detected");
self.sending_delay_controller.record_backpressure_detected();
}
// If the buffer is running out, slow down the sending rate
if self.mix_tx.capacity() == 0
&& self.sending_delay_controller.not_increased_delay_recently()
{
self.sending_delay_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_delay_controller.is_sending_reliable() {
self.sending_delay_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();
// Start by checking if we have any incoming messages about closed connections
// NOTE: this feels a bit iffy, the `OutQueueControl` is getting ripe for a rewrite to
// something simpler.
if let Poll::Ready(Some(id)) = Pin::new(&mut self.closed_connection_rx).poll_next(cx) {
self.on_close_connection(id);
}
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 +330,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
@@ -260,28 +346,35 @@ where
next_delay.as_mut().reset(next_poisson_delay);
}
// check if we have anything immediately available
if let Some(real_available) = self.received_buffer.pop_front() {
return Poll::Ready(Some(StreamMessage::Real(Box::new(real_available))));
}
// decide what kind of message to send
// On every iteration we get new messages from upstream. Given that these come bunched
// in `Vec`, this ensures that on average we will fetch messages faster than we can
// send, which is a condition for being able to multiplex sphinx packets from multiple
// data streams.
match Pin::new(&mut self.real_receiver).poll_next(cx) {
// in the case our real message channel stream was closed, we should also indicate we are closed
// (and whoever is using the stream should panic)
Poll::Ready(None) => Poll::Ready(None),
// if there are more messages available, return first one and store the rest
Poll::Ready(Some(real_messages)) => {
self.received_buffer = real_messages.into();
// we MUST HAVE received at least ONE message
Poll::Ready(Some(StreamMessage::Real(Box::new(
self.received_buffer.pop_front().unwrap(),
))))
Poll::Ready(Some((real_messages, conn_id))) => {
log::trace!("handling real_messages: size: {}", real_messages.len());
self.transmission_buffer.store(&conn_id, real_messages);
let real_next = self
.transmission_buffer
.pop_next_message_at_random()
.expect("we just added one");
Poll::Ready(Some(StreamMessage::Real(Box::new(real_next))))
}
// otherwise construct a dummy one
Poll::Pending => Poll::Ready(Some(StreamMessage::Cover)),
Poll::Pending => {
if let Some(real_next) = self.transmission_buffer.pop_next_message_at_random() {
Poll::Ready(Some(StreamMessage::Real(Box::new(real_next))))
} else {
// otherwise construct a dummy one
Poll::Ready(Some(StreamMessage::Cover))
}
}
}
} else {
// we never set an initial delay - let's do it now
@@ -303,14 +396,9 @@ where
}
fn poll_immediate(&mut self, cx: &mut Context<'_>) -> Poll<Option<StreamMessage>> {
// check if we have anything immediately available
if let Some(real_available) = self.received_buffer.pop_front() {
// if there are more messages immediately available, notify the runtime
// because we should be polled again
if !self.received_buffer.is_empty() {
cx.waker().wake_by_ref()
}
return Poll::Ready(Some(StreamMessage::Real(Box::new(real_available))));
// Start by checking if we have any incoming messages about closed connections
if let Poll::Ready(Some(id)) = Pin::new(&mut self.closed_connection_rx).poll_next(cx) {
self.on_close_connection(id);
}
match Pin::new(&mut self.real_receiver).poll_next(cx) {
@@ -318,17 +406,26 @@ where
// (and whoever is using the stream should panic)
Poll::Ready(None) => Poll::Ready(None),
// if there are more messages available, return first one and store the rest
Poll::Ready(Some(real_messages)) => {
self.received_buffer = real_messages.into();
// we MUST HAVE received at least ONE message
Poll::Ready(Some(StreamMessage::Real(Box::new(
self.received_buffer.pop_front().unwrap(),
))))
Poll::Ready(Some((real_messages, conn_id))) => {
log::trace!("handling real_messages: size: {}", real_messages.len());
// First store what we got for the given connection id
self.transmission_buffer.store(&conn_id, real_messages);
let real_next = self
.transmission_buffer
.pop_next_message_at_random()
.expect("we just added one");
Poll::Ready(Some(StreamMessage::Real(Box::new(real_next))))
}
// if there's nothing, then there's nothing
Poll::Pending => Poll::Pending,
Poll::Pending => {
if let Some(real_next) = self.transmission_buffer.pop_next_message_at_random() {
Poll::Ready(Some(StreamMessage::Real(Box::new(real_next))))
} else {
Poll::Pending
}
}
}
}
@@ -343,24 +440,47 @@ where
}
}
#[cfg(not(target_arch = "wasm32"))]
fn log_status(&self) {
let packets = self.transmission_buffer.total_size();
let backlog = self.transmission_buffer.total_size_in_bytes() as f64 / 1024.0;
let lanes = self.transmission_buffer.num_lanes();
let mult = self.sending_delay_controller.current_multiplier();
let delay = self.current_average_message_sending_delay().as_millis();
if self.config.disable_poisson_packet_distribution {
log::info!(
"Status: {lanes} lanes, backlog: {:.2} kiB ({packets}), no delay",
backlog
);
} else {
log::info!(
"Status: {lanes} lanes, backlog: {:.2} kiB ({packets}), avg delay: {}ms ({mult})",
backlog,
delay
);
}
}
#[cfg(not(target_arch = "wasm32"))]
pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) {
debug!("Started OutQueueControl with graceful shutdown support");
let mut status_timer = tokio::time::interval(Duration::from_secs(1));
while !shutdown.is_shutdown() {
tokio::select! {
biased;
_ = shutdown.recv() => {
log::trace!("OutQueueControl: Received shutdown");
}
next_message = self.next() => match next_message {
Some(next_message) => {
self.on_message(next_message).await;
},
None => {
log::trace!("OutQueueControl: Stopping since channel closed");
break;
}
_ = status_timer.tick() => {
self.log_status();
}
next_message = self.next() => if let Some(next_message) = next_message {
self.on_message(next_message).await;
} else {
log::trace!("OutQueueControl: Stopping since channel closed");
break;
}
}
}
@@ -0,0 +1,124 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::get_time_now;
use std::time::Duration;
#[cfg(not(target_arch = "wasm32"))]
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;
pub(crate) 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,
}
impl Default for SendingDelayController {
fn default() -> Self {
SendingDelayController::new(MIN_DELAY_MULTIPLIER, MAX_DELAY_MULTIPLIER)
}
}
impl SendingDelayController {
pub(crate) 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,
}
}
pub(crate) fn current_multiplier(&self) -> u32 {
self.current_multiplier
}
pub(crate) fn increase_delay_multiplier(&mut self) {
if self.current_multiplier < self.upper_bound {
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
);
} else {
log::warn!("Trying to increase delay multipler higher than allowed");
}
}
pub(crate) fn decrease_delay_multiplier(&mut self) {
if self.current_multiplier > self.lower_bound {
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
);
}
}
pub(crate) fn record_backpressure_detected(&mut self) {
self.time_when_backpressure_detected = get_time_now();
}
pub(crate) fn not_increased_delay_recently(&self) -> bool {
get_time_now()
> self.time_when_changed + Duration::from_secs(INCREASE_DELAY_MIN_CHANGE_INTERVAL_SECS)
}
pub(crate) 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
}
}
@@ -0,0 +1,207 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use client_connections::TransmissionLane;
use rand::seq::SliceRandom;
use std::{
collections::{HashMap, HashSet, VecDeque},
time::Duration,
};
#[cfg(not(target_arch = "wasm32"))]
use tokio::time;
#[cfg(target_arch = "wasm32")]
use wasm_timer;
use super::{get_time_now, RealMessage};
// The number of lanes included in the oldest set. Used when we need to prioritize traffic.
const OLDEST_LANE_SET_SIZE: usize = 5;
// As a way of prune connections we also check for timeouts.
const MSG_CONSIDERED_STALE_AFTER_SECS: u64 = 10 * 60;
#[derive(Default)]
pub(crate) struct TransmissionBuffer {
buffer: HashMap<TransmissionLane, LaneBufferEntry>,
}
impl TransmissionBuffer {
#[allow(unused)]
pub(crate) fn is_empty(&self) -> bool {
self.buffer.is_empty()
}
pub(crate) fn remove(&mut self, lane: &TransmissionLane) -> Option<LaneBufferEntry> {
self.buffer.remove(lane)
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn num_lanes(&self) -> usize {
self.buffer.keys().count()
}
#[allow(unused)]
pub(crate) fn connections(&self) -> HashSet<u64> {
self.buffer
.keys()
.filter_map(|lane| match lane {
TransmissionLane::ConnectionId(id) => Some(id),
_ => None,
})
.copied()
.collect()
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn total_size(&self) -> usize {
self.buffer.values().map(LaneBufferEntry::len).sum()
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn total_size_in_bytes(&self) -> usize {
self.buffer
.values()
.map(|lane_buffer_entry| {
lane_buffer_entry
.real_messages
.iter()
.map(|real_message| real_message.mix_packet.sphinx_packet().len())
.sum::<usize>()
})
.sum()
}
fn get_oldest_set(&self) -> Vec<TransmissionLane> {
let mut buffer: Vec<_> = self
.buffer
.iter()
.map(|(k, v)| (k, v.messages_transmitted))
.collect();
buffer.sort_by_key(|v| v.1);
buffer
.iter()
.rev()
.map(|(k, _)| *k)
.take(OLDEST_LANE_SET_SIZE)
.copied()
.collect()
}
pub(crate) fn store(&mut self, lane: &TransmissionLane, real_messages: Vec<RealMessage>) {
if let Some(lane_buffer_entry) = self.buffer.get_mut(lane) {
lane_buffer_entry.append(real_messages);
} else {
self.buffer
.insert(*lane, LaneBufferEntry::new(real_messages));
}
}
fn pick_random_lane(&self) -> Option<&TransmissionLane> {
let lanes: Vec<&TransmissionLane> = self.buffer.keys().collect();
lanes.choose(&mut rand::thread_rng()).copied()
}
fn pick_random_small_lane(&self) -> Option<&TransmissionLane> {
let lanes: Vec<&TransmissionLane> = self
.buffer
.iter()
.filter(|(_, v)| v.is_small())
.map(|(k, _)| k)
.collect();
lanes.choose(&mut rand::thread_rng()).copied()
}
fn pick_random_old_lane(&self) -> Option<TransmissionLane> {
let lanes = self.get_oldest_set();
lanes.choose(&mut rand::thread_rng()).copied()
}
fn pop_front_from_lane(&mut self, lane: &TransmissionLane) -> Option<RealMessage> {
let real_msgs_queued = self.buffer.get_mut(lane)?;
let real_next = real_msgs_queued.pop_front()?;
real_msgs_queued.messages_transmitted += 1;
if real_msgs_queued.is_empty() {
self.buffer.remove(lane);
}
Some(real_next)
}
pub(crate) fn pop_next_message_at_random(&mut self) -> Option<RealMessage> {
if self.buffer.is_empty() {
return None;
}
// Very basic heuristic where we prioritize according to small lanes first, the older lanes
// to try to finish lanes when possible, then the rest.
let lane = if let Some(small_lane) = self.pick_random_small_lane() {
*small_lane
} else if let Some(old_lane) = self.pick_random_old_lane() {
old_lane
} else {
*self.pick_random_lane()?
};
log::trace!("picking to send from lane: {:?}", lane);
self.pop_front_from_lane(&lane)
}
pub(crate) fn prune_stale_connections(&mut self) {
let stale_entries: Vec<_> = self
.buffer
.iter()
.filter_map(|(lane, entry)| if entry.is_stale() { Some(lane) } else { None })
.copied()
.collect();
for lane in stale_entries {
self.remove(&lane);
}
}
}
pub(crate) struct LaneBufferEntry {
pub real_messages: VecDeque<RealMessage>,
pub messages_transmitted: usize,
#[cfg(not(target_arch = "wasm32"))]
pub time_for_last_activity: time::Instant,
#[cfg(target_arch = "wasm32")]
pub time_for_last_activity: wasm_timer::Instant,
}
impl LaneBufferEntry {
fn new(real_messages: Vec<RealMessage>) -> Self {
LaneBufferEntry {
real_messages: real_messages.into(),
messages_transmitted: 0,
time_for_last_activity: get_time_now(),
}
}
fn append(&mut self, real_messages: Vec<RealMessage>) {
self.real_messages.append(&mut real_messages.into());
self.time_for_last_activity = get_time_now();
}
fn pop_front(&mut self) -> Option<RealMessage> {
self.real_messages.pop_front()
}
fn is_small(&self) -> bool {
self.real_messages.len() < 100
}
fn is_stale(&self) -> bool {
get_time_now() - self.time_for_last_activity
> Duration::from_secs(MSG_CONSIDERED_STALE_AFTER_SECS)
}
#[cfg(not(target_arch = "wasm32"))]
fn len(&self) -> usize {
self.real_messages.len()
}
fn is_empty(&self) -> bool {
self.real_messages.is_empty()
}
}
@@ -208,7 +208,7 @@ impl ReceivedMessagesBuffer {
}
async fn handle_new_received(&mut self, msgs: Vec<Vec<u8>>) {
debug!(
trace!(
"Processing {:?} new message that might get added to the buffer!",
msgs.len()
);
@@ -57,24 +57,15 @@ impl<'a> TopologyReadPermit<'a> {
) -> Option<&'a NymTopology> {
// Note: implicit deref with Deref for TopologyReadPermit is happening here
let topology_ref_option = self.permit.as_ref();
match topology_ref_option {
None => None,
Some(topology_ref) => {
// see if it's possible to route the packet to both gateways
if !topology_ref.can_construct_path_through(DEFAULT_NUM_MIX_HOPS)
|| !topology_ref.gateway_exists(ack_recipient.gateway())
|| if let Some(packet_recipient) = packet_recipient {
!topology_ref.gateway_exists(packet_recipient.gateway())
} else {
false
}
{
None
topology_ref_option.as_ref().filter(|topology_ref| {
!(!topology_ref.can_construct_path_through(DEFAULT_NUM_MIX_HOPS)
|| !topology_ref.gateway_exists(ack_recipient.gateway())
|| if let Some(packet_recipient) = packet_recipient {
!topology_ref.gateway_exists(packet_recipient.gateway())
} else {
Some(topology_ref)
}
}
}
false
})
})
}
}
+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
View File
@@ -24,4 +24,6 @@ pub enum ClientCoreError {
ListOfValidatorApisIsEmpty,
#[error("Could not load existing gateway configuration: {0}")]
CouldNotLoadExistingGatewayConfiguration(std::io::Error),
#[error("The current network topology seem to be insufficient to route any packets through")]
InsufficientNetworkTopology,
}
+1 -1
View File
@@ -22,7 +22,7 @@ cfg_if::cfg_if! {
#[clap(author = "Nymtech", version, about)]
struct Cli {
/// Path pointing to an env file that configures the client.
#[clap(long)]
#[clap(short, long)]
pub(crate) config_env_file: Option<std::path::PathBuf>,
/// Path where the sqlite credental database will be located.
+4 -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"
@@ -27,17 +27,20 @@ pretty_env_logger = "0.4" # for formatting log messages
rand = { version = "0.7.3", features = ["wasm-bindgen"] } # rng-related traits + some rng implementation to use
serde = { version = "1.0.104", features = ["derive"] } # for config serialization/deserialization
sled = "0.34" # for storage of replySURB decryption keys
thiserror = "1.0.34"
tokio = { version = "1.21.2", features = ["rt-multi-thread", "net", "signal"] } # async runtime
tokio-tungstenite = "0.14" # websocket
## internal
client-core = { path = "../client-core" }
client-connections = { path = "../../common/client-connections" }
coconut-interface = { path = "../../common/coconut-interface", optional = true }
config = { path = "../../common/config" }
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" }
@@ -43,6 +43,7 @@ async fn send_file_with_reply() {
recipient,
message: read_data,
with_reply_surb: true,
connection_id: 0,
};
println!("sending content of 'dummy_file' over the mix network...");
@@ -91,6 +92,7 @@ async fn send_file_without_reply() {
recipient,
message: read_data,
with_reply_surb: false,
connection_id: 0,
};
println!("sending content of 'dummy_file' over the mix network...");
+54 -36
View File
@@ -1,14 +1,13 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use client_connections::{ClosedConnectionReceiver, ClosedConnectionSender, TransmissionLane};
use client_core::client::cover_traffic_stream::LoopCoverTrafficStream;
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::{
@@ -20,6 +19,7 @@ use client_core::client::topology_control::{
TopologyAccessor, TopologyRefresher, TopologyRefresherConfig,
};
use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder;
use client_core::error::ClientCoreError;
use crypto::asymmetric::identity;
use futures::channel::mpsc;
use gateway_client::bandwidth::BandwidthController;
@@ -31,11 +31,11 @@ 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};
use crate::client::config::{Config, SocketType};
use crate::error::ClientError;
use crate::websocket;
pub(crate) mod config;
@@ -103,13 +103,15 @@ 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);
}
#[allow(clippy::too_many_arguments)]
fn start_real_traffic_controller(
&self,
topology_accessor: TopologyAccessor,
@@ -117,6 +119,7 @@ impl NymClient {
ack_receiver: AcknowledgementReceiver,
input_receiver: InputMessageReceiver,
mix_sender: BatchMixMessageSender,
closed_connection_rx: ClosedConnectionReceiver,
shutdown: ShutdownListener,
) {
let mut controller_config = real_messages_control::Config::new(
@@ -132,8 +135,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...");
@@ -145,6 +149,7 @@ impl NymClient {
mix_sender,
topology_accessor,
reply_key_storage,
closed_connection_rx,
)
.start_with_shutdown(shutdown);
}
@@ -233,7 +238,7 @@ impl NymClient {
&mut self,
topology_accessor: TopologyAccessor,
shutdown: ShutdownListener,
) {
) -> Result<(), ClientError> {
let topology_refresher_config = TopologyRefresherConfig::new(
self.config.get_base().get_validator_api_endpoints(),
self.config.get_base().get_topology_refresh_rate(),
@@ -248,14 +253,16 @@ impl NymClient {
// TODO: a slightly more graceful termination here
if !topology_refresher.is_topology_routable().await {
panic!(
"The current network topology seem to be insufficient to route any packets through\
log::error!(
"The current network topology seem to be insufficient to route any packets through \
- check if enough nodes and a gateway are online"
);
return Err(ClientCoreError::InsufficientNetworkTopology.into());
}
info!("Starting topology refresher...");
topology_refresher.start_with_shutdown(shutdown);
Ok(())
}
// controller for sending sphinx packets to mixnet (either real traffic or cover traffic)
@@ -263,24 +270,29 @@ 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(
&self,
buffer_requester: ReceivedBufferRequestSender,
msg_input: InputMessageSender,
closed_connection_tx: ClosedConnectionSender,
) {
info!("Starting websocket listener...");
let websocket_handler =
websocket::Handler::new(msg_input, buffer_requester, self.as_mix_recipient());
let websocket_handler = websocket::Handler::new(
msg_input,
closed_connection_tx,
buffer_requester,
&self.as_mix_recipient(),
);
websocket::Listener::new(self.config.get_listening_port()).start(websocket_handler);
}
@@ -289,7 +301,8 @@ impl NymClient {
/// It's untested and there are absolutely no guarantees about it (but seems to have worked
/// well enough in local tests)
pub fn send_message(&mut self, recipient: Recipient, message: Vec<u8>, with_reply_surb: bool) {
let input_msg = InputMessage::new_fresh(recipient, message, with_reply_surb);
let lane = TransmissionLane::General;
let input_msg = InputMessage::new_fresh(recipient, message, with_reply_surb, lane);
self.input_tx
.as_ref()
@@ -329,8 +342,8 @@ impl NymClient {
}
/// blocking version of `start` method. Will run forever (or until SIGINT is sent)
pub async fn run_forever(&mut self) {
let shutdown = self.start().await;
pub async fn run_forever(&mut self) -> Result<(), ClientError> {
let shutdown = self.start().await?;
wait_for_signal().await;
println!(
@@ -347,20 +360,16 @@ impl NymClient {
//shutdown.wait_for_shutdown().await;
log::info!("Stopping nym-client");
Ok(())
}
pub async fn start(&mut self) -> ShutdownNotifier {
pub async fn start(&mut self) -> Result<ShutdownNotifier, ClientError> {
info!("Starting nym client");
// channels for inter-component communication
// TODO: make the channels be internally created by the relevant components
// 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();
@@ -385,7 +394,7 @@ impl NymClient {
// the components are started in very specific order. Unless you know what you are doing,
// do not change that.
self.start_topology_refresher(shared_topology_accessor.clone(), shutdown.subscribe())
.await;
.await?;
self.start_received_messages_buffer_controller(
received_buffer_request_receiver,
mixnet_messages_receiver,
@@ -397,17 +406,24 @@ 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());
// Channels that the websocket listener can use to signal downstream to the real traffic
// controller that connections are closed.
let (closed_connection_tx, closed_connection_rx) = mpsc::unbounded();
self.start_real_traffic_controller(
shared_topology_accessor.clone(),
reply_key_storage,
ack_receiver,
input_receiver,
sphinx_message_sender.clone(),
closed_connection_rx,
shutdown.subscribe(),
);
@@ -424,9 +440,11 @@ impl NymClient {
}
match self.config.get_socket_type() {
SocketType::WebSocket => {
self.start_websocket_listener(received_buffer_request_sender, input_sender)
}
SocketType::WebSocket => self.start_websocket_listener(
received_buffer_request_sender,
input_sender,
closed_connection_tx,
),
SocketType::None => {
// if we did not start the socket, it means we're running (supposedly) in the native mode
// and hence we should announce 'ourselves' to the buffer
@@ -447,6 +465,6 @@ impl NymClient {
info!("Client startup finished!");
info!("The address of this client is: {}", self.as_mix_recipient());
shutdown
Ok(shutdown)
}
}
+5 -3
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::client::config::{Config, SocketType};
use crate::error::ClientError;
use clap::CommandFactory;
use clap::{Parser, Subcommand};
use completions::{fig_generate, ArgShell};
@@ -49,7 +50,7 @@ fn long_version_static() -> &'static str {
#[clap(author = "Nymtech", version, long_version = long_version_static(), about)]
pub(crate) struct Cli {
/// Path pointing to an env file that configures the client.
#[clap(long)]
#[clap(short, long)]
pub(crate) config_env_file: Option<std::path::PathBuf>,
#[clap(subcommand)]
@@ -83,16 +84,17 @@ pub(crate) struct OverrideConfig {
enabled_credentials_mode: bool,
}
pub(crate) async fn execute(args: &Cli) {
pub(crate) async fn execute(args: &Cli) -> Result<(), ClientError> {
let bin_name = "nym-native-client";
match &args.command {
Commands::Init(m) => init::execute(m).await,
Commands::Run(m) => run::execute(m).await,
Commands::Run(m) => run::execute(m).await?,
Commands::Upgrade(m) => upgrade::execute(m),
Commands::Completions(s) => s.generate(&mut Cli::into_app(), bin_name),
Commands::GenerateFigSpec => fig_generate(&mut Cli::into_app(), bin_name),
}
Ok(())
}
pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Config {
+5 -4
View File
@@ -4,6 +4,7 @@
use crate::{
client::{config::Config, NymClient},
commands::{override_config, OverrideConfig},
error::ClientError,
};
use clap::Args;
@@ -73,14 +74,14 @@ fn version_check(cfg: &Config) -> bool {
}
}
pub(crate) async fn execute(args: &Run) {
pub(crate) async fn execute(args: &Run) -> Result<(), ClientError> {
let id = &args.id;
let mut config = match Config::load_from_file(Some(id)) {
Ok(cfg) => cfg,
Err(err) => {
error!("Failed to load config for {}. Are you sure you have run `init` before? (Error was: {})", id, err);
return;
return Err(ClientError::FailedToLoadConfig(id.to_string()));
}
};
@@ -89,8 +90,8 @@ pub(crate) async fn execute(args: &Run) {
if !version_check(&config) {
error!("failed the local version check");
return;
return Err(ClientError::FailedLocalVersionCheck);
}
NymClient::new(config).run_forever().await;
NymClient::new(config).run_forever().await
}
+23
View File
@@ -0,0 +1,23 @@
use client_core::error::ClientCoreError;
use crypto::asymmetric::identity::Ed25519RecoveryError;
use gateway_client::error::GatewayClientError;
use validator_client::ValidatorClientError;
#[derive(thiserror::Error, Debug)]
pub enum ClientError {
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("Gateway client error: {0}")]
GatewayClientError(#[from] GatewayClientError),
#[error("Ed25519 error: {0}")]
Ed25519RecoveryError(#[from] Ed25519RecoveryError),
#[error("Validator client error: {0}")]
ValidatorClientError(#[from] ValidatorClientError),
#[error("client-core error: {0}")]
ClientCoreError(#[from] ClientCoreError),
#[error("Failed to load config for: {0}")]
FailedToLoadConfig(String),
#[error("Failed local version check, client and config mismatch")]
FailedLocalVersionCheck,
}
+1
View File
@@ -2,4 +2,5 @@
// SPDX-License-Identifier: Apache-2.0
pub mod client;
pub mod error;
pub mod websocket;
+5 -24
View File
@@ -2,20 +2,23 @@
// SPDX-License-Identifier: Apache-2.0
use clap::{crate_version, Parser};
use error::ClientError;
use logging::setup_logging;
use network_defaults::setup_env;
pub mod client;
pub mod commands;
pub mod error;
pub mod websocket;
#[tokio::main]
async fn main() {
async fn main() -> Result<(), ClientError> {
setup_logging();
println!("{}", banner());
let args = commands::Cli::parse();
setup_env(args.config_env_file.clone());
commands::execute(&args).await;
commands::execute(&args).await
}
fn banner() -> String {
@@ -34,25 +37,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();
}
+49 -37
View File
@@ -1,6 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use client_connections::{ClosedConnectionSender, TransmissionLane};
use client_core::client::{
inbound_messages::{InputMessage, InputMessageSender},
received_buffer::{
@@ -34,6 +35,7 @@ impl Default for ReceivedResponseType {
pub(crate) struct Handler {
msg_input: InputMessageSender,
closed_connection_tx: ClosedConnectionSender,
buffer_requester: ReceivedBufferRequestSender,
self_full_address: Recipient,
socket: Option<WebSocketStream<TcpStream>>,
@@ -45,6 +47,7 @@ impl Clone for Handler {
fn clone(&self) -> Self {
Handler {
msg_input: self.msg_input.clone(),
closed_connection_tx: self.closed_connection_tx.clone(),
buffer_requester: self.buffer_requester.clone(),
self_full_address: self.self_full_address,
socket: None,
@@ -64,13 +67,15 @@ impl Drop for Handler {
impl Handler {
pub(crate) fn new(
msg_input: InputMessageSender,
closed_connection_tx: ClosedConnectionSender,
buffer_requester: ReceivedBufferRequestSender,
self_full_address: Recipient,
self_full_address: &Recipient,
) -> Self {
Handler {
msg_input,
closed_connection_tx,
buffer_requester,
self_full_address,
self_full_address: *self_full_address,
socket: None,
received_response_type: Default::default(),
}
@@ -78,12 +83,14 @@ impl Handler {
fn handle_send(
&mut self,
recipient: Recipient,
recipient: &Recipient,
message: Vec<u8>,
with_reply_surb: bool,
connection_id: u64,
) -> Option<ServerResponse> {
// the ack control is now responsible for chunking, etc.
let input_msg = InputMessage::new_fresh(recipient, message, with_reply_surb);
let lane = TransmissionLane::ConnectionId(connection_id);
let input_msg = InputMessage::new_fresh(*recipient, message, with_reply_surb, lane);
self.msg_input.unbounded_send(input_msg).unwrap();
None
@@ -104,18 +111,27 @@ impl Handler {
ServerResponse::SelfAddress(self.self_full_address)
}
fn handle_closed_connection(&self, connection_id: u64) -> Option<ServerResponse> {
self.closed_connection_tx
.unbounded_send(connection_id)
.unwrap();
None
}
fn handle_request(&mut self, request: ClientRequest) -> Option<ServerResponse> {
match request {
ClientRequest::Send {
recipient,
message,
with_reply_surb,
} => self.handle_send(recipient, message, with_reply_surb),
connection_id,
} => self.handle_send(&recipient, message, with_reply_surb, connection_id),
ClientRequest::Reply {
message,
reply_surb,
} => self.handle_reply(reply_surb, message),
ClientRequest::SelfAddress => Some(self.handle_self_address()),
ClientRequest::ClosedConnection(id) => self.handle_closed_connection(id),
}
}
@@ -134,7 +150,7 @@ impl Handler {
response.map(|resp| WsMessage::text(resp.into_text()))
}
fn handle_binary_message(&mut self, msg: Vec<u8>) -> Option<WsMessage> {
fn handle_binary_message(&mut self, msg: &[u8]) -> Option<WsMessage> {
debug!("Handling binary message request");
self.received_response_type = ReceivedResponseType::Binary;
@@ -154,37 +170,11 @@ impl Handler {
// old version of this file.
match raw_request {
WsMessage::Text(text_message) => self.handle_text_message(text_message),
WsMessage::Binary(binary_message) => self.handle_binary_message(binary_message),
WsMessage::Binary(binary_message) => self.handle_binary_message(&binary_message),
_ => None,
}
}
// I'm still not entirely sure why `send_all` requires `TryStream` rather than `Stream`, but
// let's just play along for now
fn prepare_reconstructed_binary(
&self,
reconstructed_messages: Vec<ReconstructedMessage>,
) -> Vec<Result<WsMessage, WsError>> {
reconstructed_messages
.into_iter()
.map(ServerResponse::Received)
.map(|resp| Ok(WsMessage::Binary(resp.into_binary())))
.collect()
}
// I'm still not entirely sure why `send_all` requires `TryStream` rather than `Stream`, but
// let's just play along for now
fn prepare_reconstructed_text(
&self,
reconstructed_messages: Vec<ReconstructedMessage>,
) -> Vec<Result<WsMessage, WsError>> {
reconstructed_messages
.into_iter()
.map(ServerResponse::Received)
.map(|resp| Ok(WsMessage::Text(resp.into_text())))
.collect()
}
async fn push_websocket_received_plaintexts(
&mut self,
reconstructed_messages: Vec<ReconstructedMessage>,
@@ -193,10 +183,8 @@ impl Handler {
// if it's text or binary, but for time being we use the naive assumption that if
// client is sending Message::Text it expects text back. Same for Message::Binary
let response_messages = match self.received_response_type {
ReceivedResponseType::Binary => {
self.prepare_reconstructed_binary(reconstructed_messages)
}
ReceivedResponseType::Text => self.prepare_reconstructed_text(reconstructed_messages),
ReceivedResponseType::Binary => prepare_reconstructed_binary(reconstructed_messages),
ReceivedResponseType::Text => prepare_reconstructed_text(reconstructed_messages),
};
let mut send_stream = futures::stream::iter(response_messages);
@@ -291,3 +279,27 @@ impl Handler {
self.listen_for_requests(reconstructed_receiver).await;
}
}
// I'm still not entirely sure why `send_all` requires `TryStream` rather than `Stream`, but
// let's just play along for now
fn prepare_reconstructed_binary(
reconstructed_messages: Vec<ReconstructedMessage>,
) -> Vec<Result<WsMessage, WsError>> {
reconstructed_messages
.into_iter()
.map(ServerResponse::Received)
.map(|resp| Ok(WsMessage::Binary(resp.into_binary())))
.collect()
}
// I'm still not entirely sure why `send_all` requires `TryStream` rather than `Stream`, but
// let's just play along for now
fn prepare_reconstructed_text(
reconstructed_messages: Vec<ReconstructedMessage>,
) -> Vec<Result<WsMessage, WsError>> {
reconstructed_messages
.into_iter()
.map(ServerResponse::Received)
.map(|resp| Ok(WsMessage::Text(resp.into_text())))
.collect()
}
@@ -20,6 +20,9 @@ pub const REPLY_REQUEST_TAG: u8 = 0x01;
/// Value tag representing [`SelfAddress`] variant of the [`ClientRequest`]
pub const SELF_ADDRESS_REQUEST_TAG: u8 = 0x02;
/// Value tag representing [`ClosedConnection`] variant of the [`ClientRequest`]
pub const CLOSED_CONNECTION_REQUEST_TAG: u8 = 0x03;
#[allow(non_snake_case)]
#[derive(Debug)]
pub enum ClientRequest {
@@ -28,32 +31,41 @@ pub enum ClientRequest {
message: Vec<u8>,
// Perhaps we could change it to a number to indicate how many reply_SURBs we want to include?
with_reply_surb: bool,
connection_id: u64,
},
Reply {
message: Vec<u8>,
reply_surb: ReplySurb,
},
SelfAddress,
ClosedConnection(u64),
}
// we could have been parsing it directly TryFrom<WsMessage>, but we want to retain
// information about whether it came from binary or text to send appropriate response back
impl ClientRequest {
// SEND_REQUEST_TAG || with_surb || recipient || data_len || data
fn serialize_send(recipient: Recipient, data: Vec<u8>, with_reply_surb: bool) -> Vec<u8> {
// SEND_REQUEST_TAG || with_surb || recipient || conn_id || data_len || data
fn serialize_send(
recipient: Recipient,
data: Vec<u8>,
with_reply_surb: bool,
connection_id: u64,
) -> Vec<u8> {
let data_len_bytes = (data.len() as u64).to_be_bytes();
let conn_id_bytes = connection_id.to_be_bytes();
std::iter::once(SEND_REQUEST_TAG)
.chain(std::iter::once(with_reply_surb as u8))
.chain(recipient.to_bytes().iter().cloned()) // will not be length prefixed because the length is constant
.chain(conn_id_bytes.iter().cloned())
.chain(data_len_bytes.iter().cloned())
.chain(data.into_iter())
.collect()
}
// SEND_REQUEST_TAG || with_reply || recipient || data_len || data
// SEND_REQUEST_TAG || with_reply || recipient || conn_id || data_len || data
fn deserialize_send(b: &[u8]) -> Result<Self, error::Error> {
// we need to have at least 1 (tag) + 1 (reply flag) + Recipient::LEN + sizeof<u64> bytes
if b.len() < 2 + Recipient::LEN + size_of::<u64>() {
// we need to have at least 1 (tag) + 1 (reply flag) + Recipient::LEN + 2*sizeof<u64> bytes
if b.len() < 2 + Recipient::LEN + 2 * size_of::<u64>() {
return Err(error::Error::new(
ErrorKind::TooShortRequest,
"not enough data provided to recover 'send'".to_string(),
@@ -86,9 +98,15 @@ impl ClientRequest {
}
};
let data_len_bytes = &b[2 + Recipient::LEN..2 + Recipient::LEN + size_of::<u64>()];
let mut connection_id_bytes = [0u8; size_of::<u64>()];
connection_id_bytes
.copy_from_slice(&b[2 + Recipient::LEN..2 + Recipient::LEN + size_of::<u64>()]);
let connection_id = u64::from_be_bytes(connection_id_bytes);
let data_len_bytes =
&b[2 + Recipient::LEN + size_of::<u64>()..2 + Recipient::LEN + 2 * size_of::<u64>()];
let data_len = u64::from_be_bytes(data_len_bytes.try_into().unwrap());
let data = &b[2 + Recipient::LEN + size_of::<u64>()..];
let data = &b[2 + Recipient::LEN + 2 * size_of::<u64>()..];
if data.len() as u64 != data_len {
return Err(error::Error::new(
ErrorKind::MalformedRequest,
@@ -104,11 +122,12 @@ impl ClientRequest {
with_reply_surb,
recipient,
message: data.to_vec(),
connection_id,
})
}
// REPLY_REQUEST_TAG || surb_len || surb || message_len || message
fn serialize_reply(message: Vec<u8>, reply_surb: ReplySurb) -> Vec<u8> {
fn serialize_reply(message: Vec<u8>, reply_surb: &ReplySurb) -> Vec<u8> {
let reply_surb_bytes = reply_surb.to_bytes();
let surb_len_bytes = (reply_surb_bytes.len() as u64).to_be_bytes();
let message_len_bytes = (message.len() as u64).to_be_bytes();
@@ -202,20 +221,43 @@ impl ClientRequest {
ClientRequest::SelfAddress
}
// CLOSED_CONNECTION_REQUEST_TAG
fn serialize_closed_connection(connection_id: u64) -> Vec<u8> {
let conn_id_bytes = connection_id.to_be_bytes();
std::iter::once(CLOSED_CONNECTION_REQUEST_TAG)
.chain(conn_id_bytes.iter().copied())
.collect()
}
// CLOSED_CONNECTION_REQUEST_TAG
fn deserialize_closed_connection(b: &[u8]) -> Self {
// this MUST match because it was called by 'deserialize'
debug_assert_eq!(b[0], CLOSED_CONNECTION_REQUEST_TAG);
let mut connection_id_bytes = [0u8; size_of::<u64>()];
connection_id_bytes.copy_from_slice(&b[1..=size_of::<u64>()]);
let connection_id = u64::from_be_bytes(connection_id_bytes);
ClientRequest::ClosedConnection(connection_id)
}
pub fn serialize(self) -> Vec<u8> {
match self {
ClientRequest::Send {
recipient,
message,
with_reply_surb,
} => Self::serialize_send(recipient, message, with_reply_surb),
connection_id,
} => Self::serialize_send(recipient, message, with_reply_surb, connection_id),
ClientRequest::Reply {
message,
reply_surb,
} => Self::serialize_reply(message, reply_surb),
} => Self::serialize_reply(message, &reply_surb),
ClientRequest::SelfAddress => Self::serialize_self_address(),
ClientRequest::ClosedConnection(id) => Self::serialize_closed_connection(id),
}
}
@@ -245,15 +287,16 @@ impl ClientRequest {
SEND_REQUEST_TAG => Self::deserialize_send(b),
REPLY_REQUEST_TAG => Self::deserialize_reply(b),
SELF_ADDRESS_REQUEST_TAG => Ok(Self::deserialize_self_address(b)),
CLOSED_CONNECTION_REQUEST_TAG => Ok(Self::deserialize_closed_connection(b)),
n => Err(error::Error::new(
ErrorKind::UnknownRequest,
format!("type {}", n),
format!("type {n}"),
)),
}
}
pub fn try_from_binary(raw_req: Vec<u8>) -> Result<Self, error::Error> {
Self::deserialize(&raw_req)
pub fn try_from_binary(raw_req: &[u8]) -> Result<Self, error::Error> {
Self::deserialize(raw_req)
}
pub fn try_from_text(raw_req: String) -> Result<Self, error::Error> {
@@ -280,6 +323,7 @@ mod tests {
recipient,
message: b"foomp".to_vec(),
with_reply_surb: false,
connection_id: 42,
};
let bytes = send_request_no_surb.serialize();
@@ -289,10 +333,12 @@ mod tests {
recipient,
message,
with_reply_surb,
connection_id,
} => {
assert_eq!(recipient.to_string(), recipient_string);
assert_eq!(message, b"foomp".to_vec());
assert!(!with_reply_surb)
assert!(!with_reply_surb);
assert_eq!(connection_id, 42)
}
_ => unreachable!(),
}
@@ -301,6 +347,7 @@ mod tests {
recipient,
message: b"foomp".to_vec(),
with_reply_surb: true,
connection_id: 213,
};
let bytes = send_request_surb.serialize();
@@ -310,10 +357,12 @@ mod tests {
recipient,
message,
with_reply_surb,
connection_id,
} => {
assert_eq!(recipient.to_string(), recipient_string);
assert_eq!(message, b"foomp".to_vec());
assert!(with_reply_surb)
assert!(with_reply_surb);
assert_eq!(connection_id, 213)
}
_ => unreachable!(),
}
@@ -352,4 +401,15 @@ mod tests {
_ => unreachable!(),
}
}
#[test]
fn close_connection_request_serialization_works() {
let close_connection_request = ClientRequest::ClosedConnection(42);
let bytes = close_connection_request.serialize();
let recovered = ClientRequest::deserialize(&bytes).unwrap();
match recovered {
ClientRequest::ClosedConnection(id) => assert_eq!(id, 42),
_ => unreachable!(),
}
}
}
@@ -23,10 +23,14 @@ pub const RECEIVED_RESPONSE_TAG: u8 = 0x01;
/// Value tag representing [`SelfAddress`] variant of the [`ServerResponse`]
pub const SELF_ADDRESS_RESPONSE_TAG: u8 = 0x02;
/// Value tag representing [`LaneQueueLength`] variant of the [`ServerResponse`]
pub const LANE_QUEUE_LENGTH_RESPONSE_TAG: u8 = 0x03;
#[derive(Debug)]
pub enum ServerResponse {
Received(ReconstructedMessage),
SelfAddress(Recipient),
LaneQueueLength(u64, usize),
Error(error::Error),
}
@@ -193,6 +197,31 @@ impl ServerResponse {
Ok(ServerResponse::SelfAddress(recipient))
}
// LANE_QUEUE_LENGTH_RESPONSE_TAG || lane || queue_length
fn serialize_lane_queue_length(lane: u64, queue_length: usize) -> Vec<u8> {
std::iter::once(LANE_QUEUE_LENGTH_RESPONSE_TAG)
.chain(lane.to_be_bytes().iter().cloned())
.chain(queue_length.to_be_bytes().iter().cloned())
.collect()
}
// LANE_QUEUE_LENGTH_RESPONSE_TAG || lane || queue_length
fn deserialize_lane_queue_length(b: &[u8]) -> Result<Self, error::Error> {
// this MUST match because it was called by 'deserialize'
debug_assert_eq!(b[0], LANE_QUEUE_LENGTH_RESPONSE_TAG);
let mut lane_bytes = [0u8; size_of::<u64>()];
lane_bytes.copy_from_slice(&b[1..=size_of::<u64>()]);
let lane = u64::from_be_bytes(lane_bytes);
let mut queue_length_bytes = [0u8; size_of::<usize>()];
queue_length_bytes
.copy_from_slice(&b[1 + size_of::<u64>()..1 + size_of::<u64>() + size_of::<usize>()]);
let queue_length = usize::from_be_bytes(queue_length_bytes);
Ok(ServerResponse::LaneQueueLength(lane, queue_length))
}
// ERROR_RESPONSE_TAG || err_code || msg_len || msg
fn serialize_error(error: error::Error) -> Vec<u8> {
let message_len_bytes = (error.message.len() as u64).to_be_bytes();
@@ -272,6 +301,9 @@ impl ServerResponse {
Self::serialize_received(reconstructed_message)
}
ServerResponse::SelfAddress(address) => Self::serialize_self_address(address),
ServerResponse::LaneQueueLength(lane, queue_length) => {
Self::serialize_lane_queue_length(lane, queue_length)
}
ServerResponse::Error(err) => Self::serialize_error(err),
}
}
@@ -302,6 +334,7 @@ impl ServerResponse {
match response_tag {
RECEIVED_RESPONSE_TAG => Self::deserialize_received(b),
SELF_ADDRESS_RESPONSE_TAG => Self::deserialize_self_address(b),
LANE_QUEUE_LENGTH_RESPONSE_TAG => Self::deserialize_lane_queue_length(b),
ERROR_RESPONSE_TAG => Self::deserialize_error(b),
n => Err(error::Error::new(
ErrorKind::UnknownResponse,
@@ -378,6 +411,20 @@ mod tests {
}
}
#[test]
fn lane_queue_length_response_serialization_works() {
let lane_queue_length_response = ServerResponse::LaneQueueLength(13, 42);
let bytes = lane_queue_length_response.serialize();
let recovered = ServerResponse::deserialize(&bytes).unwrap();
match recovered {
ServerResponse::LaneQueueLength(lane, queue_length) => {
assert_eq!(lane, 13);
assert_eq!(queue_length, 42)
}
_ => unreachable!(),
}
}
#[test]
fn error_response_serialization_works() {
let dummy_error = error::Error::new(ErrorKind::UnknownRequest, "foomp message".to_string());
@@ -20,6 +20,7 @@ pub(super) enum ClientRequestText {
message: String,
recipient: String,
with_reply_surb: bool,
connection_id: u64,
},
SelfAddress,
#[serde(rename_all = "camelCase")]
@@ -46,6 +47,7 @@ impl TryInto<ClientRequest> for ClientRequestText {
message,
recipient,
with_reply_surb,
connection_id,
} => {
let message_bytes = message.into_bytes();
let recipient = Recipient::try_from_base58_string(recipient).map_err(|err| {
@@ -56,6 +58,7 @@ impl TryInto<ClientRequest> for ClientRequestText {
message: message_bytes,
recipient,
with_reply_surb,
connection_id,
})
}
ClientRequestText::SelfAddress => Ok(ClientRequest::SelfAddress),
@@ -91,6 +94,10 @@ pub(super) enum ServerResponseText {
SelfAddress {
address: String,
},
LaneQueueLength {
lane: u64,
queue_length: usize,
},
Error {
message: String,
},
@@ -132,6 +139,9 @@ impl From<ServerResponse> for ServerResponseText {
ServerResponse::SelfAddress(recipient) => ServerResponseText::SelfAddress {
address: recipient.to_string(),
},
ServerResponse::LaneQueueLength(lane, queue_length) => {
ServerResponseText::LaneQueueLength { lane, queue_length }
}
ServerResponse::Error(err) => ServerResponseText::Error {
message: err.to_string(),
},
+4 -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"
@@ -20,17 +20,20 @@ pretty_env_logger = "0.4"
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
serde = { version = "1.0", features = ["derive"] } # for config serialization/deserialization
snafu = "0.6"
thiserror = "1.0.34"
tokio = { version = "1.21.2", features = ["rt-multi-thread", "net", "signal"] }
url = "2.2"
# internal
client-core = { path = "../client-core" }
client-connections = { path = "../../common/client-connections" }
coconut-interface = { path = "../../common/coconut-interface", optional = true }
config = { path = "../../common/config" }
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" }
+53 -33
View File
@@ -4,18 +4,18 @@
use std::sync::atomic::Ordering;
use crate::client::config::Config;
use crate::error::Socks5ClientError;
use crate::socks::{
authentication::{AuthenticationMethods, Authenticator, User},
server::SphinxSocksServer,
};
use client_connections::{ClosedConnectionReceiver, ClosedConnectionSender};
use client_core::client::cover_traffic_stream::LoopCoverTrafficStream;
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,
@@ -25,6 +25,7 @@ use client_core::client::topology_control::{
TopologyAccessor, TopologyRefresher, TopologyRefresherConfig,
};
use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder;
use client_core::error::ClientCoreError;
use crypto::asymmetric::identity;
use futures::channel::mpsc;
use futures::StreamExt;
@@ -36,7 +37,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,13 +103,15 @@ 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);
}
#[allow(clippy::too_many_arguments)]
fn start_real_traffic_controller(
&self,
topology_accessor: TopologyAccessor,
@@ -117,6 +119,7 @@ impl NymClient {
ack_receiver: AcknowledgementReceiver,
input_receiver: InputMessageReceiver,
mix_sender: BatchMixMessageSender,
closed_connection_rx: ClosedConnectionReceiver,
shutdown: ShutdownListener,
) {
let mut controller_config = client_core::client::real_messages_control::Config::new(
@@ -132,8 +135,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...");
@@ -145,6 +149,7 @@ impl NymClient {
mix_sender,
topology_accessor,
reply_key_storage,
closed_connection_rx,
)
.start_with_shutdown(shutdown);
}
@@ -233,7 +238,7 @@ impl NymClient {
&mut self,
topology_accessor: TopologyAccessor,
shutdown: ShutdownListener,
) {
) -> Result<(), Socks5ClientError> {
let topology_refresher_config = TopologyRefresherConfig::new(
self.config.get_base().get_validator_api_endpoints(),
self.config.get_base().get_topology_refresh_rate(),
@@ -248,14 +253,16 @@ impl NymClient {
// TODO: a slightly more graceful termination here
if !topology_refresher.is_topology_routable().await {
panic!(
"The current network topology seem to be insufficient to route any packets through\
log::error!(
"The current network topology seem to be insufficient to route any packets through \
- check if enough nodes and a gateway are online"
);
return Err(ClientCoreError::InsufficientNetworkTopology.into());
}
info!("Starting topology refresher...");
topology_refresher.start_with_shutdown(shutdown);
Ok(())
}
// controller for sending sphinx packets to mixnet (either real traffic or cover traffic)
@@ -263,19 +270,20 @@ 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(
&self,
buffer_requester: ReceivedBufferRequestSender,
msg_input: InputMessageSender,
closed_connection_tx: ClosedConnectionSender,
shutdown: ShutdownListener,
) {
info!("Starting socks5 listener...");
@@ -290,12 +298,16 @@ impl NymClient {
self.as_mix_recipient(),
shutdown,
);
tokio::spawn(async move { sphinx_socks.serve(msg_input, buffer_requester).await });
tokio::spawn(async move {
sphinx_socks
.serve(msg_input, buffer_requester, closed_connection_tx)
.await
});
}
/// blocking version of `start` method. Will run forever (or until SIGINT is sent)
pub async fn run_forever(&mut self) {
let mut shutdown = self.start().await;
pub async fn run_forever(&mut self) -> Result<(), Socks5ClientError> {
let mut shutdown = self.start().await?;
wait_for_signal().await;
log::info!("Sending shutdown");
@@ -306,11 +318,15 @@ impl NymClient {
shutdown.wait_for_shutdown().await;
log::info!("Stopping nym-socks5-client");
Ok(())
}
// Variant of `run_forever` that listends for remote control messages
pub async fn run_and_listen(&mut self, mut receiver: Socks5ControlMessageReceiver) {
let mut shutdown = self.start().await;
pub async fn run_and_listen(
&mut self,
mut receiver: Socks5ControlMessageReceiver,
) -> Result<(), Socks5ClientError> {
let mut shutdown = self.start().await?;
tokio::select! {
message = receiver.next() => {
log::debug!("Received message: {:?}", message);
@@ -336,20 +352,16 @@ impl NymClient {
shutdown.wait_for_shutdown().await;
log::info!("Stopping nym-socks5-client");
Ok(())
}
pub async fn start(&mut self) -> ShutdownNotifier {
pub async fn start(&mut self) -> Result<ShutdownNotifier, Socks5ClientError> {
info!("Starting nym client");
// channels for inter-component communication
// TODO: make the channels be internally created by the relevant components
// 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();
@@ -374,7 +386,7 @@ impl NymClient {
// the components are started in very specific order. Unless you know what you are doing,
// do not change that.
self.start_topology_refresher(shared_topology_accessor.clone(), shutdown.subscribe())
.await;
.await?;
self.start_received_messages_buffer_controller(
received_buffer_request_receiver,
mixnet_messages_receiver,
@@ -386,17 +398,24 @@ 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());
// Channel for announcing closed (socks5) connections by the controller.
// This will be forwarded to `OutQueueControl`
let (closed_connection_tx, closed_connection_rx) = mpsc::unbounded();
self.start_real_traffic_controller(
shared_topology_accessor.clone(),
reply_key_storage,
ack_receiver,
input_receiver,
sphinx_message_sender.clone(),
closed_connection_rx,
shutdown.subscribe(),
);
@@ -415,12 +434,13 @@ impl NymClient {
self.start_socks5_listener(
received_buffer_request_sender,
input_sender,
closed_connection_tx,
shutdown.subscribe(),
);
info!("Client startup finished!");
info!("The address of this client is: {}", self.as_mix_recipient());
shutdown
Ok(shutdown)
}
}
+7 -3
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::client::config::Config;
use crate::error::Socks5ClientError;
use clap::CommandFactory;
use clap::{Parser, Subcommand};
use completions::{fig_generate, ArgShell};
@@ -50,7 +51,7 @@ fn long_version_static() -> &'static str {
#[clap(author = "Nymtech", version, long_version = long_version_static(), about)]
pub(crate) struct Cli {
/// Path pointing to an env file that configures the client.
#[clap(long)]
#[clap(short, long)]
pub(crate) config_env_file: Option<std::path::PathBuf>,
#[clap(subcommand)]
@@ -61,8 +62,10 @@ pub(crate) struct Cli {
pub(crate) enum Commands {
/// Initialise a Nym client. Do this first!
Init(init::Init),
/// Run the Nym client with provided configuration client optionally overriding set parameters
Run(run::Run),
/// Try to upgrade the client
Upgrade(upgrade::Upgrade),
@@ -83,16 +86,17 @@ pub(crate) struct OverrideConfig {
enabled_credentials_mode: bool,
}
pub(crate) async fn execute(args: &Cli) {
pub(crate) async fn execute(args: &Cli) -> Result<(), Socks5ClientError> {
let bin_name = "nym-socks5-client";
match &args.command {
Commands::Init(m) => init::execute(m).await,
Commands::Run(m) => run::execute(m).await,
Commands::Run(m) => run::execute(m).await?,
Commands::Upgrade(m) => upgrade::execute(m),
Commands::Completions(s) => s.generate(&mut Cli::into_app(), bin_name),
Commands::GenerateFigSpec => fig_generate(&mut Cli::into_app(), bin_name),
}
Ok(())
}
pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Config {
+5 -4
View File
@@ -4,6 +4,7 @@
use crate::{
client::{config::Config, NymClient},
commands::{override_config, OverrideConfig},
error::Socks5ClientError,
};
use clap::Args;
@@ -80,14 +81,14 @@ fn version_check(cfg: &Config) -> bool {
}
}
pub(crate) async fn execute(args: &Run) {
pub(crate) async fn execute(args: &Run) -> Result<(), Socks5ClientError> {
let id = &args.id;
let mut config = match Config::load_from_file(Some(id)) {
Ok(cfg) => cfg,
Err(err) => {
error!("Failed to load config for {}. Are you sure you have run `init` before? (Error was: {})", id, err);
return;
return Err(Socks5ClientError::FailedToLoadConfig(id.to_string()));
}
};
@@ -96,8 +97,8 @@ pub(crate) async fn execute(args: &Run) {
if !version_check(&config) {
error!("failed the local version check");
return;
return Err(Socks5ClientError::FailedLocalVersionCheck);
}
NymClient::new(config).run_forever().await;
NymClient::new(config).run_forever().await
}
+23
View File
@@ -0,0 +1,23 @@
use client_core::error::ClientCoreError;
use crypto::asymmetric::identity::Ed25519RecoveryError;
use gateway_client::error::GatewayClientError;
use validator_client::ValidatorClientError;
#[derive(thiserror::Error, Debug)]
pub enum Socks5ClientError {
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("Gateway client error: {0}")]
GatewayClientError(#[from] GatewayClientError),
#[error("Ed25519 error: {0}")]
Ed25519RecoveryError(#[from] Ed25519RecoveryError),
#[error("Validator client error: {0}")]
ValidatorClientError(#[from] ValidatorClientError),
#[error("client-core error: {0}")]
ClientCoreError(#[from] ClientCoreError),
#[error("Failed to load config for: {0}")]
FailedToLoadConfig(String),
#[error("Failed local version check, client and config mismatch")]
FailedLocalVersionCheck,
}
+1
View File
@@ -2,4 +2,5 @@
// SPDX-License-Identifier: Apache-2.0
pub mod client;
pub mod error;
pub mod socks;
+5 -22
View File
@@ -2,20 +2,23 @@
// SPDX-License-Identifier: Apache-2.0
use clap::{crate_version, Parser};
use error::Socks5ClientError;
use logging::setup_logging;
use network_defaults::setup_env;
pub mod client;
mod commands;
pub mod error;
pub mod socks;
#[tokio::main]
async fn main() {
async fn main() -> Result<(), Socks5ClientError> {
setup_logging();
println!("{}", banner());
let args = commands::Cli::parse();
setup_env(args.config_env_file.clone());
commands::execute(&args).await;
commands::execute(&args).await
}
fn banner() -> String {
@@ -34,23 +37,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();
}
+12 -7
View File
@@ -4,8 +4,8 @@ use super::authentication::{AuthenticationMethods, Authenticator, User};
use super::request::{SocksCommand, SocksRequest};
use super::types::{ResponseCode, SocksProxyError};
use super::{RESERVED, SOCKS_VERSION};
use client_core::client::inbound_messages::InputMessage;
use client_core::client::inbound_messages::InputMessageSender;
use client_connections::TransmissionLane;
use client_core::client::inbound_messages::{InputMessage, InputMessageSender};
use futures::channel::mpsc;
use futures::task::{Context, Poll};
use log::*;
@@ -226,17 +226,21 @@ impl SocksClient {
}
}
async fn send_connect_to_mixnet(&mut self, remote_address: RemoteAddress) {
fn send_connect_to_mixnet(&mut self, remote_address: RemoteAddress) {
let req = Request::new_connect(self.connection_id, remote_address, self.self_address);
let msg = Message::Request(req);
let input_message = InputMessage::new_fresh(self.service_provider, msg.into_bytes(), false);
let input_message = InputMessage::new_fresh(
self.service_provider,
msg.into_bytes(),
false,
TransmissionLane::ConnectionId(self.connection_id),
);
self.input_sender.unbounded_send(input_message).unwrap();
}
async fn run_proxy(&mut self, conn_receiver: ConnectionReceiver, remote_proxy_target: String) {
self.send_connect_to_mixnet(remote_proxy_target.clone())
.await;
self.send_connect_to_mixnet(remote_proxy_target.clone());
let stream = self.stream.run_proxy();
let local_stream_remote = stream
@@ -259,7 +263,8 @@ impl SocksClient {
.run(move |conn_id, read_data, socket_closed| {
let provider_request = Request::new_send(conn_id, read_data, socket_closed);
let provider_message = Message::Request(provider_request);
InputMessage::new_fresh(recipient, provider_message.into_bytes(), false)
let lane = TransmissionLane::ConnectionId(conn_id);
InputMessage::new_fresh(recipient, provider_message.into_bytes(), false, lane)
})
.await
.into_inner();
+3 -1
View File
@@ -4,6 +4,7 @@ use super::{
mixnet_responses::MixnetResponseListener,
types::{ResponseCode, SocksProxyError},
};
use client_connections::ClosedConnectionSender;
use client_core::client::{
inbound_messages::InputMessageSender, received_buffer::ReceivedBufferRequestSender,
};
@@ -51,13 +52,14 @@ impl SphinxSocksServer {
&mut self,
input_sender: InputMessageSender,
buffer_requester: ReceivedBufferRequestSender,
closed_connection_tx: ClosedConnectionSender,
) -> Result<(), SocksProxyError> {
let listener = TcpListener::bind(self.listening_address).await.unwrap();
info!("Serving Connections...");
// controller for managing all active connections
let (mut active_streams_controller, controller_sender) =
Controller::new(self.shutdown.clone());
Controller::new(closed_connection_tx, self.shutdown.clone());
tokio::spawn(async move {
active_streams_controller.run().await;
});
+6 -3
View File
@@ -1,7 +1,7 @@
[package]
name = "nym-client-wasm"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jedrzej Stuczynski <andrew@nymtech.net>"]
version = "1.0.0"
version = "1.1.0"
edition = "2021"
keywords = ["nym", "sphinx", "wasm", "webassembly", "privacy", "client"]
license = "Apache-2.0"
@@ -29,6 +29,7 @@ url = "2.2"
# internal
client-core = { path = "../client-core", default-features = false, features = ["wasm"] }
client-connections = { path = "../../common/client-connections" }
coconut-interface = { path = "../../common/coconut-interface", optional = true }
credentials = { path = "../../common/credentials", optional = true }
crypto = { path = "../../common/crypto" }
@@ -55,6 +56,8 @@ wee_alloc = { version = "0.4", optional = true }
wasm-bindgen-test = "0.3"
[package.metadata.wasm-pack.profile.release]
wasm-opt = false
wasm-opt = true
[profile.release]
lto = true
opt-level = 'z'
+5
View File
@@ -0,0 +1,5 @@
clippy:
cargo clippy --all-features --target wasm32-unknown-unknown -- -D warnings
test:
wasm-pack test --node
@@ -2,6 +2,11 @@ const CopyWebpackPlugin = require('copy-webpack-plugin');
const path = require('path');
module.exports = {
performance: {
hints: false,
maxEntrypointSize: 512000,
maxAssetSize: 512000
},
entry: {
bootstrap: './bootstrap.js',
worker: './worker.js',
@@ -22,6 +27,7 @@ module.exports = {
},
],
}),
],
experiments: { syncWebAssembly: true },
};
+1
View File
@@ -73,6 +73,7 @@ async function main() {
// const preferredGateway = 'CgQrYP8etksSBf4nALNqp93SHPpgFwEUyTsjBNNLj5WM';
const gatewayEndpoint = await get_gateway(validator, preferredGateway);
gatewayEndpoint.gateway_listener = "wss://gateway1.nymtech.net:443"; // this is needed if we want it to work on the web. However this gateway is a v1 gateway, we will need to change for v2 once we get there
// only really useful if you want to adjust some settings like traffic rate
// (if not needed you can just pass a null)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,217 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
#[wasm_bindgen(typescript_custom_section)]
const TS_DEFS: &'static str = r#"
export interface BinaryMessage {
kind: number,
payload: Uint8Array;
headers: string,
}
export interface StringMessage {
kind: number,
payload: string;
}
"#;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(typescript_type = "BinaryMessage")]
pub type IBinaryMessage;
#[wasm_bindgen(typescript_type = "StringMessage")]
pub type IStringMessage;
}
#[derive(Serialize, Deserialize)]
pub struct BinaryMessage {
pub kind: u8,
pub payload: Vec<u8>,
pub headers: String,
}
#[derive(Serialize, Deserialize)]
pub struct StringMessage {
pub kind: u8,
pub payload: String,
}
/// Create a new binary message with a user-specified `kind`.
#[wasm_bindgen]
pub fn create_binary_message(kind: u8, payload: Vec<u8>) -> Vec<u8> {
create_binary_message_with_headers(kind, payload, "".to_string())
}
/// Create a new message with a UTF-8 encoded string `payload` and a user-specified `kind`.
#[wasm_bindgen]
pub fn create_binary_message_from_string(kind: u8, payload: String) -> Vec<u8> {
create_binary_message_with_headers(kind, payload.as_bytes().to_vec(), "".to_string())
}
/// Create a new binary message with a user-specified `kind`, and `headers` as a string.
#[wasm_bindgen]
pub fn create_binary_message_with_headers(kind: u8, payload: Vec<u8>, headers: String) -> Vec<u8> {
let headers = headers.as_bytes().to_vec();
let size = (headers.len() as u64).to_be_bytes().to_vec();
vec![vec![kind], size, headers, payload].concat()
}
/// Parse the `kind` and byte array `payload` from a byte array
#[wasm_bindgen]
pub async fn parse_binary_message(message: Vec<u8>) -> Result<IBinaryMessage, JsError> {
if message.len() < 2 {
return Err(JsError::new(
"Could not parse message, as less than 2 bytes long",
));
}
let (kind, _headers, payload) = parse_binary_payload(&message);
Ok(serde_wasm_bindgen::to_value(&BinaryMessage {
kind,
payload: payload.to_vec(),
headers: "".to_string(),
})
.unwrap()
.unchecked_into::<IBinaryMessage>())
}
/// Parse the `kind` and byte array `payload` from a byte array with headers
#[wasm_bindgen]
pub async fn parse_binary_message_with_headers(
message: Vec<u8>,
) -> Result<IBinaryMessage, JsError> {
if message.len() < 2 {
return Err(JsError::new(
"Could not parse message, as less than 2 bytes long",
));
}
let (kind, headers, payload) = parse_binary_payload(&message);
Ok(serde_wasm_bindgen::to_value(&BinaryMessage {
kind,
payload: payload.to_vec(),
headers,
})
.unwrap()
.unchecked_into::<IBinaryMessage>())
}
/// Parse the `kind` and UTF-8 string `payload` from a byte array with headers
#[wasm_bindgen]
pub async fn parse_string_message_with_headers(
message: Vec<u8>,
) -> Result<IStringMessage, JsError> {
if message.len() < 2 {
return Err(JsError::new(
"Could not parse message, as less than 2 bytes long",
));
}
let (kind, _headers, payload) = parse_binary_payload(&message);
let payload = String::from_utf8_lossy(payload).into_owned();
Ok(
serde_wasm_bindgen::to_value(&StringMessage { kind, payload })
.unwrap()
.unchecked_into::<IStringMessage>(),
)
}
pub(crate) fn parse_binary_payload(message: &[u8]) -> (u8, String, &[u8]) {
// 1st byte is the kind
let kind = message[0];
// then the size as u64 big endian
let mut size = [0u8; 8];
size.clone_from_slice(&message[1..9]);
let size = u64::from_be_bytes(size) as usize;
// then the headers
let headers = String::from_utf8_lossy(&message[9..9 + size]).into_owned();
// finally the payload
let payload = &message[9 + size..];
(kind, headers, payload)
}
/// Parse the `kind` and UTF-8 string `payload` from a byte array
#[wasm_bindgen]
pub async fn parse_string_message(message: Vec<u8>) -> Result<IStringMessage, JsError> {
if message.len() < 2 {
return Err(JsError::new(
"Could not parse message, as less than 2 bytes long",
));
}
let kind = message[0];
let payload = String::from_utf8_lossy(&message[1..]).into_owned();
Ok(
serde_wasm_bindgen::to_value(&StringMessage { kind, payload })
.unwrap()
.unchecked_into::<IStringMessage>(),
)
}
#[cfg(test)]
mod tests {
use super::{create_binary_message_with_headers, parse_binary_payload};
use wasm_bindgen_test::*;
#[wasm_bindgen_test]
fn test_binary_with_headers() {
let message_as_bytes = create_binary_message_with_headers(
42u8,
vec![0u8, 1u8, 2u8],
"test headers".to_string(),
);
// calculate header size
let headers = "test headers".as_bytes().to_vec();
let size = headers.len();
// the expected size
let expected_size = 12;
assert_eq!(size, expected_size);
assert_eq!(message_as_bytes[0], 42u8);
assert_eq!(message_as_bytes[1..9], 12u64.to_be_bytes());
assert_eq!(
message_as_bytes[9 + expected_size..9 + expected_size + 3],
vec![0u8, 1u8, 2u8]
);
let res = parse_binary_payload(&message_as_bytes);
assert_eq!(res.0, 42u8);
assert_eq!(res.1, "test headers".to_string());
assert_eq!(res.2, vec![0u8, 1u8, 2u8]);
}
#[wasm_bindgen_test]
fn test_binary_with_empty_headers() {
let message_as_bytes =
create_binary_message_with_headers(42u8, vec![0u8, 1u8, 2u8], "".to_string());
let expected_size = 0;
assert_eq!(message_as_bytes[0], 42u8);
assert_eq!(message_as_bytes[1..9], 0u64.to_be_bytes());
assert_eq!(
message_as_bytes[9 + expected_size..9 + expected_size + 3],
vec![0u8, 1u8, 2u8]
);
let res = parse_binary_payload(&message_as_bytes);
assert_eq!(res.0, 42u8);
assert_eq!(res.1, "".to_string());
assert_eq!(res.2, vec![0u8, 1u8, 2u8]);
}
}
+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(),
}
}
}
+41 -26
View File
@@ -2,11 +2,12 @@
// SPDX-License-Identifier: Apache-2.0
use self::config::Config;
use client_connections::{ClosedConnectionReceiver, TransmissionLane};
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,11 +23,10 @@ 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;
use wasm_utils::{console_log, console_warn};
use wasm_utils::console_log;
pub mod config;
@@ -45,6 +45,7 @@ pub struct NymClient {
// callbacks
on_message: Option<js_sys::Function>,
on_binary_message: Option<js_sys::Function>,
on_gateway_connect: Option<js_sys::Function>,
}
@@ -56,6 +57,7 @@ impl NymClient {
config,
key_manager: Self::setup_key_manager(),
on_message: None,
on_binary_message: None,
on_gateway_connect: None,
input_tx: None,
}
@@ -73,8 +75,11 @@ impl NymClient {
self.on_message = Some(on_message);
}
pub fn set_on_binary_message(&mut self, on_binary_message: js_sys::Function) {
self.on_binary_message = Some(on_binary_message);
}
pub fn set_on_gateway_connect(&mut self, on_connect: js_sys::Function) {
console_log!("setting on connect...");
self.on_gateway_connect = Some(on_connect)
}
@@ -110,8 +115,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();
@@ -123,6 +128,7 @@ impl NymClient {
ack_receiver: AcknowledgementReceiver,
input_receiver: InputMessageReceiver,
mix_sender: BatchMixMessageSender,
closed_connection_rx: ClosedConnectionReceiver,
) {
let mut controller_config = real_messages_control::Config::new(
self.key_manager.ack_key(),
@@ -135,8 +141,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...");
@@ -147,6 +153,7 @@ impl NymClient {
input_receiver,
mix_sender,
topology_accessor,
closed_connection_rx,
)
.start();
}
@@ -253,13 +260,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
@@ -269,6 +274,7 @@ impl NymClient {
received_buffer_request_sender: ReceivedBufferRequestSender,
) {
let on_message = self.on_message.take();
let on_binary_message = self.on_binary_message.take();
spawn_local(async move {
let (reconstructed_sender, mut reconstructed_receiver) = mpsc::unbounded();
@@ -283,8 +289,14 @@ impl NymClient {
let this = JsValue::null();
while let Some(reconstructed) = reconstructed_receiver.next().await {
if let Some(ref callback) = on_message {
for msg in reconstructed {
for msg in reconstructed {
if let Some(ref callback_binary) = on_binary_message {
let arg1 = serde_wasm_bindgen::to_value(&msg.message).unwrap();
callback_binary
.call1(&this, &arg1)
.expect("on binary message failed!");
}
if let Some(ref callback) = on_message {
if msg.reply_surb.is_some() {
console_log!("the received message contained a reply-surb that we do not know how to handle (yet)")
}
@@ -292,9 +304,6 @@ impl NymClient {
let arg1 = serde_wasm_bindgen::to_value(&stringified).unwrap();
callback.call1(&this, &arg1).expect("on message failed!");
}
} else {
console_warn!("no on_message callback was specified. the received message content is getting dropped");
console_log!("the raw messages: {:?}", reconstructed)
}
}
});
@@ -307,11 +316,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();
@@ -326,6 +330,10 @@ impl NymClient {
let (ack_sender, ack_receiver) = mpsc::unbounded();
let shared_topology_accessor = TopologyAccessor::new();
// Channel that the real traffix controller can listed to for closing connections.
// Currently unused in the wasm client.
let (_closed_connection_tx, closed_connection_rx) = mpsc::unbounded();
// the components are started in very specific order. Unless you know what you are doing,
// do not change that.
self.start_topology_refresher(shared_topology_accessor.clone())
@@ -339,12 +347,18 @@ 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,
input_receiver,
sphinx_message_sender.clone(),
closed_connection_rx,
);
if !self.config.debug.disable_loop_cover_traffic_stream {
@@ -370,8 +384,9 @@ impl NymClient {
console_log!("Sending {} bytes to {}", message.len(), recipient);
let recipient = Recipient::try_from_base58_string(recipient).unwrap();
let lane = TransmissionLane::General;
let input_msg = InputMessage::new_fresh(recipient, message, false);
let input_msg = InputMessage::new_fresh(recipient, message, false, lane);
self.input_tx
.as_ref()
+2
View File
@@ -3,6 +3,8 @@
use wasm_bindgen::prelude::*;
#[cfg(target_arch = "wasm32")]
pub mod binary_message_helper;
#[cfg(target_arch = "wasm32")]
mod client;
#[cfg(target_arch = "wasm32")]
+2
View File
@@ -0,0 +1,2 @@
allow-unwrap-in-tests = true
allow-expect-in-tests = true
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "client-connections"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
futures = "0.3"
+21
View File
@@ -0,0 +1,21 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use futures::channel::mpsc;
pub type ConnectionId = u64;
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
pub enum TransmissionLane {
General,
Reply,
Retransmission,
Control,
ConnectionId(ConnectionId),
}
/// Announce connections that are closed, for whoever is interested.
/// One usecase is that the network-requester and socks5-client wants to know about this, so that
/// they can forward this to the `OutQueueControl` (via `ClientRequest` for the network-requester)
pub type ClosedConnectionSender = mpsc::UnboundedSender<ConnectionId>;
pub type ClosedConnectionReceiver = mpsc::UnboundedReceiver<ConnectionId>;
@@ -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" }
@@ -15,14 +15,12 @@ use cosmrs::rpc::query::Query;
use cosmrs::rpc::Error as TendermintRpcError;
use cosmrs::rpc::HttpClientUrl;
use cosmrs::tx::Msg;
use cosmwasm_std::Uint128;
use execute::execute;
use network_defaults::{ChainDetails, NymNetworkDetails};
use serde::{Deserialize, Serialize};
use std::convert::TryInto;
use std::time::SystemTime;
use vesting_contract_common::ExecuteMsg as VestingExecuteMsg;
use vesting_contract_common::QueryMsg as VestingQueryMsg;
pub use crate::nymd::cosmwasm_client::client::CosmWasmClient;
pub use crate::nymd::cosmwasm_client::signing_client::SigningCosmWasmClient;
@@ -47,6 +45,7 @@ pub use fee::{gas_price::GasPrice, GasAdjustable, GasAdjustment};
use mixnet_contract_common::MixId;
pub use signing_client::Client as SigningNymdClient;
pub use traits::{VestingQueryClient, VestingSigningClient};
use vesting_contract_common::PledgeCap;
pub mod coin;
pub mod cosmwasm_client;
@@ -482,16 +481,6 @@ impl<C> NymdClient<C> {
self.client.get_total_supply().await
}
pub async fn vesting_get_locked_pledge_cap(&self) -> Result<Uint128, NymdError>
where
C: CosmWasmClient + Sync,
{
let request = VestingQueryMsg::GetLockedPledgeCap {};
self.client
.query_contract_smart(self.vesting_contract_address(), &request)
.await
}
pub async fn simulate<I, M>(&self, messages: I) -> Result<SimulateResponse, NymdError>
where
C: SigningCosmWasmClient + Sync,
@@ -737,12 +726,16 @@ impl<C> NymdClient<C> {
#[execute("vesting")]
fn _vesting_update_locked_pledge_cap(
&self,
amount: Uint128,
address: String,
cap: PledgeCap,
fee: Option<Fee>,
) -> (VestingExecuteMsg, Option<Fee>)
where
C: SigningCosmWasmClient + Sync,
{
(VestingExecuteMsg::UpdateLockedPledgeCap { amount }, fee)
(
VestingExecuteMsg::UpdateLockedPledgeCap { address, cap },
fee,
)
}
}
@@ -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,
@@ -9,6 +9,7 @@ use async_trait::async_trait;
use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams};
use mixnet_contract_common::{Gateway, MixId, MixNode};
use vesting_contract_common::messages::{ExecuteMsg as VestingExecuteMsg, VestingSpecification};
use vesting_contract_common::PledgeCap;
#[async_trait]
pub trait VestingSigningClient {
@@ -105,6 +106,13 @@ pub trait VestingSigningClient {
staking_address: Option<String>,
vesting_spec: Option<VestingSpecification>,
amount: Coin,
cap: Option<PledgeCap>,
fee: Option<Fee>,
) -> Result<ExecuteResult, NymdError>;
async fn transfer_ownership(
&self,
to_address: &str,
fee: Option<Fee>,
) -> Result<ExecuteResult, NymdError>;
}
@@ -382,6 +390,7 @@ impl<C: SigningCosmWasmClient + Sync + Send> VestingSigningClient for NymdClient
staking_address: Option<String>,
vesting_spec: Option<VestingSpecification>,
amount: Coin,
cap: Option<PledgeCap>,
fee: Option<Fee>,
) -> Result<ExecuteResult, NymdError> {
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier)));
@@ -389,6 +398,7 @@ impl<C: SigningCosmWasmClient + Sync + Send> VestingSigningClient for NymdClient
owner_address: owner_address.to_string(),
staking_address,
vesting_spec,
cap,
};
self.client
.execute(
@@ -401,4 +411,25 @@ impl<C: SigningCosmWasmClient + Sync + Send> VestingSigningClient for NymdClient
)
.await
}
async fn transfer_ownership(
&self,
to_address: &str,
fee: Option<Fee>,
) -> Result<ExecuteResult, NymdError> {
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier)));
let req = VestingExecuteMsg::TransferOwnership {
to_address: to_address.to_string(),
};
self.client
.execute(
self.address(),
self.vesting_contract_address(),
&req,
fee,
"VestingContract::TransferOwnership",
vec![],
)
.await
}
}
@@ -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";
@@ -9,7 +9,7 @@ use crate::utils::{pretty_cosmwasm_coin, show_error_passthrough};
use comfy_table::Table;
use cosmwasm_std::Addr;
use mixnet_contract_common::{Delegation, PendingEpochEvent, PendingEpochEventData};
use mixnet_contract_common::{Delegation, PendingEpochEvent, PendingEpochEventKind};
#[derive(Debug, Parser)]
pub struct Args {}
@@ -90,8 +90,8 @@ async fn print_delegation_events(
]);
for event in events {
match event.event {
PendingEpochEventData::Delegate {
match event.event.kind {
PendingEpochEventKind::Delegate {
owner,
mix_id,
amount,
@@ -107,7 +107,7 @@ async fn print_delegation_events(
]);
}
}
PendingEpochEventData::Undelegate {
PendingEpochEventKind::Undelegate {
owner,
mix_id,
proxy,
@@ -13,7 +13,7 @@ use validator_client::nymd::wallet::DirectSecp256k1HdWallet;
pub struct SignatureOutputJson {
pub account_id: String,
pub public_key: PublicKey,
pub signature: String,
pub signature_as_hex: String,
}
#[derive(Debug, Parser)]
@@ -46,7 +46,7 @@ pub fn sign(args: Args, prefix: &str, mnemonic: Option<bip39::Mnemonic>) {
let output = SignatureOutputJson {
account_id: account.address().to_string(),
public_key: account.public_key(),
signature: signature.to_string(),
signature_as_hex: signature.to_string(),
};
println!("{}", json!(output));
}
@@ -12,6 +12,7 @@ use validator_client::nymd::AccountId;
use validator_client::nymd::VestingSigningClient;
use validator_client::nymd::{CosmosCoin, Denom};
use vesting_contract_common::messages::VestingSpecification;
use vesting_contract_common::PledgeCap;
use crate::context::SigningClient;
@@ -34,6 +35,12 @@ pub struct Args {
#[clap(long)]
pub staking_address: Option<String>,
#[clap(
long,
help = "Pledge cap as either absolute uNYM value or percentage, floats need to be in the 0.0 to 1.0 range and will be parsed as percentages, integers will be parsed as uNYM"
)]
pub pledge_cap: Option<PledgeCap>,
}
pub async fn create(args: Args, client: SigningClient, network_details: &NymNetworkDetails) {
@@ -55,6 +62,7 @@ pub async fn create(args: Args, client: SigningClient, network_details: &NymNetw
args.staking_address,
Some(vesting),
coin.into(),
args.pledge_cap,
None,
)
.await
@@ -9,3 +9,8 @@ edition = "2021"
[dependencies]
cosmwasm-std = "1.0.0"
serde = { version = "1.0", features = ["derive"] }
schemars = "0.8"
thiserror = "1"
[dev-dependencies]
serde_json = "1.0.0"
@@ -11,6 +11,8 @@ use cosmwasm_std::Event;
/// * `event`: event to search through.
/// * `key`: key associated with the particular attribute
pub fn must_find_attribute(event: &Event, key: &str) -> String {
// due to how the function is supposed to work, the unwrap is fine in this instance
#[allow(clippy::unwrap_used)]
may_find_attribute(event, key).unwrap()
}
@@ -1,6 +1,9 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![warn(clippy::expect_used)]
#![warn(clippy::unwrap_used)]
pub mod events;
pub mod types;
@@ -1,7 +1,128 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use serde::{Deserialize, Serialize};
use cosmwasm_std::Decimal;
use cosmwasm_std::Uint128;
use schemars::JsonSchema;
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serialize};
use std::fmt::{self, Display, Formatter};
use std::ops::Mul;
use std::str::FromStr;
use thiserror::Error;
pub fn truncate_decimal(amount: Decimal) -> Uint128 {
amount * Uint128::new(1)
}
#[derive(Error, Debug)]
pub enum ContractsCommonError {
#[error("Provided percent value ({0}) is greater than 100%")]
InvalidPercent(Decimal),
#[error("{source}")]
StdErr {
#[from]
source: cosmwasm_std::StdError,
},
}
/// Percent represents a value between 0 and 100%
/// (i.e. between 0.0 and 1.0)
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Serialize, Deserialize, JsonSchema,
)]
pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal);
impl Percent {
pub fn new(value: Decimal) -> Result<Self, ContractsCommonError> {
if value > Decimal::one() {
Err(ContractsCommonError::InvalidPercent(value))
} else {
Ok(Percent(value))
}
}
pub fn is_zero(&self) -> bool {
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))
}
pub fn value(&self) -> Decimal {
self.0
}
pub fn round_to_integer(&self) -> u8 {
let hundred = Decimal::from_ratio(100u32, 1u32);
// we know the cast from u128 to u8 is a safe one since the internal value must be within 0 - 1 range
truncate_decimal(hundred * self.0).u128() as u8
}
}
impl Display for Percent {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let adjusted = Decimal::from_ratio(100u32, 1u32) * self.0;
write!(f, "{}%", adjusted)
}
}
impl FromStr for Percent {
type Err = ContractsCommonError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Percent::new(Decimal::from_str(s)?)
}
}
impl Mul<Decimal> for Percent {
type Output = Decimal;
fn mul(self, rhs: Decimal) -> Self::Output {
self.0 * rhs
}
}
impl Mul<Percent> for Decimal {
type Output = Decimal;
fn mul(self, rhs: Percent) -> Self::Output {
rhs * self
}
}
impl Mul<Uint128> for Percent {
type Output = Uint128;
fn mul(self, rhs: Uint128) -> Self::Output {
self.0 * rhs
}
}
// implement custom Deserialize because we want to validate Percent has the correct range
fn de_decimal_percent<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
where
D: Deserializer<'de>,
{
let v = Decimal::deserialize(deserializer)?;
if v > Decimal::one() {
Err(D::Error::custom(
"provided decimal percent is larger than 100%",
))
} else {
Ok(v)
}
}
// TODO: there's no reason this couldn't be used for proper binaries, but in that case
// perhaps the struct should get renamed and moved to a "more" common crate
@@ -31,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);
}
}
@@ -1,7 +1,9 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::Decimal;
use cosmwasm_std::{Decimal, Uint128};
pub const TOKEN_SUPPLY: Uint128 = Uint128::new(1_000_000_000_000_000);
// I'm still not 100% sure how to feel about existence of this file
// This is equivalent of representing our display coin with 6 decimal places.
@@ -4,8 +4,10 @@
// due to code generated by JsonSchema
#![allow(clippy::field_reassign_with_default)]
use crate::constants::TOKEN_SUPPLY;
use crate::helpers::IntoBaseDecimal;
use crate::{Addr, MixId};
use cosmwasm_std::{Coin, Decimal};
use cosmwasm_std::{Coin, Decimal, StdResult};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -34,13 +36,11 @@ pub struct Delegation {
pub owner: Addr,
/// Id of the MixNode that this delegation was performed against.
#[serde(alias = "node_id")]
pub mix_id: MixId,
// Note to UI/UX devs: there's absolutely no point in displaying this value to the users,
// it would serve them no purpose. It's only used for calculating rewards
/// Value of the "unit delegation" associated with the mixnode at the time of delegation.
#[serde(alias = "crr")]
pub cumulative_reward_ratio: Decimal,
/// Original delegation amount. Note that it is never mutated as delegation accumulates rewards.
@@ -62,6 +62,11 @@ impl Delegation {
height: u64,
proxy: Option<Addr>,
) -> Self {
assert!(
amount.amount <= TOKEN_SUPPLY,
"delegation cannot be larger than the token supply"
);
Delegation {
owner,
mix_id,
@@ -89,10 +94,8 @@ impl Delegation {
(mix_id, owner_proxy_subkey)
}
pub fn dec_amount(&self) -> Decimal {
// the unwrap here is fine as we're guaranteed our base coin amount is going to fit in a Decimal
// with 0 decimal places
Decimal::from_atomics(self.amount.amount, 0).unwrap()
pub fn dec_amount(&self) -> StdResult<Decimal> {
self.amount.amount.into_base_decimal()
}
pub fn proxy_storage_key(&self) -> OwnerProxySubKey {
@@ -13,9 +13,6 @@ pub enum MixnetContractError {
source: cosmwasm_std::StdError,
},
#[error("Provided percent value is greater than 100%")]
InvalidPercent,
#[error("Attempted to subtract decimals with overflow ({minuend}.sub({subtrahend}))")]
OverflowDecimalSubtraction {
minuend: Decimal,
@@ -4,7 +4,7 @@
use crate::mixnode::{MixNodeConfigUpdate, MixNodeCostParams};
use crate::reward_params::{IntervalRewardParams, IntervalRewardingParamsUpdate};
use crate::rewarding::RewardDistribution;
use crate::{ContractStateParams, IdentityKeyRef, Interval, Layer, MixId};
use crate::{BlockHeight, ContractStateParams, IdentityKeyRef, Interval, Layer, MixId};
pub use contracts_common::events::*;
use cosmwasm_std::{Addr, Coin, Decimal, Event};
@@ -96,7 +96,7 @@ pub const PROXY_KEY: &str = "proxy";
// delegation/undelegation
pub const DELEGATOR_KEY: &str = "delegator";
pub const DELEGATION_TARGET_KEY: &str = "delegation_target";
pub const DELEGATION_HEIGHT_KEY: &str = "delegation_latest_block_height";
pub const UNIT_REWARD_KEY: &str = "unit_reward";
// bonding/unbonding
pub const MIX_ID_KEY: &str = "mix_id";
@@ -120,52 +120,45 @@ pub const UPDATED_MIXNODE_COST_PARAMS_KEY: &str = "updated_mixnode_cost_params";
// rewarding
pub const INTERVAL_KEY: &str = "interval_details";
pub const TOTAL_MIXNODE_REWARD_KEY: &str = "total_node_reward";
pub const TOTAL_PLEDGE_KEY: &str = "pledge";
pub const TOTAL_DELEGATIONS_KEY: &str = "delegated";
pub const OPERATOR_REWARD_KEY: &str = "operator_reward";
pub const DELEGATES_REWARD_KEY: &str = "delegates_reward";
pub const APPROXIMATE_TIME_LEFT_SECS_KEY: &str = "approximate_time_left_secs";
pub const INTERVAL_REWARDING_PARAMS_UPDATE_KEY: &str = "interval_rewarding_params_update";
pub const UPDATED_INTERVAL_REWARDING_PARAMS_KEY: &str = "updated_interval_rewarding_params";
pub const PRIOR_DELEGATES_KEY: &str = "prior_delegates";
pub const PRIOR_UNIT_REWARD: &str = "prior_unit_reward";
pub const PRIOR_UNIT_REWARD_KEY: &str = "prior_unit_reward";
pub const DISTRIBUTED_DELEGATION_REWARDS_KEY: &str = "distributed_delegation_rewards";
pub const FURTHER_DELEGATIONS_TO_REWARD_KEY: &str = "further_delegations";
pub const NO_REWARD_REASON_KEY: &str = "no_reward_reason";
pub const BOND_NOT_FOUND_VALUE: &str = "bond_not_found";
pub const BOND_TOO_FRESH_VALUE: &str = "bond_too_fresh";
pub const ZERO_PERFORMANCE_VALUE: &str = "zero_performance";
// rewarded set update
pub const ACTIVE_SET_SIZE_KEY: &str = "active_set_size";
pub const REWARDED_SET_SIZE_KEY: &str = "rewarded_set_size";
pub const NODES_IN_REWARDED_SET_KEY: &str = "nodes_in_rewarded_set";
pub const CURRENT_INTERVAL_ID_KEY: &str = "current_interval";
pub const NEW_CURRENT_INTERVAL_KEY: &str = "new_current_interval";
pub const NEW_CURRENT_EPOCH_KEY: &str = "new_current_epoch";
pub const BLOCK_HEIGHT_KEY: &str = "block_height";
pub const RECONCILIATION_ERROR_EVENT: &str = "reconciliation_error";
// interval
pub const EVENTS_EXECUTED_KEY: &str = "number_of_events_executed";
pub const EVENT_CREATION_HEIGHT_KEY: &str = "created_at";
pub const REWARDED_SET_NODES_KEY: &str = "rewarded_set_nodes";
pub const NEW_EPOCHS_DURATION_SECS_KEY: &str = "new_epoch_durations_secs";
pub const NEW_EPOCHS_IN_INTERVAL: &str = "new_epochs_in_interval";
pub fn new_delegation_event(
created_at: BlockHeight,
delegator: &Addr,
proxy: &Option<Addr>,
amount: &Coin,
mix_id: MixId,
unit_reward: Decimal,
) -> Event {
Event::new(MixnetEventType::Delegation)
.add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string())
.add_attribute(DELEGATOR_KEY, delegator)
.add_optional_attribute(PROXY_KEY, proxy.as_ref())
.add_attribute(AMOUNT_KEY, amount.to_string())
.add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string())
.add_attribute(UNIT_REWARD_KEY, unit_reward.to_string())
}
pub fn new_delegation_on_unbonded_node_event(
@@ -218,8 +211,9 @@ pub fn new_withdraw_delegator_reward_event(
.add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string())
}
pub fn new_active_set_update_event(new_size: u32) -> Event {
pub fn new_active_set_update_event(created_at: BlockHeight, new_size: u32) -> Event {
Event::new(MixnetEventType::ActiveSetUpdate)
.add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string())
.add_attribute(ACTIVE_SET_SIZE_KEY, new_size.to_string())
}
@@ -236,10 +230,13 @@ pub fn new_pending_active_set_update_event(
}
pub fn new_rewarding_params_update_event(
created_at: BlockHeight,
update: IntervalRewardingParamsUpdate,
updated: IntervalRewardParams,
) -> Event {
Event::new(MixnetEventType::IntervalRewardingParamsUpdate)
.add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string())
.add_attribute(
INTERVAL_REWARDING_PARAMS_UPDATE_KEY,
update.to_inline_json(),
@@ -265,8 +262,14 @@ pub fn new_pending_rewarding_params_update_event(
)
}
pub fn new_undelegation_event(delegator: &Addr, proxy: &Option<Addr>, mix_id: MixId) -> Event {
pub fn new_undelegation_event(
created_at: BlockHeight,
delegator: &Addr,
proxy: &Option<Addr>,
mix_id: MixId,
) -> Event {
Event::new(MixnetEventType::Undelegation)
.add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string())
.add_attribute(DELEGATOR_KEY, delegator)
.add_optional_attribute(PROXY_KEY, proxy.as_ref())
.add_attribute(MIX_ID_KEY, mix_id.to_string())
@@ -327,8 +330,10 @@ pub fn new_mixnode_bonding_event(
.add_attribute(AMOUNT_KEY, amount.to_string())
}
pub fn new_mixnode_unbonding_event(mix_id: MixId) -> Event {
Event::new(MixnetEventType::MixnodeUnbonding).add_attribute(MIX_ID_KEY, mix_id.to_string())
pub fn new_mixnode_unbonding_event(created_at: BlockHeight, mix_id: MixId) -> Event {
Event::new(MixnetEventType::MixnodeUnbonding)
.add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string())
.add_attribute(MIX_ID_KEY, mix_id.to_string())
}
pub fn new_pending_mixnode_unbonding_event(
@@ -370,8 +375,13 @@ pub fn new_mixnode_pending_cost_params_update_event(
.add_attribute(UPDATED_MIXNODE_COST_PARAMS_KEY, new_costs.to_inline_json())
}
pub fn new_mixnode_cost_params_update_event(mix_id: MixId, new_costs: &MixNodeCostParams) -> Event {
pub fn new_mixnode_cost_params_update_event(
created_at: BlockHeight,
mix_id: MixId,
new_costs: &MixNodeCostParams,
) -> Event {
Event::new(MixnetEventType::MixnodeCostParamsUpdate)
.add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string())
.add_attribute(MIX_ID_KEY, mix_id.to_string())
.add_attribute(UPDATED_MIXNODE_COST_PARAMS_KEY, new_costs.to_inline_json())
}
@@ -461,7 +471,7 @@ pub fn new_mix_rewarding_event(
interval.current_epoch_absolute_id().to_string(),
)
.add_attribute(PRIOR_DELEGATES_KEY, prior_delegates.to_string())
.add_attribute(PRIOR_UNIT_REWARD, prior_unit_reward.to_string())
.add_attribute(PRIOR_UNIT_REWARD_KEY, prior_unit_reward.to_string())
.add_attribute(MIX_ID_KEY, mix_id.to_string())
.add_attribute(
OPERATOR_REWARD_KEY,
@@ -497,11 +507,13 @@ pub fn new_reconcile_pending_events() -> Event {
}
pub fn new_interval_config_update_event(
created_at: BlockHeight,
epochs_in_interval: u32,
epoch_duration_secs: u64,
updated_rewarding_params: IntervalRewardParams,
) -> Event {
Event::new(MixnetEventType::IntervalConfigUpdate)
.add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string())
.add_attribute(
NEW_EPOCHS_DURATION_SECS_KEY,
epoch_duration_secs.to_string(),
@@ -1,13 +1,33 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::Decimal;
use cosmwasm_std::{Decimal, StdError, StdResult, Uint128};
pub fn compare_decimals(a: Decimal, b: Decimal) {
let epsilon = Decimal::from_ratio(1u128, 100_000_000u128);
pub fn compare_decimals(a: Decimal, b: Decimal, epsilon: Option<Decimal>) {
let epsilon = epsilon.unwrap_or_else(|| Decimal::from_ratio(1u128, 100_000_000u128));
if a > b {
assert!(a - b < epsilon, "{} != {}", a, b)
} else {
assert!(b - a < epsilon, "{} != {}", a, b)
}
}
pub fn into_base_decimal(val: impl Into<Uint128>) -> StdResult<Decimal> {
val.into_base_decimal()
}
pub trait IntoBaseDecimal {
fn into_base_decimal(self) -> StdResult<Decimal>;
}
impl<T> IntoBaseDecimal for T
where
T: Into<Uint128>,
{
fn into_base_decimal(self) -> StdResult<Decimal> {
let atomics = self.into();
Decimal::from_atomics(atomics, 0).map_err(|_| StdError::GenericErr {
msg: format!("Decimal range exceeded for {atomics} with 0 decimal places."),
})
}
}
@@ -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; }"))]
@@ -142,14 +145,17 @@ impl JsonSchema for Interval {
impl Interval {
/// Initialize epoch in the contract with default values.
pub fn init_interval(epochs_in_interval: u32, epoch_length: Duration, env: &Env) -> Self {
// if this fails it means the value provided from the chain itself (via cosmwasm) is invalid,
// so we really have to panic here as anything beyond that point would be invalid anyway
#[allow(clippy::expect_used)]
let current_epoch_start =
OffsetDateTime::from_unix_timestamp(env.block.time.seconds() as i64)
.expect("The timestamp provided via env.block.time is invalid");
Interval {
id: 0,
epochs_in_interval,
// I really don't see a way for this to fail, unless the blockchain is lying to us
current_epoch_start: OffsetDateTime::from_unix_timestamp(
env.block.time.seconds() as i64
)
.expect("Invalid timestamp from env.block.time"),
current_epoch_start,
current_epoch_id: 0,
epoch_length,
total_elapsed_epochs: 0,
@@ -1,6 +1,9 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![warn(clippy::expect_used)]
#![warn(clippy::unwrap_used)]
mod constants;
pub mod delegation;
pub mod error;
@@ -34,7 +37,8 @@ pub use mixnode::{
};
pub use msg::*;
pub use pending_events::{
PendingEpochEvent, PendingEpochEventData, PendingIntervalEvent, PendingIntervalEventData,
PendingEpochEvent, PendingEpochEventData, PendingEpochEventKind, PendingIntervalEvent,
PendingIntervalEventData, PendingIntervalEventKind,
};
pub use reward_params::{IntervalRewardParams, IntervalRewardingParamsUpdate, RewardingParams};
pub use types::*;
@@ -4,13 +4,14 @@
// due to code generated by JsonSchema
#![allow(clippy::field_reassign_with_default)]
use crate::constants::UNIT_DELEGATION_BASE;
use crate::constants::{TOKEN_SUPPLY, UNIT_DELEGATION_BASE};
use crate::error::MixnetContractError;
use crate::helpers::IntoBaseDecimal;
use crate::reward_params::{NodeRewardParams, RewardingParams};
use crate::rewarding::helpers::truncate_reward;
use crate::rewarding::RewardDistribution;
use crate::{Delegation, EpochId, IdentityKey, MixId, Percent, SphinxKey};
use cosmwasm_std::{Addr, Coin, Decimal, Uint128};
use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
@@ -64,7 +65,7 @@ impl MixNodeDetails {
self.rewarding_details.pending_operator_reward(pledge)
}
pub fn pending_detailed_operator_reward(&self) -> Decimal {
pub fn pending_detailed_operator_reward(&self) -> StdResult<Decimal> {
let pledge = self.original_pledge();
self.rewarding_details
.pending_detailed_operator_reward(pledge)
@@ -78,34 +79,27 @@ impl MixNodeDetails {
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
pub struct MixNodeRewarding {
/// Information provided by the operator that influence the cost function.
#[serde(alias = "cp")]
pub cost_params: MixNodeCostParams,
/// Total pledge and compounded reward earned by the node operator.
#[serde(alias = "op")]
pub operator: Decimal,
/// Total delegation and compounded reward earned by all node delegators.
#[serde(alias = "dg")]
pub delegates: Decimal,
/// Cumulative reward earned by the "unit delegation" since the block 0.
#[serde(alias = "tur")]
pub total_unit_reward: Decimal,
/// Value of the theoretical "unit delegation" that has delegated to this mixnode at block 0.
#[serde(alias = "ud")]
pub unit_delegation: Decimal,
/// Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt
/// to reward it multiple times in the same epoch.
#[serde(alias = "le")]
pub last_rewarded_epoch: EpochId,
// technically we don't need that field to determine reward magnitude or anything
// but it saves on extra queries to determine if we're removing the final delegation
// (so that we could zero the field correctly)
#[serde(alias = "uqd")]
pub unique_delegations: u32,
}
@@ -114,16 +108,21 @@ impl MixNodeRewarding {
cost_params: MixNodeCostParams,
initial_pledge: &Coin,
current_epoch: EpochId,
) -> Self {
MixNodeRewarding {
) -> Result<Self, MixnetContractError> {
assert!(
initial_pledge.amount <= TOKEN_SUPPLY,
"pledge cannot be larger than the token supply"
);
Ok(MixNodeRewarding {
cost_params,
operator: Decimal::from_atomics(initial_pledge.amount, 0).unwrap(),
operator: initial_pledge.amount.into_base_decimal()?,
delegates: Decimal::zero(),
total_unit_reward: Decimal::zero(),
unit_delegation: UNIT_DELEGATION_BASE,
last_rewarded_epoch: current_epoch,
unique_delegations: 0,
}
})
}
/// Determines whether this node is still bonded. This is performed via a simple check,
@@ -142,27 +141,30 @@ impl MixNodeRewarding {
}
}
pub fn pending_detailed_operator_reward(&self, original_pledge: &Coin) -> Decimal {
let initial_dec = Decimal::from_atomics(original_pledge.amount, 0).unwrap();
pub fn pending_detailed_operator_reward(&self, original_pledge: &Coin) -> StdResult<Decimal> {
let initial_dec = original_pledge.amount.into_base_decimal()?;
if initial_dec > self.operator {
panic!(
"seems slashing has occurred while it has not been implemented nor accounted for!"
)
}
self.operator - initial_dec
Ok(self.operator - initial_dec)
}
pub fn operator_pledge_with_reward(&self, denom: impl Into<String>) -> Coin {
truncate_reward(self.operator, denom)
}
pub fn pending_delegator_reward(&self, delegation: &Delegation) -> Coin {
let delegator_reward = self.determine_delegation_reward(delegation);
truncate_reward(delegator_reward, &delegation.amount.denom)
pub fn pending_delegator_reward(&self, delegation: &Delegation) -> StdResult<Coin> {
let delegator_reward = self.determine_delegation_reward(delegation)?;
Ok(truncate_reward(delegator_reward, &delegation.amount.denom))
}
pub fn withdraw_operator_reward(&mut self, original_pledge: &Coin) -> Coin {
let initial_dec = Decimal::from_atomics(original_pledge.amount, 0).unwrap();
pub fn withdraw_operator_reward(
&mut self,
original_pledge: &Coin,
) -> Result<Coin, MixnetContractError> {
let initial_dec = original_pledge.amount.into_base_decimal()?;
if initial_dec > self.operator {
panic!(
"seems slashing has occurred while it has not been implemented nor accounted for!"
@@ -171,14 +173,14 @@ impl MixNodeRewarding {
let diff = self.operator - initial_dec;
self.operator = initial_dec;
truncate_reward(diff, &original_pledge.denom)
Ok(truncate_reward(diff, &original_pledge.denom))
}
pub fn withdraw_delegator_reward(
&mut self,
delegation: &mut Delegation,
) -> Result<Coin, MixnetContractError> {
let reward = self.determine_delegation_reward(delegation);
let reward = self.determine_delegation_reward(delegation)?;
self.decrease_delegates_decimal(reward)?;
delegation.cumulative_reward_ratio = self.full_reward_ratio();
@@ -308,23 +310,27 @@ impl MixNodeRewarding {
self.distribute_rewards(reward_distribution, absolute_epoch_id)
}
pub fn determine_delegation_reward(&self, delegation: &Delegation) -> Decimal {
pub fn determine_delegation_reward(&self, delegation: &Delegation) -> StdResult<Decimal> {
let starting_ratio = delegation.cumulative_reward_ratio;
let ending_ratio = self.full_reward_ratio();
let adjust = starting_ratio + self.unit_delegation;
(ending_ratio - starting_ratio) * delegation.dec_amount() / adjust
Ok((ending_ratio - starting_ratio) * delegation.dec_amount()? / adjust)
}
// this updates `unique_delegations` field
pub fn add_base_delegation(&mut self, amount: Uint128) {
self.increase_delegates_uint128(amount);
pub fn add_base_delegation(&mut self, amount: Uint128) -> Result<(), MixnetContractError> {
self.increase_delegates_uint128(amount)?;
self.unique_delegations += 1;
Ok(())
}
pub fn increase_delegates_uint128(&mut self, amount: Uint128) {
// the unwrap here is fine as the value is guaranteed to fit under provided constraints
self.delegates += Decimal::from_atomics(amount, 0).unwrap()
pub fn increase_delegates_uint128(
&mut self,
amount: Uint128,
) -> Result<(), MixnetContractError> {
self.delegates += amount.into_base_decimal()?;
Ok(())
}
// this updates `unique_delegations` field
@@ -342,7 +348,7 @@ impl MixNodeRewarding {
&mut self,
amount: Uint128,
) -> Result<(), MixnetContractError> {
let amount_dec = Decimal::from_atomics(amount, 0).unwrap();
let amount_dec = amount.into_base_decimal()?;
self.decrease_delegates_decimal(amount_dec)
}
@@ -375,8 +381,8 @@ impl MixNodeRewarding {
}
pub fn undelegate(&mut self, delegation: &Delegation) -> Result<Coin, MixnetContractError> {
let reward = self.determine_delegation_reward(delegation);
let full_amount = reward + delegation.dec_amount();
let reward = self.determine_delegation_reward(delegation)?;
let full_amount = reward + delegation.dec_amount()?;
self.remove_delegation_decimal(full_amount)?;
Ok(truncate_reward(full_amount, &delegation.amount.denom))
}
@@ -428,7 +434,6 @@ impl MixNodeRewarding {
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
pub struct MixNodeBond {
/// Unique id assigned to the bonded mixnode.
#[serde(alias = "id")]
pub mix_id: MixId,
/// Address of the owner of this mixnode.
@@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
use crate::delegation::OwnerProxySubKey;
use crate::error::MixnetContractError;
use crate::helpers::IntoBaseDecimal;
use crate::mixnode::{MixNodeConfigUpdate, MixNodeCostParams};
use crate::reward_params::{
IntervalRewardParams, IntervalRewardingParamsUpdate, Performance, RewardingParams,
@@ -31,6 +33,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,
@@ -40,17 +43,21 @@ pub struct InitialRewardingParams {
}
impl InitialRewardingParams {
pub fn into_rewarding_params(self, epochs_in_interval: u32) -> RewardingParams {
pub fn into_rewarding_params(
self,
epochs_in_interval: u32,
) -> Result<RewardingParams, MixnetContractError> {
let epoch_reward_budget = self.initial_reward_pool
/ Decimal::from_atomics(epochs_in_interval, 0).unwrap()
/ epochs_in_interval.into_base_decimal()?
* self.interval_pool_emission;
let stake_saturation_point =
self.initial_staking_supply / Decimal::from_atomics(self.rewarded_set_size, 0).unwrap();
self.initial_staking_supply / self.rewarded_set_size.into_base_decimal()?;
RewardingParams {
Ok(RewardingParams {
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,
@@ -59,7 +66,7 @@ impl InitialRewardingParams {
},
rewarded_set_size: self.rewarded_set_size,
active_set_size: self.active_set_size,
}
})
}
}
@@ -406,4 +413,6 @@ pub enum QueryMsg {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct MigrateMsg {}
pub struct MigrateMsg {
pub vesting_contract_address: Option<String>,
}
@@ -3,7 +3,7 @@
use crate::mixnode::MixNodeCostParams;
use crate::reward_params::IntervalRewardingParamsUpdate;
use crate::{EpochEventId, IntervalEventId, MixId};
use crate::{BlockHeight, EpochEventId, IntervalEventId, MixId};
use cosmwasm_std::{Addr, Coin};
use serde::{Deserialize, Serialize};
@@ -14,7 +14,13 @@ pub struct PendingEpochEvent {
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum PendingEpochEventData {
pub struct PendingEpochEventData {
pub created_at: BlockHeight,
pub kind: PendingEpochEventKind,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum PendingEpochEventKind {
// can't just pass the `Delegation` struct here as it's impossible to determine
// `cumulative_reward_ratio` ahead of time
Delegate {
@@ -36,6 +42,15 @@ pub enum PendingEpochEventData {
},
}
impl PendingEpochEventKind {
pub fn attach_source_height(self, created_at: BlockHeight) -> PendingEpochEventData {
PendingEpochEventData {
created_at,
kind: self,
}
}
}
impl From<(EpochEventId, PendingEpochEventData)> for PendingEpochEvent {
fn from(data: (EpochEventId, PendingEpochEventData)) -> Self {
PendingEpochEvent {
@@ -52,7 +67,13 @@ pub struct PendingIntervalEvent {
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum PendingIntervalEventData {
pub struct PendingIntervalEventData {
pub created_at: BlockHeight,
pub kind: PendingIntervalEventKind,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum PendingIntervalEventKind {
ChangeMixCostParams {
mix_id: MixId,
new_costs: MixNodeCostParams,
@@ -67,6 +88,15 @@ pub enum PendingIntervalEventData {
},
}
impl PendingIntervalEventKind {
pub fn attach_source_height(self, created_at: BlockHeight) -> PendingIntervalEventData {
PendingIntervalEventData {
created_at,
kind: self,
}
}
}
impl From<(IntervalEventId, PendingIntervalEventData)> for PendingIntervalEvent {
fn from(data: (IntervalEventId, PendingIntervalEventData)) -> Self {
PendingIntervalEvent {
@@ -1,6 +1,7 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::helpers::IntoBaseDecimal;
use crate::{error::MixnetContractError, Percent};
use cosmwasm_std::Decimal;
use schemars::JsonSchema;
@@ -26,6 +27,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.
@@ -105,24 +111,33 @@ impl RewardingParams {
pub fn dec_rewarded_set_size(&self) -> Decimal {
// the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal
// with 0 decimal places
Decimal::from_atomics(self.rewarded_set_size, 0).unwrap()
#[allow(clippy::unwrap_used)]
self.rewarded_set_size.into_base_decimal().unwrap()
}
pub fn dec_active_set_size(&self) -> Decimal {
// the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal
// with 0 decimal places
Decimal::from_atomics(self.active_set_size, 0).unwrap()
#[allow(clippy::unwrap_used)]
self.active_set_size.into_base_decimal().unwrap()
}
fn dec_standby_set_size(&self) -> Decimal {
// the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal
// with 0 decimal places
Decimal::from_atomics(self.rewarded_set_size - self.active_set_size, 0).unwrap()
#[allow(clippy::unwrap_used)]
(self.rewarded_set_size - self.active_set_size)
.into_base_decimal()
.unwrap()
}
pub fn apply_epochs_in_interval_change(&mut self, new_epochs_in_interval: u32) {
self.interval.epoch_reward_budget = self.interval.reward_pool
/ Decimal::from_atomics(new_epochs_in_interval, 0).unwrap()
// the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal
// with 0 decimal places
#[allow(clippy::unwrap_used)]
let new_epochs_in_interval = new_epochs_in_interval.into_base_decimal().unwrap();
self.interval.epoch_reward_budget = self.interval.reward_pool / new_epochs_in_interval
* self.interval.interval_pool_emission;
}
@@ -164,6 +179,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;
}
@@ -191,13 +210,13 @@ impl RewardingParams {
if recompute_epoch_budget {
self.interval.epoch_reward_budget = self.interval.reward_pool
/ Decimal::from_atomics(epochs_in_interval, 0).unwrap()
/ epochs_in_interval.into_base_decimal()?
* self.interval.interval_pool_emission;
}
if recompute_saturation_point {
self.interval.stake_saturation_point = self.interval.staking_supply
/ Decimal::from_atomics(self.rewarded_set_size, 0).unwrap();
self.interval.stake_saturation_point =
self.interval.staking_supply / self.rewarded_set_size.into_base_decimal()?
}
Ok(())
@@ -235,6 +254,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 +274,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()
@@ -1,6 +1,7 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use contracts_common::truncate_decimal;
use cosmwasm_std::{Coin, Decimal, Uint128};
/// Truncates all decimal points so that the reward would fit in a `Coin` and so that we would
@@ -17,7 +18,3 @@ pub fn truncate_reward(reward: Decimal, denom: impl Into<String>) -> Coin {
pub fn truncate_reward_amount(reward: Decimal) -> Uint128 {
truncate_decimal(reward)
}
pub fn truncate_decimal(amount: Decimal) -> Uint128 {
amount * Uint128::new(1)
}
@@ -29,7 +29,7 @@ pub struct RewardEstimate {
pub operating_cost: Decimal,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
pub struct RewardDistribution {
pub operator: Decimal,
pub delegates: Decimal,
@@ -1,457 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::MixnetContractError;
use crate::mixnode::{MixNodeCostParams, MixNodeRewarding};
use crate::reward_params::{IntervalRewardParams, NodeRewardParams, RewardingParams};
use crate::rewarding::helpers::truncate_reward;
use crate::rewarding::RewardDistribution;
use crate::{Delegation, Interval, Percent};
use cosmwasm_std::{Addr, Coin, Decimal};
pub struct Simulator {
pub node_rewarding_details: MixNodeRewarding,
pub node_delegations: Vec<Delegation>,
pub system_rewarding_params: RewardingParams,
pub interval: Interval,
}
impl Simulator {
pub fn new(
profit_margin_percent: Percent,
interval_operating_cost: Coin,
system_rewarding_params: RewardingParams,
initial_pledge: Coin,
interval: Interval,
) -> Self {
let cost_params = MixNodeCostParams {
profit_margin_percent,
interval_operating_cost,
};
Simulator {
node_rewarding_details: MixNodeRewarding::initialise_new(
cost_params,
&initial_pledge,
Default::default(),
),
node_delegations: vec![],
system_rewarding_params,
interval,
}
}
pub fn delegate(&mut self, amount: Coin) {
let cumulative_reward_ratio = self.node_rewarding_details.total_unit_reward;
// let record = self.node_rewarding_details.increment_period();
// self.node_historical_records.insert(period, record);
self.node_rewarding_details
.add_base_delegation(amount.amount);
// we don't care about the owner/node details here
self.node_delegations.push(Delegation::new(
Addr::unchecked("bob"),
42,
cumulative_reward_ratio,
amount,
123,
None,
))
}
pub fn determine_delegation_reward(&self, delegation: &Delegation) -> Decimal {
self.node_rewarding_details
.determine_delegation_reward(delegation)
}
// since this is a simulator only, not something to be used in the production code, the unwraps are fine
// if user inputs are invalid
pub fn undelegate(
&mut self,
delegation_index: usize,
) -> Result<(Coin, Coin), MixnetContractError> {
let delegation = self.node_delegations.remove(delegation_index);
let reward = self.determine_delegation_reward(&delegation);
self.node_rewarding_details
.decrease_delegates_decimal(delegation.dec_amount() + reward)?;
self.node_rewarding_details.unique_delegations -= 1;
let reward_denom = &delegation.amount.denom;
let truncated_reward = truncate_reward(reward, reward_denom);
// if this was last delegation, move all leftover decimal tokens to the operator
// (this is literally in the order of a millionth of a micronym)
if self.node_delegations.is_empty() {
assert_eq!(self.node_rewarding_details.unique_delegations, 0);
self.node_rewarding_details.operator += self.node_rewarding_details.delegates;
self.node_rewarding_details.delegates = Decimal::zero();
}
Ok((delegation.amount, truncated_reward))
}
pub fn determine_total_delegation_reward(&self) -> Decimal {
let mut total = Decimal::zero();
for delegation in &self.node_delegations {
total += self.determine_delegation_reward(delegation)
}
total
}
pub fn simulate_epoch(&mut self, node_params: NodeRewardParams) -> RewardDistribution {
let reward_distribution = self.node_rewarding_details.calculate_epoch_reward(
&self.system_rewarding_params,
node_params,
self.interval.epochs_in_interval(),
);
self.node_rewarding_details.distribute_rewards(
reward_distribution,
self.interval.current_epoch_absolute_id(),
);
self.interval = self.interval.advance_epoch();
reward_distribution
}
// assume node state doesn't change in the interval (kinda unrealistic)
pub fn simulate_interval(&mut self, node_params: NodeRewardParams) {
assert_eq!(self.interval.current_epoch_id(), 0);
let id = self.interval.current_interval_id();
let mut distributed = Decimal::zero();
for _ in 0..self.interval.epochs_in_interval() {
let distr = self.simulate_epoch(node_params);
distributed += distr.operator;
distributed += distr.delegates;
}
assert_eq!(id + 1, self.interval.current_interval_id());
// update reward pool and all of that
let old = self.system_rewarding_params.interval;
let reward_pool = old.reward_pool - distributed;
let staking_supply = old.staking_supply + distributed;
let epoch_reward_budget = reward_pool
/ Decimal::from_atomics(self.interval.epochs_in_interval(), 0).unwrap()
* old.interval_pool_emission.value();
let stake_saturation_point = staking_supply
/ Decimal::from_atomics(self.system_rewarding_params.rewarded_set_size, 0).unwrap();
let updated_params = RewardingParams {
interval: IntervalRewardParams {
reward_pool,
staking_supply,
epoch_reward_budget,
stake_saturation_point,
sybil_resistance: old.sybil_resistance,
active_set_work_factor: old.active_set_work_factor,
interval_pool_emission: old.interval_pool_emission,
},
rewarded_set_size: self.system_rewarding_params.rewarded_set_size,
active_set_size: self.system_rewarding_params.active_set_size,
};
self.system_rewarding_params = updated_params;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::helpers::compare_decimals;
use crate::reward_params::IntervalRewardParams;
use crate::rewarding::helpers::truncate_reward_amount;
use cosmwasm_std::testing::mock_env;
use cosmwasm_std::{coin, Uint128};
use std::time::Duration;
fn base_simulator(initial_pledge: u128) -> Simulator {
let profit_margin = Percent::from_percentage_value(10).unwrap();
let interval_operating_cost = Coin::new(40_000_000, "unym");
let epochs_in_interval = 720u32;
let rewarded_set_size = 240;
let active_set_size = 100;
let interval_pool_emission = Percent::from_percentage_value(2).unwrap();
let reward_pool = 250_000_000_000_000u128;
let staking_supply = 100_000_000_000_000u128;
let epoch_reward_budget =
interval_pool_emission * Decimal::from_ratio(reward_pool, epochs_in_interval);
let stake_saturation_point = Decimal::from_ratio(staking_supply, rewarded_set_size);
let rewarding_params = RewardingParams {
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
epoch_reward_budget,
stake_saturation_point,
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
active_set_work_factor: Decimal::percent(1000), // value '10'
interval_pool_emission,
},
rewarded_set_size,
active_set_size,
};
let interval = Interval::init_interval(
epochs_in_interval,
Duration::from_secs(60 * 60),
&mock_env(),
);
let initial_pledge = Coin::new(initial_pledge, "unym");
Simulator::new(
profit_margin,
interval_operating_cost,
rewarding_params,
initial_pledge,
interval,
)
}
// essentially our delegations + estimated rewards HAVE TO equal to what we actually determined
fn check_rewarding_invariant(simulator: &Simulator) {
let delegation_sum: Decimal = simulator
.node_delegations
.iter()
.map(|d| d.dec_amount())
.sum();
let reward_sum = simulator.determine_total_delegation_reward();
compare_decimals(
delegation_sum + reward_sum,
simulator.node_rewarding_details.delegates,
)
}
#[test]
fn simulator_returns_expected_values_for_base_case() {
let mut simulator = base_simulator(10000_000000);
let epoch_params =
NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
let rewards = simulator.simulate_epoch(epoch_params);
assert_eq!(rewards.delegates, Decimal::zero());
compare_decimals(rewards.operator, "1128452.5416104363".parse().unwrap());
}
#[test]
fn single_delegation_at_genesis() {
let mut simulator = base_simulator(10000_000000);
simulator.delegate(Coin::new(18000_000000, "unym"));
let node_params = NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
let rewards = simulator.simulate_epoch(node_params);
compare_decimals(rewards.delegates, "1795950.2602660495".parse().unwrap());
compare_decimals(rewards.operator, "1363716.856243172".parse().unwrap());
compare_decimals(
rewards.delegates,
simulator.determine_total_delegation_reward(),
);
assert_eq!(
simulator.node_rewarding_details.operator,
rewards.operator + Decimal::from_atomics(10000_000000u128, 0).unwrap()
);
assert_eq!(
simulator.node_rewarding_details.delegates,
rewards.delegates + Decimal::from_atomics(18000_000000u128, 0).unwrap()
);
}
#[test]
fn delegation_and_undelegation() {
let mut simulator = base_simulator(10000_000000);
let node_params = NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
let rewards1 = simulator.simulate_epoch(node_params);
let expected_operator1 = "1128452.5416104363".parse().unwrap();
assert_eq!(rewards1.delegates, Decimal::zero());
compare_decimals(rewards1.operator, expected_operator1);
simulator.delegate(Coin::new(18000_000000, "unym"));
let rewards2 = simulator.simulate_epoch(node_params);
let expected_operator2 = "1363843.413584609".parse().unwrap();
let expected_delegator_reward1 = "1795952.25874404".parse().unwrap();
compare_decimals(rewards2.delegates, expected_delegator_reward1);
compare_decimals(rewards2.operator, expected_operator2);
let rewards3 = simulator.simulate_epoch(node_params);
let expected_operator3 = "1364017.7824440491".parse().unwrap();
let expected_delegator_reward2 = "1796135.9269468693".parse().unwrap();
compare_decimals(rewards3.delegates, expected_delegator_reward2);
compare_decimals(rewards3.operator, expected_operator3);
let (delegation, reward) = simulator.undelegate(0).unwrap();
assert_eq!(delegation.amount.u128(), 18000_000000);
assert_eq!(
reward.amount,
(expected_delegator_reward1 + expected_delegator_reward2) * Uint128::new(1)
);
let base_op = Decimal::from_atomics(10000_000000u128, 0).unwrap();
compare_decimals(
simulator.node_rewarding_details.operator,
base_op + expected_operator1 + expected_operator2 + expected_operator3,
);
assert_eq!(Decimal::zero(), simulator.node_rewarding_details.delegates);
}
#[test]
fn withdrawing_operator_reward() {
// essentially all delegators' rewards (and the operator itself) are still correctly computed
let original_pledge = coin(10000_000000, "unym");
let mut simulator = base_simulator(original_pledge.amount.u128());
let node_params = NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
// add 2 delegations at genesis (because it makes things easier and as shown with previous tests
// delegating at different times still work)
simulator.delegate(Coin::new(18000_000000, "unym"));
simulator.delegate(Coin::new(4000_000000, "unym"));
// "normal", sanity check rewarding
let rewards1 = simulator.simulate_epoch(node_params);
let expected_operator1 = "1411087.1007647323".parse().unwrap();
let expected_delegator_reward1 = "2199961.032388664".parse().unwrap();
compare_decimals(rewards1.delegates, expected_delegator_reward1);
compare_decimals(rewards1.operator, expected_operator1);
check_rewarding_invariant(&simulator);
let reward = simulator
.node_rewarding_details
.withdraw_operator_reward(&original_pledge);
assert_eq!(reward.amount, truncate_reward_amount(expected_operator1));
assert_eq!(
simulator.node_rewarding_details.operator,
Decimal::from_atomics(original_pledge.amount, 0).unwrap()
);
let rewards2 = simulator.simulate_epoch(node_params);
let expected_operator2 = "1411113.0004067947".parse().unwrap();
let expected_delegator_reward2 = "2200183.3879084454".parse().unwrap();
compare_decimals(rewards2.delegates, expected_delegator_reward2);
compare_decimals(rewards2.operator, expected_operator2);
check_rewarding_invariant(&simulator);
}
#[test]
fn withdrawing_delegator_reward() {
// essentially all delegators' rewards (and the operator itself) are still correctly computed
let mut simulator = base_simulator(10000_000000);
let node_params = NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
// add 2 delegations at genesis (because it makes things easier and as shown with previous tests
// delegating at different times still work)
simulator.delegate(Coin::new(18000_000000, "unym"));
simulator.delegate(Coin::new(4000_000000, "unym"));
// "normal", sanity check rewarding
let rewards1 = simulator.simulate_epoch(node_params);
let expected_operator1 = "1411087.1007647323".parse().unwrap();
let expected_delegator_reward1 = "2199961.032388664".parse().unwrap();
compare_decimals(rewards1.delegates, expected_delegator_reward1);
compare_decimals(rewards1.operator, expected_operator1);
check_rewarding_invariant(&simulator);
// reference to our `18000_000000` delegation
let delegation1 = &mut simulator.node_delegations[0];
let reward = simulator
.node_rewarding_details
.withdraw_delegator_reward(delegation1)
.unwrap();
let expected_del1_reward = "1799968.1174089068".parse().unwrap();
assert_eq!(reward.amount, truncate_reward_amount(expected_del1_reward));
// new reward after withdrawal
let rewards2 = simulator.simulate_epoch(node_params);
let expected_operator2 = "1411250.1907492676".parse().unwrap();
let expected_delegator_reward2 = "2200004.051009689".parse().unwrap();
compare_decimals(rewards2.delegates, expected_delegator_reward2);
compare_decimals(rewards2.operator, expected_operator2);
check_rewarding_invariant(&simulator);
// check final values
let reward_del1 = simulator
.node_rewarding_details
.withdraw_delegator_reward(&mut simulator.node_delegations[0])
.unwrap();
let expected_del1_reward = "1799970.5883041779".parse().unwrap();
assert_eq!(
reward_del1.amount,
truncate_reward_amount(expected_del1_reward)
);
let reward_del2 = simulator
.node_rewarding_details
.withdraw_delegator_reward(&mut simulator.node_delegations[1])
.unwrap();
let first: Decimal = "399992.91497975704".parse().unwrap();
let second: Decimal = "400033.4627055114".parse().unwrap();
let expected_del2_reward = first + second;
assert_eq!(
reward_del2.amount,
truncate_reward_amount(expected_del2_reward)
);
}
#[test]
fn simulating_multiple_epochs() {
let mut simulator = base_simulator(10000_000000);
let mut is_active = true;
let mut performance = Percent::from_percentage_value(100).unwrap();
for epoch in 0..720 {
if epoch == 0 {
simulator.delegate(Coin::new(18000_000000, "unym"))
}
if epoch == 42 {
simulator.delegate(Coin::new(2000_000000, "unym"))
}
if epoch == 89 {
is_active = false;
}
if epoch == 123 {
simulator.delegate(Coin::new(6666_000000, "unym"))
}
if epoch == 167 {
performance = Percent::from_percentage_value(90).unwrap();
}
if epoch == 245 {
simulator.delegate(Coin::new(2050_000000, "unym"))
}
if epoch == 264 {
let (delegation, _reward) = simulator.undelegate(1).unwrap();
// sanity check to make sure we undelegated what we wanted to undelegate : )
assert_eq!(delegation.amount.u128(), 2000_000000);
// TODO: figure out if there's a good way to verify whether `reward` is what we expect it to be
}
if epoch == 345 {
is_active = true;
}
if epoch == 358 {
performance = Percent::from_percentage_value(100).unwrap();
}
if epoch == 458 {
let (delegation, _reward) = simulator.undelegate(0).unwrap();
// sanity check to make sure we undelegated what we wanted to undelegate : )
assert_eq!(delegation.amount.u128(), 18000_000000);
// TODO: figure out if there's a good way to verify whether `reward` is what we expect it to be
}
if epoch == 545 {
simulator.delegate(Coin::new(5000_000000, "unym"))
}
// this has to always hold
check_rewarding_invariant(&simulator);
let node_params = NodeRewardParams::new(performance, is_active);
simulator.simulate_epoch(node_params);
}
// after everyone undelegates, there should be nothing left in the delegates pool
let delegations = simulator.node_delegations.len();
for _ in 0..delegations {
simulator.undelegate(0).unwrap();
}
assert_eq!(Decimal::zero(), simulator.node_rewarding_details.delegates);
}
}
@@ -0,0 +1,873 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::MixnetContractError;
use crate::helpers::IntoBaseDecimal;
use crate::reward_params::NodeRewardParams;
use crate::rewarding::simulator::simulated_node::SimulatedNode;
use crate::rewarding::RewardDistribution;
use crate::{
Delegation, Interval, IntervalRewardParams, MixId, MixNodeCostParams, RewardingParams,
};
use cosmwasm_std::{Coin, Decimal};
use std::collections::BTreeMap;
pub mod simulated_node;
pub struct Simulator {
pub nodes: BTreeMap<MixId, SimulatedNode>,
pub system_rewarding_params: RewardingParams,
pub interval: Interval,
next_mix_id: MixId,
pending_reward_pool_emission: Decimal,
}
impl Simulator {
pub fn new(system_rewarding_params: RewardingParams, interval: Interval) -> Self {
Simulator {
nodes: Default::default(),
system_rewarding_params,
interval,
next_mix_id: 0,
pending_reward_pool_emission: Default::default(),
}
}
fn advance_epoch(&mut self) -> Result<(), MixnetContractError> {
let updated = self.interval.advance_epoch();
// we rolled over an interval
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
.system_rewarding_params
.interval
.staking_supply_scale_factor
* self.pending_reward_pool_emission;
let epoch_reward_budget = reward_pool
/ self.interval.epochs_in_interval().into_base_decimal()?
* old.interval_pool_emission.value();
let stake_saturation_point = staking_supply
/ self
.system_rewarding_params
.rewarded_set_size
.into_base_decimal()?;
let updated_params = RewardingParams {
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,
active_set_work_factor: old.active_set_work_factor,
interval_pool_emission: old.interval_pool_emission,
},
rewarded_set_size: self.system_rewarding_params.rewarded_set_size,
active_set_size: self.system_rewarding_params.active_set_size,
};
self.system_rewarding_params = updated_params;
self.pending_reward_pool_emission = Decimal::zero();
}
self.interval = updated;
Ok(())
}
pub fn bond(
&mut self,
pledge: Coin,
cost_params: MixNodeCostParams,
) -> Result<MixId, MixnetContractError> {
let mix_id = self.next_mix_id;
self.nodes.insert(
mix_id,
SimulatedNode::new(
mix_id,
cost_params,
&pledge,
self.interval.current_epoch_absolute_id(),
)?,
);
self.next_mix_id += 1;
Ok(mix_id)
}
pub fn delegate<S: Into<String>>(
&mut self,
delegator: S,
delegation: Coin,
mix_id: MixId,
) -> Result<(), MixnetContractError> {
let node = self
.nodes
.get_mut(&mix_id)
.ok_or(MixnetContractError::MixNodeBondNotFound { mix_id })?;
node.delegate(delegator, delegation)
}
// since this is a simulator only, not something to be used in the production code, the unwraps are fine
// if user inputs are invalid
pub fn undelegate<S: Into<String>>(
&mut self,
delegator: S,
mix_id: MixId,
) -> Result<(Coin, Coin), MixnetContractError> {
let node = self
.nodes
.get_mut(&mix_id)
.ok_or(MixnetContractError::MixNodeBondNotFound { mix_id })?;
node.undelegate(delegator)
}
pub fn simulate_epoch_single_node(
&mut self,
params: NodeRewardParams,
) -> Result<RewardDistribution, MixnetContractError> {
assert_eq!(self.nodes.len(), 1);
if let Some(&id) = self.nodes.keys().next() {
let mut params_map = BTreeMap::new();
params_map.insert(id, params);
Ok(self
.simulate_epoch(&params_map)?
.remove(&id)
.unwrap_or_default())
} else {
Ok(RewardDistribution::default())
}
}
pub fn simulate_epoch(
&mut self,
node_params: &BTreeMap<MixId, NodeRewardParams>,
) -> Result<BTreeMap<MixId, RewardDistribution>, MixnetContractError> {
let mut params_keys = node_params.keys().copied().collect::<Vec<_>>();
params_keys.sort_unstable();
let mut node_keys = self.nodes.keys().copied().collect::<Vec<_>>();
node_keys.sort_unstable();
if params_keys != node_keys {
panic!("invalid node rewarding params provided");
}
let mut dist = BTreeMap::new();
for (mix_id, node) in self.nodes.iter_mut() {
let reward_distribution = node.rewarding_details.calculate_epoch_reward(
&self.system_rewarding_params,
node_params[mix_id],
self.interval.epochs_in_interval(),
);
node.rewarding_details.distribute_rewards(
reward_distribution,
self.interval.current_epoch_absolute_id(),
);
self.pending_reward_pool_emission += reward_distribution.operator;
self.pending_reward_pool_emission += reward_distribution.delegates;
dist.insert(*mix_id, reward_distribution);
}
self.advance_epoch()?;
Ok(dist)
}
pub fn determine_delegation_reward(
&self,
delegation: &Delegation,
) -> Result<Decimal, MixnetContractError> {
Ok(self.nodes[&delegation.mix_id]
.rewarding_details
.determine_delegation_reward(delegation)?)
}
pub fn determine_total_delegation_reward(&self) -> Result<Decimal, MixnetContractError> {
let mut total = Decimal::zero();
for node in self.nodes.values() {
for delegation in node.delegations.values() {
total += node
.rewarding_details
.determine_delegation_reward(delegation)?
}
}
Ok(total)
}
// assume node state doesn't change in the interval (kinda unrealistic)
pub fn simulate_full_interval(
&mut self,
node_params: &BTreeMap<MixId, NodeRewardParams>,
) -> Result<(), MixnetContractError> {
for _ in 0..self.interval.epochs_in_interval() {
self.simulate_epoch(node_params)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::helpers::compare_decimals;
use crate::Percent;
use cosmwasm_std::testing::mock_env;
use std::time::Duration;
#[cfg(test)]
mod single_node_case {
use super::*;
use crate::helpers::compare_decimals;
use crate::reward_params::IntervalRewardParams;
use crate::rewarding::helpers::truncate_reward_amount;
use crate::Percent;
use cosmwasm_std::coin;
use cosmwasm_std::testing::mock_env;
use std::time::Duration;
// explicitly marking this as part of #[allow(clippy::unwrap_used)] until
// https://github.com/rust-lang/rust-clippy/pull/9686
// is merged into a release
#[allow(clippy::unwrap_used)]
fn base_simulator(initial_pledge: u128) -> Simulator {
let profit_margin = Percent::from_percentage_value(10).unwrap();
let interval_operating_cost = Coin::new(40_000_000, "unym");
let epochs_in_interval = 720u32;
let rewarded_set_size = 240;
let active_set_size = 100;
let interval_pool_emission = Percent::from_percentage_value(2).unwrap();
let reward_pool = 250_000_000_000_000u128;
let staking_supply = 100_000_000_000_000u128;
let epoch_reward_budget =
interval_pool_emission * Decimal::from_ratio(reward_pool, epochs_in_interval);
let stake_saturation_point = Decimal::from_ratio(staking_supply, rewarded_set_size);
let rewarding_params = RewardingParams {
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(),
active_set_work_factor: Decimal::percent(1000), // value '10'
interval_pool_emission,
},
rewarded_set_size,
active_set_size,
};
let interval = Interval::init_interval(
epochs_in_interval,
Duration::from_secs(60 * 60),
&mock_env(),
);
let initial_pledge = Coin::new(initial_pledge, "unym");
let mut simulator = Simulator::new(rewarding_params, interval);
let cost_params = MixNodeCostParams {
profit_margin_percent: profit_margin,
interval_operating_cost,
};
simulator.bond(initial_pledge, cost_params).unwrap();
simulator
}
// essentially our delegations + estimated rewards HAVE TO equal to what we actually determined
//
// explicitly marking this as part of #[allow(clippy::unwrap_used)] until
// https://github.com/rust-lang/rust-clippy/pull/9686
// is merged into a release
#[allow(clippy::unwrap_used)]
fn check_rewarding_invariant(simulator: &Simulator) {
for node in simulator.nodes.values() {
let delegation_sum: Decimal = node
.delegations
.values()
.map(|d| d.dec_amount().unwrap())
.sum();
let reward_sum: Decimal = node
.delegations
.values()
.map(|d| {
node.rewarding_details
.determine_delegation_reward(d)
.unwrap()
})
.sum();
// let reward_sum = simulator.determine_total_delegation_reward();
compare_decimals(
delegation_sum + reward_sum,
node.rewarding_details.delegates,
None,
)
}
}
#[test]
fn simulator_returns_expected_values_for_base_case() {
let mut simulator = base_simulator(10000_000000);
let epoch_params =
NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
let rewards = simulator.simulate_epoch_single_node(epoch_params).unwrap();
assert_eq!(rewards.delegates, Decimal::zero());
compare_decimals(
rewards.operator,
"1128452.5416104363".parse().unwrap(),
None,
);
}
#[test]
fn single_delegation_at_genesis() {
let mut simulator = base_simulator(10000_000000);
simulator
.delegate("alice", Coin::new(18000_000000, "unym"), 0)
.unwrap();
let node_params =
NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
let rewards = simulator.simulate_epoch_single_node(node_params).unwrap();
compare_decimals(
rewards.delegates,
"1795950.2602660495".parse().unwrap(),
None,
);
compare_decimals(rewards.operator, "1363716.856243172".parse().unwrap(), None);
compare_decimals(
rewards.delegates,
simulator.determine_total_delegation_reward().unwrap(),
None,
);
let node = &simulator.nodes[&0];
assert_eq!(
node.rewarding_details.operator,
rewards.operator + Decimal::from_atomics(10000_000000u128, 0).unwrap()
);
assert_eq!(
node.rewarding_details.delegates,
rewards.delegates + Decimal::from_atomics(18000_000000u128, 0).unwrap()
);
}
#[test]
fn delegation_and_undelegation() {
let mut simulator = base_simulator(10000_000000);
let node_params =
NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
let rewards1 = simulator.simulate_epoch_single_node(node_params).unwrap();
let expected_operator1 = "1128452.5416104363".parse().unwrap();
assert_eq!(rewards1.delegates, Decimal::zero());
compare_decimals(rewards1.operator, expected_operator1, None);
simulator
.delegate("alice", Coin::new(18000_000000, "unym"), 0)
.unwrap();
let rewards2 = simulator.simulate_epoch_single_node(node_params).unwrap();
let expected_operator2 = "1363843.413584609".parse().unwrap();
let expected_delegator_reward1 = "1795952.25874404".parse().unwrap();
compare_decimals(rewards2.delegates, expected_delegator_reward1, None);
compare_decimals(rewards2.operator, expected_operator2, None);
let rewards3 = simulator.simulate_epoch_single_node(node_params).unwrap();
let expected_operator3 = "1364017.7824440491".parse().unwrap();
let expected_delegator_reward2 = "1796135.9269468693".parse().unwrap();
compare_decimals(rewards3.delegates, expected_delegator_reward2, None);
compare_decimals(rewards3.operator, expected_operator3, None);
let (delegation, reward) = simulator.undelegate("alice", 0).unwrap();
assert_eq!(delegation.amount.u128(), 18000_000000);
assert_eq!(
reward.amount,
truncate_reward_amount(expected_delegator_reward1 + expected_delegator_reward2)
);
let base_op = Decimal::from_atomics(10000_000000u128, 0).unwrap();
let node = &simulator.nodes[&0];
compare_decimals(
node.rewarding_details.operator,
base_op + expected_operator1 + expected_operator2 + expected_operator3,
None,
);
assert_eq!(Decimal::zero(), node.rewarding_details.delegates);
}
#[test]
fn withdrawing_operator_reward() {
// essentially all delegators' rewards (and the operator itself) are still correctly computed
let original_pledge = coin(10000_000000, "unym");
let mut simulator = base_simulator(original_pledge.amount.u128());
let node_params =
NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
// add 2 delegations at genesis (because it makes things easier and as shown with previous tests
// delegating at different times still work)
simulator
.delegate("alice", Coin::new(18000_000000, "unym"), 0)
.unwrap();
simulator
.delegate("bob", Coin::new(4000_000000, "unym"), 0)
.unwrap();
// "normal", sanity check rewarding
let rewards1 = simulator.simulate_epoch_single_node(node_params).unwrap();
let expected_operator1 = "1411087.1007647323".parse().unwrap();
let expected_delegator_reward1 = "2199961.032388664".parse().unwrap();
compare_decimals(rewards1.delegates, expected_delegator_reward1, None);
compare_decimals(rewards1.operator, expected_operator1, None);
check_rewarding_invariant(&simulator);
let node = simulator.nodes.get_mut(&0).unwrap();
let reward = node
.rewarding_details
.withdraw_operator_reward(&original_pledge)
.unwrap();
assert_eq!(reward.amount, truncate_reward_amount(expected_operator1));
assert_eq!(
node.rewarding_details.operator,
Decimal::from_atomics(original_pledge.amount, 0).unwrap()
);
let rewards2 = simulator.simulate_epoch_single_node(node_params).unwrap();
let expected_operator2 = "1411113.0004067947".parse().unwrap();
let expected_delegator_reward2 = "2200183.3879084454".parse().unwrap();
compare_decimals(rewards2.delegates, expected_delegator_reward2, None);
compare_decimals(rewards2.operator, expected_operator2, None);
check_rewarding_invariant(&simulator);
}
#[test]
fn withdrawing_delegator_reward() {
// essentially all delegators' rewards (and the operator itself) are still correctly computed
let mut simulator = base_simulator(10000_000000);
let node_params =
NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
// add 2 delegations at genesis (because it makes things easier and as shown with previous tests
// delegating at different times still work)
simulator
.delegate("alice", Coin::new(18000_000000, "unym"), 0)
.unwrap();
simulator
.delegate("bob", Coin::new(4000_000000, "unym"), 0)
.unwrap();
// "normal", sanity check rewarding
let rewards1 = simulator.simulate_epoch_single_node(node_params).unwrap();
let expected_operator1 = "1411087.1007647323".parse().unwrap();
let expected_delegator_reward1 = "2199961.032388664".parse().unwrap();
compare_decimals(rewards1.delegates, expected_delegator_reward1, None);
compare_decimals(rewards1.operator, expected_operator1, None);
check_rewarding_invariant(&simulator);
// reference to our `18000_000000` delegation
let node = simulator.nodes.get_mut(&0).unwrap();
let delegation1 = node.delegations.get_mut("alice").unwrap();
let reward = node
.rewarding_details
.withdraw_delegator_reward(delegation1)
.unwrap();
let expected_del1_reward = "1799968.1174089068".parse().unwrap();
assert_eq!(reward.amount, truncate_reward_amount(expected_del1_reward));
// new reward after withdrawal
let rewards2 = simulator.simulate_epoch_single_node(node_params).unwrap();
let expected_operator2 = "1411250.1907492676".parse().unwrap();
let expected_delegator_reward2 = "2200004.051009689".parse().unwrap();
compare_decimals(rewards2.delegates, expected_delegator_reward2, None);
compare_decimals(rewards2.operator, expected_operator2, None);
check_rewarding_invariant(&simulator);
// check final values
let node = simulator.nodes.get_mut(&0).unwrap();
let delegation1 = node.delegations.get_mut("alice").unwrap();
let reward_del1 = node
.rewarding_details
.withdraw_delegator_reward(delegation1)
.unwrap();
let expected_del1_reward = "1799970.5883041779".parse().unwrap();
assert_eq!(
reward_del1.amount,
truncate_reward_amount(expected_del1_reward)
);
let delegation2 = node.delegations.get_mut("bob").unwrap();
let reward_del2 = node
.rewarding_details
.withdraw_delegator_reward(delegation2)
.unwrap();
let first: Decimal = "399992.91497975704".parse().unwrap();
let second: Decimal = "400033.4627055114".parse().unwrap();
let expected_del2_reward = first + second;
assert_eq!(
reward_del2.amount,
truncate_reward_amount(expected_del2_reward)
);
}
#[test]
fn simulating_multiple_epochs() {
let mut simulator = base_simulator(10000_000000);
let mut is_active = true;
let mut performance = Percent::from_percentage_value(100).unwrap();
for epoch in 0..720 {
if epoch == 0 {
simulator
.delegate("a", Coin::new(18000_000000, "unym"), 0)
.unwrap()
}
if epoch == 42 {
simulator
.delegate("b", Coin::new(2000_000000, "unym"), 0)
.unwrap()
}
if epoch == 89 {
is_active = false;
}
if epoch == 123 {
simulator
.delegate("c", Coin::new(6666_000000, "unym"), 0)
.unwrap()
}
if epoch == 167 {
performance = Percent::from_percentage_value(90).unwrap();
}
if epoch == 245 {
simulator
.delegate("d", Coin::new(2050_000000, "unym"), 0)
.unwrap()
}
if epoch == 264 {
let (delegation, _reward) = simulator.undelegate("b", 0).unwrap();
// sanity check to make sure we undelegated what we wanted to undelegate : )
assert_eq!(delegation.amount.u128(), 2000_000000);
// TODO: figure out if there's a good way to verify whether `reward` is what we expect it to be
}
if epoch == 345 {
is_active = true;
}
if epoch == 358 {
performance = Percent::from_percentage_value(100).unwrap();
}
if epoch == 458 {
let (delegation, _reward) = simulator.undelegate("a", 0).unwrap();
// sanity check to make sure we undelegated what we wanted to undelegate : )
assert_eq!(delegation.amount.u128(), 18000_000000);
// TODO: figure out if there's a good way to verify whether `reward` is what we expect it to be
}
if epoch == 545 {
simulator
.delegate("e", Coin::new(5000_000000, "unym"), 0)
.unwrap()
}
// this has to always hold
check_rewarding_invariant(&simulator);
let node_params = NodeRewardParams::new(performance, is_active);
simulator.simulate_epoch_single_node(node_params).unwrap();
}
// after everyone undelegates, there should be nothing left in the delegates pool
simulator.undelegate("c", 0).unwrap();
simulator.undelegate("d", 0).unwrap();
simulator.undelegate("e", 0).unwrap();
let node = &simulator.nodes[&0];
assert_eq!(Decimal::zero(), node.rewarding_details.delegates);
}
}
#[test]
fn multiple_nodes_against_known_values() {
#![allow(clippy::inconsistent_digit_grouping)]
// TODO: this test can be further improved by checking values after EVERY interval
// rather than just checking the final results
let epochs_in_interval = 1u32;
let rewarded_set_size = 10;
let active_set_size = 6;
let interval_pool_emission = Percent::from_percentage_value(2).unwrap();
let reward_pool = 250_000_000_000_000u128;
let staking_supply = 100_000_000_000_000u128;
let epoch_reward_budget =
interval_pool_emission * Decimal::from_ratio(reward_pool, epochs_in_interval);
let stake_saturation_point = Decimal::from_ratio(staking_supply, rewarded_set_size);
let rewarding_params = RewardingParams {
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(),
active_set_work_factor: Decimal::percent(1000), // value '10'
interval_pool_emission,
},
rewarded_set_size,
active_set_size,
};
let interval = Interval::init_interval(
epochs_in_interval,
Duration::from_secs(60 * 60),
&mock_env(),
);
let mut simulator = Simulator::new(rewarding_params, interval);
let n0 = simulator
.bond(
Coin::new(11_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(1_000_000_000000, "unym"), n0)
.unwrap();
let n1 = simulator
.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(11_000_000_000000, "unym"), n1)
.unwrap();
let n2 = simulator
.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(9_000_000_000000, "unym"), n2)
.unwrap();
let n3 = simulator
.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(0).unwrap(),
interval_operating_cost: Coin::new(500_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n3)
.unwrap();
let n4 = simulator
.bond(
Coin::new(1000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(7_999_000_000000, "unym"), n4)
.unwrap();
let n5 = simulator
.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n5)
.unwrap();
let n6 = simulator
.bond(
Coin::new(11_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(1_000_000_000000, "unym"), n6)
.unwrap();
let n7 = simulator
.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(9_000_000_000000, "unym"), n7)
.unwrap();
let n8 = simulator
.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(0).unwrap(),
interval_operating_cost: Coin::new(500_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n8)
.unwrap();
let n9 = simulator
.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n9)
.unwrap();
let uptime_1 = Percent::from_percentage_value(100).unwrap();
let uptime_09 = Percent::from_percentage_value(90).unwrap();
let uptime_0 = Percent::from_percentage_value(0).unwrap();
let node_params = [
(n0, NodeRewardParams::new(uptime_1, true)),
(n1, NodeRewardParams::new(uptime_1, true)),
(n2, NodeRewardParams::new(uptime_1, true)),
(n3, NodeRewardParams::new(uptime_09, true)),
(n4, NodeRewardParams::new(uptime_09, true)),
(n5, NodeRewardParams::new(uptime_0, true)),
(n6, NodeRewardParams::new(uptime_1, false)),
(n7, NodeRewardParams::new(uptime_1, false)),
(n8, NodeRewardParams::new(uptime_09, false)),
(n9, NodeRewardParams::new(uptime_0, false)),
]
.into_iter()
.collect::<BTreeMap<_, _>>();
for _ in 0..23 {
simulator.simulate_full_interval(&node_params).unwrap();
}
// we allow the delta to be within 0.1unym,
// because the expected values, especially for the reward pool,
// do not provide us with any higher precision
let epsilon = Some(Decimal::from_ratio(1u32, 10u32));
let expected_reward_pool = "184876811322111.7".parse().unwrap();
let expected_staking_supply = "165123188677888.3".parse().unwrap();
compare_decimals(
expected_reward_pool,
simulator.system_rewarding_params.interval.reward_pool,
epsilon,
);
compare_decimals(
expected_staking_supply,
simulator.system_rewarding_params.interval.staking_supply,
epsilon,
);
let expected_n0_pledge = "24307061704726.808".parse().unwrap();
let expected_n0_delegated = "2031528592775.6752".parse().unwrap();
let node = &simulator.nodes[&0].rewarding_details;
compare_decimals(node.operator, expected_n0_pledge, epsilon);
compare_decimals(node.delegates, expected_n0_delegated, epsilon);
let expected_n1_pledge = "3544171010629.92".parse().unwrap();
let expected_n1_delegated = "20854154351479.96".parse().unwrap();
let node = &simulator.nodes[&1].rewarding_details;
compare_decimals(node.operator, expected_n1_pledge, epsilon);
compare_decimals(node.delegates, expected_n1_delegated, epsilon);
let expected_n2_pledge = "3781120900089.8865".parse().unwrap();
let expected_n2_delegated = "18634530734287.53".parse().unwrap();
let node = &simulator.nodes[&2].rewarding_details;
compare_decimals(node.operator, expected_n2_pledge, epsilon);
compare_decimals(node.delegates, expected_n2_delegated, epsilon);
let expected_n3_pledge = "2313562111772.3165".parse().unwrap();
let expected_n3_delegated = "16090515100131.858".parse().unwrap();
let node = &simulator.nodes[&3].rewarding_details;
compare_decimals(node.operator, expected_n3_pledge, epsilon);
compare_decimals(node.delegates, expected_n3_delegated, epsilon);
let expected_n4_pledge = "1419679306492.7962".parse().unwrap();
let expected_n4_delegated = "16802494863659.93".parse().unwrap();
let node = &simulator.nodes[&4].rewarding_details;
compare_decimals(node.operator, expected_n4_pledge, epsilon);
compare_decimals(node.delegates, expected_n4_delegated, epsilon);
let expected_n5_pledge = "1000000000000".parse().unwrap();
let expected_n5_delegated = "7000000000000".parse().unwrap();
let node = &simulator.nodes[&5].rewarding_details;
compare_decimals(node.operator, expected_n5_pledge, epsilon);
compare_decimals(node.delegates, expected_n5_delegated, epsilon);
let expected_n6_pledge = "14114996375922.574".parse().unwrap();
let expected_n6_delegated = "1249173915284.053".parse().unwrap();
let node = &simulator.nodes[&6].rewarding_details;
compare_decimals(node.operator, expected_n6_pledge, epsilon);
compare_decimals(node.delegates, expected_n6_delegated, epsilon);
let expected_n7_pledge = "1225564192694.3525".parse().unwrap();
let expected_n7_delegated = "9931461332688.53".parse().unwrap();
let node = &simulator.nodes[&7].rewarding_details;
compare_decimals(node.operator, expected_n7_pledge, epsilon);
compare_decimals(node.delegates, expected_n7_delegated, epsilon);
let expected_n8_pledge = "1112319106593.8608".parse().unwrap();
let expected_n8_delegated = "7710855078658.264".parse().unwrap();
let node = &simulator.nodes[&8].rewarding_details;
compare_decimals(node.operator, expected_n8_pledge, epsilon);
compare_decimals(node.delegates, expected_n8_delegated, epsilon);
let expected_n9_pledge = "1000000000000".parse().unwrap();
let expected_n9_delegated = "7000000000000".parse().unwrap();
let node = &simulator.nodes[&9].rewarding_details;
compare_decimals(node.operator, expected_n9_pledge, epsilon);
compare_decimals(node.delegates, expected_n9_delegated, epsilon);
}
}
@@ -0,0 +1,81 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{Delegation, EpochId, MixId, MixNodeCostParams, MixNodeRewarding};
use cosmwasm_std::{Addr, Coin};
use std::collections::HashMap;
use crate::error::MixnetContractError;
use crate::rewarding::helpers::truncate_reward;
pub struct SimulatedNode {
pub mix_id: MixId,
pub rewarding_details: MixNodeRewarding,
pub delegations: HashMap<String, Delegation>,
}
impl SimulatedNode {
pub fn new(
mix_id: MixId,
cost_params: MixNodeCostParams,
initial_pledge: &Coin,
current_epoch: EpochId,
) -> Result<Self, MixnetContractError> {
Ok(SimulatedNode {
mix_id,
rewarding_details: MixNodeRewarding::initialise_new(
cost_params,
initial_pledge,
current_epoch,
)?,
delegations: HashMap::new(),
})
}
pub fn delegate<S: Into<String>>(
&mut self,
delegator: S,
delegation: Coin,
) -> Result<(), MixnetContractError> {
self.rewarding_details
.add_base_delegation(delegation.amount)?;
let delegator = delegator.into();
let delegation = Delegation::new(
Addr::unchecked(&delegator),
self.mix_id,
self.rewarding_details.total_unit_reward,
delegation,
42,
None,
);
self.delegations.insert(delegator, delegation);
Ok(())
}
pub fn undelegate<S: Into<String>>(
&mut self,
delegator: S,
) -> Result<(Coin, Coin), MixnetContractError> {
let delegator = delegator.into();
let delegation = self.delegations.remove(&delegator).ok_or(
MixnetContractError::NoMixnodeDelegationFound {
mix_id: MixId::MAX,
address: delegator,
proxy: None,
},
)?;
let reward = self
.rewarding_details
.determine_delegation_reward(&delegation)?;
self.rewarding_details
.remove_delegation_decimal(delegation.dec_amount()? + reward)?;
let reward_denom = &delegation.amount.denom;
let truncated_reward = truncate_reward(reward, reward_denom);
Ok((delegation.amount, truncated_reward))
}
}
@@ -2,15 +2,12 @@
// SPDX-License-Identifier: Apache-2.0
use crate::error::MixnetContractError;
use crate::rewarding::helpers::truncate_decimal;
use crate::{Layer, RewardedSetNodeStatus};
use cosmwasm_std::{Addr, Uint128};
use cosmwasm_std::{Coin, Decimal};
use cosmwasm_std::Addr;
use cosmwasm_std::Coin;
use schemars::JsonSchema;
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serialize};
use std::fmt::{self, Display, Formatter};
use std::ops::{Index, Mul};
use serde::{Deserialize, Serialize};
use std::ops::Index;
// type aliases for better reasoning about available data
pub type IdentityKey = String;
@@ -20,90 +17,10 @@ pub type SphinxKeyRef<'a> = &'a str;
pub type EpochId = u32;
pub type IntervalId = u32;
pub type MixId = u32;
pub type BlockHeight = u64;
pub type EpochEventId = u32;
pub type IntervalEventId = u32;
/// Percent represents a value between 0 and 100%
/// (i.e. between 0.0 and 1.0)
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Serialize, Deserialize, JsonSchema,
)]
pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal);
impl Percent {
pub fn new(value: Decimal) -> Result<Self, MixnetContractError> {
if value > Decimal::one() {
Err(MixnetContractError::InvalidPercent)
} else {
Ok(Percent(value))
}
}
pub fn is_zero(&self) -> bool {
self.0 == Decimal::zero()
}
pub fn from_percentage_value(value: u64) -> Result<Self, MixnetContractError> {
Percent::new(Decimal::percent(value))
}
pub fn value(&self) -> Decimal {
self.0
}
pub fn round_to_integer(&self) -> u8 {
let hundred = Decimal::from_ratio(100u32, 1u32);
// we know the cast from u128 to u8 is a safe one since the internal value must be within 0 - 1 range
truncate_decimal(hundred * self.0).u128() as u8
}
}
impl Display for Percent {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let adjusted = Decimal::from_atomics(100u32, 0).unwrap() * self.0;
write!(f, "{}%", adjusted)
}
}
impl Mul<Decimal> for Percent {
type Output = Decimal;
fn mul(self, rhs: Decimal) -> Self::Output {
self.0 * rhs
}
}
impl Mul<Percent> for Decimal {
type Output = Decimal;
fn mul(self, rhs: Percent) -> Self::Output {
rhs * self
}
}
impl Mul<Uint128> for Percent {
type Output = Uint128;
fn mul(self, rhs: Uint128) -> Self::Output {
self.0 * rhs
}
}
// implement custom Deserialize because we want to validate Percent has the correct range
fn de_decimal_percent<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
where
D: Deserializer<'de>,
{
let v = Decimal::deserialize(deserializer)?;
if v > Decimal::one() {
Err(D::Error::custom(
"provided decimal percent is larger than 100%",
))
} else {
Ok(v)
}
}
#[derive(Debug, Default, Serialize, Deserialize, Copy, Clone, Eq, PartialEq)]
pub struct LayerDistribution {
pub layer1: u64,
@@ -118,6 +35,10 @@ impl LayerDistribution {
(Layer::Two, self.layer2),
(Layer::Three, self.layer3),
];
// we explicitly put 3 elements into the iterator, so the iterator is DEFINITELY
// not empty and thus the unwrap cannot fail
#[allow(clippy::unwrap_used)]
layers.iter().min_by_key(|x| x.1).unwrap().0
}
@@ -205,48 +126,3 @@ pub struct PagedRewardedSetResponse {
pub nodes: Vec<(MixId, RewardedSetNodeStatus)>,
pub start_next_after: Option<MixId>,
}
#[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();
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);
}
}
@@ -6,6 +6,7 @@ edition = "2021"
[dependencies]
cosmwasm-std = "1.0.0"
mixnet-contract-common = { path = "../mixnet-contract" }
contracts-common = { path = "../contracts-common" }
serde = { version = "1.0", features = ["derive"] }
schemars = "0.8"
ts-rs = {version = "6.1.2", optional = true}
@@ -1,9 +1,15 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![warn(clippy::expect_used)]
#![warn(clippy::unwrap_used)]
use contracts_common::Percent;
use cosmwasm_std::{Addr, Coin, Timestamp, Uint128};
use mixnet_contract_common::MixId;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
pub use messages::{ExecuteMsg, InitMsg, MigrateMsg, QueryMsg};
@@ -42,6 +48,35 @@ impl PledgeData {
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum PledgeCap {
Percent(Percent),
Absolute(Uint128), // This has to be in unym
}
impl FromStr for PledgeCap {
type Err = String;
fn from_str(cap: &str) -> Result<Self, Self::Err> {
let cap = cap.replace('_', "").replace(',', ".");
match Percent::from_str(&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 {
#[allow(clippy::expect_used)]
PledgeCap::Percent(Percent::from_percentage_value(10).expect("This can never fail!"))
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct OriginalVestingResponse {
pub amount: Coin,
@@ -98,3 +133,32 @@ pub struct AllDelegationsResponse {
pub delegations: Vec<VestingDelegation>,
pub start_next_after: Option<(u32, MixId, u64)>,
}
#[cfg(test)]
mod test {
use contracts_common::Percent;
use cosmwasm_std::Uint128;
use std::str::FromStr;
use crate::PledgeCap;
#[test]
fn test_pledge_cap_from_str() {
assert_eq!(
PledgeCap::from_str("0.1").unwrap(),
PledgeCap::Percent(Percent::from_percentage_value(10).unwrap())
);
assert_eq!(
PledgeCap::from_str("0,1").unwrap(),
PledgeCap::Percent(Percent::from_percentage_value(10).unwrap())
);
assert_eq!(
PledgeCap::from_str("100_000_000_000").unwrap(),
PledgeCap::Absolute(Uint128::new(100_000_000_000))
);
assert_eq!(
PledgeCap::from_str("100000000000").unwrap(),
PledgeCap::Absolute(Uint128::new(100_000_000_000))
);
}
}
@@ -1,4 +1,4 @@
use cosmwasm_std::{Coin, Timestamp, Uint128};
use cosmwasm_std::{Coin, Timestamp};
use mixnet_contract_common::{
mixnode::{MixNodeConfigUpdate, MixNodeCostParams},
Gateway, MixId, MixNode,
@@ -6,6 +6,8 @@ use mixnet_contract_common::{
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::PledgeCap;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct InitMsg {
@@ -83,6 +85,7 @@ pub enum ExecuteMsg {
owner_address: String,
staking_address: Option<String>,
vesting_spec: Option<VestingSpecification>,
cap: Option<PledgeCap>,
},
WithdrawVestedCoins {
amount: Coin,
@@ -120,7 +123,8 @@ pub enum ExecuteMsg {
to_address: Option<String>,
},
UpdateLockedPledgeCap {
amount: Uint128,
address: String,
cap: PledgeCap,
},
}
@@ -156,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>,
@@ -201,7 +206,6 @@ pub enum QueryMsg {
GetCurrentVestingPeriod {
address: String,
},
GetLockedPledgeCap {},
GetDelegationTimes {
address: String,
mix_id: MixId,
+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"

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