Compare commits

..

113 Commits

Author SHA1 Message Date
farbanas 7ed7329917 Merge branch 'release/v1.1.19' 2023-05-16 11:33:23 +02:00
Tommy Verrall 4abc0ae0ba Merge remote-tracking branch 'origin/release/v1.1.19' into release/v1.1.19 2023-05-16 11:22:59 +02:00
Tommy Verrall 47d8fcb21a remove package.json 2023-05-16 11:22:46 +02:00
Tommy Verrall acef5a5652 rename package.json to prevent ci moaning 2023-05-16 11:22:03 +02:00
farbanas 8e021a4419 update Cargo.lock 2023-05-16 10:33:04 +02:00
farbanas e5db7cb915 update changelog for release v1.1.19 2023-05-16 10:30:06 +02:00
mx 9bd03af3e9 version bump 2023-05-16 10:14:27 +02:00
mx e8a026ef0b updated sandbox peer 2023-05-16 10:02:39 +02:00
mx 86755aa6ba updated admonishments for uniformity 2023-05-16 10:00:01 +02:00
mx 77e0c6425e * updated validator go version + added link to precompiled binaries
* general version bump
2023-05-16 09:54:40 +02:00
Pierre Dommerc 0373e2b02a feat(wallet): select validator (#3375) 2023-05-15 10:29:47 +02:00
Jon Häggblad e8dd347186 Update Cargo.lock (#3410) 2023-05-14 22:00:17 +02:00
farbanas 4ebd1dd7f5 Merge branch 'master' into develop 2023-05-10 09:51:33 +01:00
Jon Häggblad 62ab760656 Add name-service endpoint to nym-api (#3394) 2023-05-10 10:42:49 +02:00
Nadim Kobeissi 32de7efc32 Fix #3371 2023-05-09 19:43:57 +02:00
farbanas deae210b82 update changelog in for release v1.1.18 2023-05-09 12:21:30 +01:00
farbanas 5b2b45a6eb updated versions for release v1.1.18 2023-05-09 12:20:07 +01:00
mx 896a3e1be6 temporarily removed compatibility table: will reintroduce once fixed 2023-05-09 11:40:07 +02:00
Jon Häggblad 800390db85 Fix warning about default-features being ignored (#3398) 2023-05-09 11:32:21 +02:00
mx 1eaa13155c Merge pull request #3392 from nymtech/feature/release-1-1-18-docs
version bump to 1.1.18
2023-05-09 09:17:02 +00:00
Tommy Verrall fad3346096 Merge pull request #3391 from nymtech/bugfix/wallet-signin-ui
Bug fix: resolve dead-lock when switching signin to main app window in the Nym Wallet
2023-05-09 09:19:07 +01:00
Tommy Verrall 150f832f8e Merge pull request #3388 from nymtech/feature/wallet_enforce_semver
feat(wallet-bonding): enforce semver for node version
2023-05-09 09:18:23 +01:00
Nadim Kobeissi 202336b8a1 Fix Typescript SDK compilation errors 2023-05-05 16:23:46 +02:00
Jon Häggblad f0e94f8e5e Add name-service support to validator-client (#3384)
* Add name-service support to validator-client

* Add default_memo

* contract address for wallet

* rustfmt

* lock file

* Tidy up nym-wallet-types network config

* Typo

* Remove some unused contract constants
2023-05-05 15:39:38 +02:00
Fouad 6cd00b8d10 estimated fees for sending tokens (#3389)
* estimated fees for sending tokens
2023-05-05 14:08:16 +01:00
mx 534187cc8f Merge pull request #3368 from nymtech/add-docs-template
Update issue templates
2023-05-05 12:21:17 +00:00
mx 25b4934f69 added ntv blog to community guides section 2023-05-05 14:12:20 +02:00
mx 5ef7e24893 removed additional whitespace 2023-05-05 13:56:35 +02:00
mx 87ef46bc05 version bump to 1.1.18 2023-05-05 13:49:52 +02:00
Mark Sinclair f7bc5be8e4 Bug fix: resolve dead-lock when switching signin to main app window in the Nym Wallet
- change operations to async
- open the new window first and then try to close the old window, to prevent the process from exiting
2023-05-05 12:12:08 +01:00
Nadim Kobeissi b309583886 Run wasm-opt manually (Apple Silicon issue)
wasm-opt has a known issue on Apple Silicon:
https://github.com/rustwasm/wasm-pack/issues/913

The workaround currently seems to be running wasm-opt locally instead of
defining it as part of the Rust package's build pipeline in Cargo.toml.

I hope this is okay!
2023-05-05 12:09:13 +02:00
pierre 245185710a strip off v in node version 2023-05-05 11:03:44 +02:00
Jon Häggblad b7cfe31d72 Initial version of nym-name-service contract (only) (#3380)
* Initial version of nym-name-service, based on nym-service-provider-directory

* rustfmt

* Rename to NameEntry

* Restrict address format

* Remove deprecated random test

* Fix clippy

* Add to top-level Makefile

* Restore wasm-opt Makefile rule

* Restore NymAddress as enum

* rustfmt

* Add contract address to qa-qwerty.env

* Rename NymAddress to Address

* Tweak event output

* rustfmt

* add event_tag()

* qwerty contract address
2023-05-04 15:57:41 +02:00
Pierre Dommerc 68ca41a6be refactor(wallet-bonding): fetch node data concurrently (#3362) 2023-05-04 10:10:50 +02:00
Pierre Dommerc 5621e7d22e refactor(wallet-bonding): fetch node data concurrently (#3362) 2023-05-04 10:02:29 +02:00
Fouad a1a5c7772d Use Loading Modal component when loading Delegations data (#3377)
* allow loading modal to display custom text

* use loading modal

* dont repeatedly reset delegation state

* show loading modal when loading + no other modal is open

* fix lint errors

* log any delegations errors

* fix typo

* refresh interval in delegations page
2023-05-03 17:41:24 +02:00
Tommy Verrall b47deafc14 Merge pull request #3381 from nymtech/feature/add-nyxd-builds-ci
Feature/add nyxd builds ci
2023-05-03 16:30:46 +01:00
benedettadavico cc6a6d8db2 tweaking file 2023-05-03 17:19:28 +02:00
benedettadavico 5b28e24c17 workflow to add nyxd to builds ci 2023-05-03 17:15:47 +02:00
farbanas f8d68d8ef0 fix: merge resolve 2023-05-02 15:34:15 +02:00
farbanas a9d86508b5 Merge branch 'master' into develop 2023-05-02 14:38:11 +02:00
Tommy Verrall bb7fa587de formatting 2023-05-02 13:19:17 +02:00
Tommy Verrall 6585732dfc fix broken network address - set to none 2023-05-02 13:14:20 +02:00
farbanas 2065e0fc17 Merge branch 'master' into develop 2023-05-02 12:09:52 +02:00
farbanas 3f7bdad59c update lock files 2023-05-02 10:54:54 +02:00
farbanas 6209e78c1e bump crates 2023-05-02 10:53:42 +02:00
farbanas 8e5062af96 bump versions and update changelogs for release v1.1.17 2023-05-02 10:39:24 +02:00
mx 496e642d7f Merge pull request #3370 from nymtech/feature/1-1-17-docs
Feature/1 1 17 docs
2023-05-02 08:10:32 +00:00
mx 07e18ec198 added tokio dependency note 2023-04-28 15:51:20 +02:00
mx d5953c28c1 added note on running example code 2023-04-28 15:45:38 +02:00
mx 3aa4b66588 added info re buying NYM from wallet with BTC 2023-04-28 15:43:57 +02:00
mx 005f0ce340 * added correct version variable to sign command output
* added info that you can buy NYM from wallet with BTC
2023-04-28 15:41:55 +02:00
mx 8d1d025fa2 bumped point version 2023-04-28 15:41:44 +02:00
mx 1d53a2f954 updated readme with more details re: each directory having a readme and running them 2023-04-28 15:41:18 +02:00
mx 966d123608 Update issue templates 2023-04-27 16:15:10 +02:00
Tommy Verrall 963d55273f Merge pull request #3367 from nymtech/feature/adding-sp-api-tests
adding a test for SP endpoint
2023-04-27 14:47:26 +01:00
benedettadavico 6872d7bf77 adding a test for SP endpoint 2023-04-27 15:37:59 +02:00
Jon Häggblad 6fe93bcda0 Merge pull request #3332 from nymtech/jon/feat/sp-integrations
Service provider directory support in nym-api, nym-cli, validator-client
2023-04-27 11:51:00 +02:00
Jędrzej Stuczyński 78d568e04e Feature/store cipher (#3350)
* initial nym-store-cipher

* cleanup
2023-04-27 10:24:36 +01:00
Jon Häggblad 8880bdd857 Fix build target in top-level Makefile 2023-04-26 16:57:01 +02:00
Jon Häggblad cc83ecf7e4 socks5: send empty keepalive msg to avoid triggering MIX_TTL during long downloads (#3364)
* socks5: send empty keepalive msg to avoid triggering MIX_TTL during long downloads

* rustfmt

* reset timer after each normal send
2023-04-26 16:44:27 +02:00
Pierre Dommerc 796f5a678a feat(wallet): update bond amount (#3338) 2023-04-26 15:35:47 +02:00
Pierre Dommerc 00b60f5493 feat(wallet): update bond amount (#3338) 2023-04-26 15:29:39 +02:00
Jon Häggblad 0dfd1cca44 Review comments 2023-04-26 10:58:12 +02:00
Jon Häggblad eacefd3697 nym-cli: add announce and delete service provider 2023-04-26 10:58:12 +02:00
Jon Häggblad 424c25768c Add schemars to lock file 2023-04-26 10:58:12 +02:00
Jon Häggblad e460c1700e Add support for listing services in nym-cli 2023-04-26 10:58:12 +02:00
Jon Häggblad 9935c99d41 nym-api: add service provider endpoint and caching 2023-04-26 10:58:12 +02:00
Jon Häggblad 2c4aee63bf Make the contract optional 2023-04-26 10:58:12 +02:00
Jon Häggblad 0ebc395df9 rustfmt 2023-04-26 10:58:12 +02:00
Jon Häggblad 1de3317e75 Add sp directory contract traits and methods to nyxd client 2023-04-26 10:58:12 +02:00
Tommy Verrall 91c20af893 Merge pull request #3328 from nymtech/feature/shared-network-monitor
Feature/shared network monitor
2023-04-26 09:49:54 +01:00
pierre 1365e2f246 chore(wallet): add v prefix in wallet version 2023-04-26 10:04:58 +02:00
pierre cfa1ce46f2 chore(wallet): add v prefix in wallet version 2023-04-26 09:30:46 +02:00
Mark Sinclair 3f4f76859b Merge pull request #3188 from nymtech/feature/wallet-login
Split wallet sign in and main into two entry points
2023-04-25 11:07:00 +01:00
Raphaël Walther 3f7f4b82de Move workflows to custom runner 2023-04-25 11:10:12 +02:00
farbanas 934ba2b027 Merge branch 'master' into develop 2023-04-25 10:53:43 +02:00
Jędrzej Stuczyński 221e1870e5 removed redundant trait 2023-04-25 09:53:32 +01:00
Jędrzej Stuczyński 9b36bccf0c wasm tester fixes 2023-04-25 09:53:32 +01:00
Jędrzej Stuczyński 80d7285497 further improvements + reduced log noise 2023-04-25 09:53:32 +01:00
Jędrzej Stuczyński b94f2a483d nym-api compiling again 2023-04-25 09:53:32 +01:00
Jędrzej Stuczyński f64cfb4cd1 wip 2023-04-25 09:53:32 +01:00
Jędrzej Stuczyński eda223ed3d Resolved beta clippy complaints (#3351) 2023-04-25 09:53:11 +01:00
farbanas c98d4305fa update cargo locks 2023-04-25 10:09:49 +02:00
farbanas 2eecbca6eb bump versions and update changelogs for release v1.1.16 2023-04-25 10:06:03 +02:00
farbanas a58c80ef08 update versions of mixnet and vesting contract crates 2023-04-25 10:06:03 +02:00
farbanas ac9d0db8be update versions of mixnet and vesting contract crates 2023-04-25 10:06:03 +02:00
farbanas 7521d98963 update versions of mixnet and vesting contract common crates 2023-04-25 10:06:03 +02:00
mx 1e98131090 Merge pull request #3349 from nymtech/feature/general-docs-updates
version bump for next release
2023-04-25 07:39:10 +00:00
mx 46bf65462c Merge pull request #3325 from esomore/mixnode/description
update mix-node setup docs with node description
2023-04-25 07:38:22 +00:00
mx e3df4c2d68 reintroduce minimum rust version variable 2023-04-24 17:13:03 +02:00
mx 45c013350f version bump for next release 2023-04-24 17:10:41 +02:00
Mark Sinclair 30e2f27c64 Fix linting error 2023-04-24 15:51:50 +01:00
mx 6fecc53975 Merge pull request #3339 from nymtech/feature/coconut-rust-sdk-docs
added coconut credential generation example
2023-04-24 14:49:24 +00:00
Tommy Verrall e4dbfb1904 Merge pull request #3222 from nymtech/feature/available_reader_changes
Feature/available reader changes
2023-04-24 15:48:08 +01:00
Mark Sinclair 3113c1e9a7 Fix build issues 2023-04-24 15:41:13 +01:00
Tommy Verrall f822d3db7b cargo fmt 2023-04-24 15:29:51 +01:00
Jędrzej Stuczyński 9d23766288 updated used packet size 2023-04-24 15:29:51 +01:00
Jędrzej Stuczyński fd4930b198 removed old leftover log statement 2023-04-24 15:29:51 +01:00
Jędrzej Stuczyński 5d7be89edb replaced inner implementation with tokio's 'ReaderStream' 2023-04-24 15:29:51 +01:00
Jędrzej Stuczyński 47f5b4ceac limit the maximum buffer size of AvailableReader by PacketSize of our mix packets 2023-04-24 15:29:51 +01:00
Jędrzej Stuczyński 790220039b added read deadline to AvailableReader 2023-04-24 15:29:51 +01:00
Tommy Verrall 16fdfa4583 Update mainnet.env 2023-04-24 13:54:54 +02:00
Mark Sinclair 1d8a931e0c Do not keep mnemonic or password (and variations) in logs 2023-04-24 12:24:19 +01:00
Mark Sinclair 48d0883b31 Clear stashed state completely on logout 2023-04-24 12:24:19 +01:00
Mark Sinclair e271370326 Split wallet sign in and main into two entry points
Stash some of the state in the Rust process and load it when the React app mounts
Fix connect_with_mnemonic logging
2023-04-24 12:24:19 +01:00
Jędrzej Stuczyński cbbeb66b5b Feature/wasm client topology injection (#3311)
* added cargo config file to explicitly specify build target

* wip

* Config option to disable topology refreshing

* extracted common parsing code

* helper trait for working on wasm topology

* wasm topology parsing

* restored (slightly modified) old js-example

* wip

* Moved message preparation into a trait

* wip

* long-winded way of sending test packet

* standalone NymNodeTester

* finishing the test upon receiving all packets even if timeout wasnt reached

* initial round of cleanup

* sending multiple test packets in normal NymClient

* javascript-side cleanup

* starting mixnode test on btn click

* Improved NymNodeTester constructors

* improved error handling and constructors

* tester utils error handling

* further cleanup + using BTreeMap for NymTopology mixnodes

* handling missed errors

* splitting up 'test_node'

* split up and cleaned up generation of test result

* clippy + fixed example

* post rebase fixes

* another broken test

* prevent running multiple parallel tests

* cargo fmt

* Added nym- prefix to node tester utils
2023-04-24 09:56:26 +01:00
mx de020f46a6 added coconut credential generation example 2023-04-21 16:32:20 +02:00
Jędrzej Stuczyński f24bb5c038 reduced noise in CODEOWNERS (#3313)
* reduced noise in CODEOWNERS

* Add @octol to codeowners

* added @mfahampshire as owner of /documentation

---------

Co-authored-by: Jon Häggblad <jon.haggblad@gmail.com>
2023-04-20 15:25:23 +01:00
Jon Häggblad 79dfe7eeda Add clippy target in top-level Makefile 2023-04-20 14:13:20 +02:00
Jon Häggblad 0108c6ed19 Merge remote-tracking branch 'origin/release/v1.1.16' into develop 2023-04-20 09:55:19 +02:00
Itamar Perez 0e8f60d501 update mix-node setup docs with node description 2023-04-19 12:01:36 -07:00
Jon Häggblad 6e30e6178b Update Cargo.lock files after bumping internal versions during release 2023-04-19 09:37:38 +02:00
254 changed files with 17126 additions and 6210 deletions
+17 -22
View File
@@ -11,30 +11,25 @@
# In each subsection folders are ordered first by depth, then alphabetically. # In each subsection folders are ordered first by depth, then alphabetically.
# This should make it easy to add new rules without breaking existing ones. # This should make it easy to add new rules without breaking existing ones.
# Something weird not covered by anything else # contracts
* @futurechimp @mmsinclair /contracts/mixnet @durch @jstuczyn
/contracts/vesting @durch @jstuczyn
/contracts/service-provider-directory @octol
# Rust rules: # crypto code
*.rs @durch @futurechimp @jstuczyn @neacsu @octol /common/crypto/ @jstuczyn
Cargo.* @durch @futurechimp @jstuczyn @neacsu @octol /common/nymcoconut/ @jstuczyn
/common/dkg/ @jstuczyn
/common/nymsphinx/ @jstuczyn
# JS rules: # rust sdk
*.js @mmsinclair @fmtabbara /sdk/rust/ @octol
*.ts @mmsinclair @fmtabbara
*.tsx @mmsinclair @fmtabbara
*.jsx @mmsinclair @fmtabbara
# Something looking like possible documentation rules: # nym-connect (rust)
*.md @mfahampshire /nym-connect/desktop/src-tauri/ @octol
# our docker scripts # nym-wallet (rust)
/docker/ @neacsu /nym-wallet/src-tauri/ @octol
# if there are any changes in the core crypto, I feel like Ania should take a look: # documentation
/common/crypto/ @aniampio /documentation @mfahampshire
/common/nymsphinx/ @aniampio
# Explorer and wallet should probably get looked by the product team
/explorer/ @nymtech/product
/nym-wallet/ @nymtech/product
/wallet-web/ @nymtech/product
+14
View File
@@ -0,0 +1,14 @@
---
name: 'Documentation'
about: Suggest a fix or enhancement to the documentation or developer portal content
title: "[DOCS]"
labels: documentation
assignees: mfahampshire
---
Is your issue either:
- [ ] a fix to existing documentation (e.g. fixing a broken link or incorrect command)
- [ ] an enhancement (e.g. adding a description for an undocumented feature)
Please briefly describe your issue:
@@ -6,7 +6,7 @@
}, },
{ {
"os":"windows-latest", "os":"windows10",
"rust":"stable", "rust":"stable",
"runOnEvent":"schedule" "runOnEvent":"schedule"
}, },
@@ -22,7 +22,7 @@
"runOnEvent":"schedule" "runOnEvent":"schedule"
}, },
{ {
"os":"windows-latest", "os":"windows10",
"rust":"beta", "rust":"beta",
"runOnEvent":"schedule" "runOnEvent":"schedule"
}, },
@@ -38,7 +38,7 @@
"runOnEvent":"schedule" "runOnEvent":"schedule"
}, },
{ {
"os":"windows-latest", "os":"windows10",
"rust":"nightly", "rust":"nightly",
"runOnEvent":"schedule" "runOnEvent":"schedule"
}, },
+79
View File
@@ -0,0 +1,79 @@
name: Upload nyxd to CI
on:
workflow_dispatch:
jobs:
publish-nyxd:
strategy:
fail-fast: false
matrix:
platform: [ubuntu-20.04]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v3
- name: Prepare build output directory
shell: bash
env:
OUTPUT_DIR: ci-builds/nyxd
run: |
rm -rf ci-builds || true
mkdir -p $OUTPUT_DIR
echo $OUTPUT_DIR
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get -y install build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools git
continue-on-error: true
- name: Update env variables to include go
run: |
sudo rm -rf /usr/local/go
curl https://dl.google.com/go/go1.19.2.linux-amd64.tar.gz | sudo tar -C/usr/local -zxvf -
cat <<'EOF' >>$HOME/.profile
export GOROOT=/usr/local/go
export GOPATH=$HOME/go
export GO111MODULE=on
export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin
EOF
source $HOME/.profile
- name: Verify Go is installed
run: go version
- name: Clone nyxd repo
run: |
git clone https://github.com/tommyv1987/nyxd
cd nyxd
git checkout release/v0.30.2
- name: Run nyxd
run: |
pwd
cd nyxd && make build
sleep 10
ls /home/runner/work/nym/nym/nyxd/build
- name: Prepare build output
shell: bash
env:
OUTPUT_DIR: ci-builds/nyxd
run: |
cp /home/runner/work/nym/nym/nyxd/build/nyxd $OUTPUT_DIR
WASMVM_SO=$(ldd /home/runner/work/nym/nym/nyxd/build/nyxd | grep "libwasm*" | awk '{ print $3 }')
ls $WASMVM_SO
sleep 3
cp $(echo $WASMVM_SO) $OUTPUT_DIR
- name: Deploy nyxd to CI www
continue-on-error: true
uses: easingthemes/ssh-deploy@main
env:
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
ARGS: "-avzr"
SOURCE: "ci-builds/"
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/
EXCLUDE: "/dist/, /node_modules/"
+62 -7
View File
@@ -4,14 +4,69 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased] ## [Unreleased]
- nym-network-statistics properly handles signals ([#3209]) ## [v1.1.19] (2023-05-16)
- add socks5 support for Rust SDK ([#3226], [#3255])
- add coconut bandwidth credential support for Rust SDK ([#3273])
[#3209]: https://github.com/nymtech/nym/issues/3209 - nym-name-service endpoint in nym-api ([#3403])
[#3226]: https://github.com/nymtech/nym/pull/3226 - Implement key storage for WASM client using IndexedDB (for browser) ([#3329])
[#3255]: https://github.com/nymtech/nym/pull/3255 - Initial version of nym-name-service contract providing name aliases for nym-addresses ([#3274])
[#3273]: https://github.com/nymtech/nym/pull/3273 - Update Cargo.lock ([#3410])
[#3403]: https://github.com/nymtech/nym/issues/3403
[#3329]: https://github.com/nymtech/nym/issues/3329
[#3274]: https://github.com/nymtech/nym/issues/3274
[#3410]: https://github.com/nymtech/nym/pull/3410
## [v1.1.18] (2023-05-09)
- Implement heartbeat messages between socks5 proxy and network requester ([#3215])
[#3215]: https://github.com/nymtech/nym/issues/3215
## [v1.1.17] (2023-05-02)
- Add service-provider-directory-contract support to nym-cli ([#3334])
- Start using the node-testing-utils (implemented in #3270) in nym-api Network monitor to simplify the logic there ([#3312])
- Add service-provider-directory support to validator-client ([#3296])
- Allow topology injection in our WASM client ('test my node' feature) ([#3270])
- Expose service-provider-directory contract data in nym-api endpoints ([#3242])
- Cache service provider contract in nym-api ([#3241])
- Feature/1 1 17 docs ([#3370])
- adding a test for SP endpoint ([#3367])
- Feature/store cipher ([#3350])
[#3334]: https://github.com/nymtech/nym/issues/3334
[#3312]: https://github.com/nymtech/nym/issues/3312
[#3296]: https://github.com/nymtech/nym/issues/3296
[#3270]: https://github.com/nymtech/nym/issues/3270
[#3242]: https://github.com/nymtech/nym/issues/3242
[#3241]: https://github.com/nymtech/nym/issues/3241
[#3370]: https://github.com/nymtech/nym/pull/3370
[#3367]: https://github.com/nymtech/nym/pull/3367
[#3350]: https://github.com/nymtech/nym/pull/3350
## [v1.1.16] (2023-04-25)
- Explorer - Fix sorting function on Stake Saturation. It is currently working per page and not globally ([#3320])
- Poisson process gets stuck at too slow rate. Rework to more aggressively up-regulate ([#3309])
- decrease the logging level of warnings associated with clients dropping packets due to gateway being overloaded (I'd say reduce it to debug/trace) - there are few sources of those, e.g. in real and cover traffic streams ([#3299])
- Make the buffer size in `AvailableReader` depend on packet sizes the client is using + introduce read timeouts ([#3213])
- Rust SDK - Support coconut, credential storage etc ([#2755])
- version bump for next release ([#3349])
- added coconut credential generation example ([#3339])
- update mix-node setup docs with node description ([#3325])
- exposed missing gateway commands in nym-cli ([#3324])
- make sure to clear inner 'ack_map' in 'GatewaysReader' ([#3300])
[#3320]: https://github.com/nymtech/nym/issues/3320
[#3309]: https://github.com/nymtech/nym/issues/3309
[#3299]: https://github.com/nymtech/nym/issues/3299
[#3213]: https://github.com/nymtech/nym/issues/3213
[#2755]: https://github.com/nymtech/nym/issues/2755
[#3349]: https://github.com/nymtech/nym/pull/3349
[#3339]: https://github.com/nymtech/nym/pull/3339
[#3325]: https://github.com/nymtech/nym/pull/3325
[#3324]: https://github.com/nymtech/nym/pull/3324
[#3300]: https://github.com/nymtech/nym/pull/3300
## [v1.1.15] (2023-04-18) ## [v1.1.15] (2023-04-18)
Generated
+551 -369
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -37,6 +37,7 @@ members = [
"common/cosmwasm-smart-contracts/group-contract", "common/cosmwasm-smart-contracts/group-contract",
"common/cosmwasm-smart-contracts/mixnet-contract", "common/cosmwasm-smart-contracts/mixnet-contract",
"common/cosmwasm-smart-contracts/multisig-contract", "common/cosmwasm-smart-contracts/multisig-contract",
"common/cosmwasm-smart-contracts/name-service",
"common/cosmwasm-smart-contracts/service-provider-directory", "common/cosmwasm-smart-contracts/service-provider-directory",
"common/cosmwasm-smart-contracts/vesting-contract", "common/cosmwasm-smart-contracts/vesting-contract",
"common/credential-storage", "common/credential-storage",
@@ -48,6 +49,7 @@ members = [
"common/ledger", "common/ledger",
"common/mixnode-common", "common/mixnode-common",
"common/network-defaults", "common/network-defaults",
"common/node-tester-utils",
"common/nonexhaustive-delayqueue", "common/nonexhaustive-delayqueue",
"common/nymcoconut", "common/nymcoconut",
"common/nymsphinx", "common/nymsphinx",
@@ -59,12 +61,14 @@ members = [
"common/nymsphinx/forwarding", "common/nymsphinx/forwarding",
"common/nymsphinx/framing", "common/nymsphinx/framing",
"common/nymsphinx/params", "common/nymsphinx/params",
"common/nymsphinx/routing",
"common/nymsphinx/types", "common/nymsphinx/types",
"common/pemstore", "common/pemstore",
"common/socks5-client-core", "common/socks5-client-core",
"common/socks5/proxy-helpers", "common/socks5/proxy-helpers",
"common/socks5/requests", "common/socks5/requests",
"common/statistics", "common/statistics",
"common/store-cipher",
"common/task", "common/task",
"common/topology", "common/topology",
"common/types", "common/types",
+8 -2
View File
@@ -13,6 +13,10 @@ happy: fmt clippy-happy test
# on all workspaces. # on all workspaces.
build-release: build-release-main wasm build-release: build-release-main wasm
# Deprecated
# For backwards compatibility
clippy-all: clippy
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Define targets for a given workspace # Define targets for a given workspace
# $(1): name # $(1): name
@@ -52,11 +56,11 @@ fmt-$(1):
cargo fmt --manifest-path $(2)/Cargo.toml --all cargo fmt --manifest-path $(2)/Cargo.toml --all
clippy-happy: clippy-happy-$(1) clippy-happy: clippy-happy-$(1)
clippy-all: clippy-$(1) clippy-examples-$(1) clippy: clippy-$(1) clippy-examples-$(1)
check: check-$(1) check: check-$(1)
cargo-test: test-$(1) cargo-test: test-$(1)
cargo-test-expensive: test-expensive-$(1) cargo-test-expensive: test-expensive-$(1)
build: build-$(1) build-$(1)-examples build: build-$(1) build-examples-$(1)
build-release-all: build-release-$(1) build-release-all: build-release-$(1)
fmt: fmt-$(1) fmt: fmt-$(1)
@@ -95,6 +99,7 @@ CONTRACTS_OUT_DIR=contracts/target/wasm32-unknown-unknown/release
VESTING_CONTRACT=$(CONTRACTS_OUT_DIR)/vesting_contract.wasm VESTING_CONTRACT=$(CONTRACTS_OUT_DIR)/vesting_contract.wasm
MIXNET_CONTRACT=$(CONTRACTS_OUT_DIR)/mixnet_contract.wasm MIXNET_CONTRACT=$(CONTRACTS_OUT_DIR)/mixnet_contract.wasm
SERVICE_PROVIDER_DIRECTORY_CONTRACT=$(CONTRACTS_OUT_DIR)/nym_service_provider_directory.wasm SERVICE_PROVIDER_DIRECTORY_CONTRACT=$(CONTRACTS_OUT_DIR)/nym_service_provider_directory.wasm
NAME_SERVICE_CONTRACT=$(CONTRACTS_OUT_DIR)/nym_name_service.wasm
wasm: wasm-build wasm-opt wasm: wasm-build wasm-opt
@@ -105,6 +110,7 @@ wasm-opt:
wasm-opt --disable-sign-ext -Os $(VESTING_CONTRACT) -o $(VESTING_CONTRACT) wasm-opt --disable-sign-ext -Os $(VESTING_CONTRACT) -o $(VESTING_CONTRACT)
wasm-opt --disable-sign-ext -Os $(MIXNET_CONTRACT) -o $(MIXNET_CONTRACT) wasm-opt --disable-sign-ext -Os $(MIXNET_CONTRACT) -o $(MIXNET_CONTRACT)
wasm-opt --disable-sign-ext -Os $(SERVICE_PROVIDER_DIRECTORY_CONTRACT) -o $(SERVICE_PROVIDER_DIRECTORY_CONTRACT) wasm-opt --disable-sign-ext -Os $(SERVICE_PROVIDER_DIRECTORY_CONTRACT) -o $(SERVICE_PROVIDER_DIRECTORY_CONTRACT)
wasm-opt --disable-sign-ext -Os $(NAME_SERVICE_CONTRACT) -o $(NAME_SERVICE_CONTRACT)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Misc # Misc
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "nym-client" name = "nym-client"
version = "1.1.15" version = "1.1.19"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"] authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client" description = "Implementation of the Nym Client"
edition = "2021" edition = "2021"
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "nym-socks5-client" name = "nym-socks5-client"
version = "1.1.15" version = "1.1.19"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"] 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" description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021" edition = "2021"
+23 -12
View File
@@ -34,7 +34,14 @@ import {
StakeSaturationResponse, StakeSaturationResponse,
UnbondedMixnodeResponse, UnbondedMixnodeResponse,
VestingAccountInfo, VestingAccountInfo,
ContractState, VestingAccountsCoinPaged, VestingAccountsPaged, DelegationTimes, Delegations, Period, VestingAccountNode, DelegationBlock ContractState,
VestingAccountsCoinPaged,
VestingAccountsPaged,
DelegationTimes,
Delegations,
Period,
VestingAccountNode,
DelegationBlock,
} from '@nymproject/types'; } from '@nymproject/types';
import QueryClient from './query-client'; import QueryClient from './query-client';
import SigningClient, { ISigningClient } from './signing-client'; import SigningClient, { ISigningClient } from './signing-client';
@@ -207,7 +214,7 @@ export default class ValidatorClient implements INymClient {
let mixNodes: UnbondedMixnodeResponse[] = []; let mixNodes: UnbondedMixnodeResponse[] = [];
const limit = 50; const limit = 50;
let startAfter; let startAfter;
for (; ;) { for (;;) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const pagedResponse: PagedUnbondedMixnodesResponse = await this.client.getUnbondedMixNodes( const pagedResponse: PagedUnbondedMixnodesResponse = await this.client.getUnbondedMixNodes(
this.mixnetContract, this.mixnetContract,
@@ -230,7 +237,7 @@ export default class ValidatorClient implements INymClient {
let mixNodes: MixNodeBond[] = []; let mixNodes: MixNodeBond[] = [];
const limit = 50; const limit = 50;
let startAfter; let startAfter;
for (; ;) { for (;;) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const pagedResponse: PagedMixNodeBondResponse = await this.client.getMixNodeBonds( const pagedResponse: PagedMixNodeBondResponse = await this.client.getMixNodeBonds(
this.mixnetContract, this.mixnetContract,
@@ -252,7 +259,7 @@ export default class ValidatorClient implements INymClient {
let mixNodes: MixNodeDetails[] = []; let mixNodes: MixNodeDetails[] = [];
const limit = 50; const limit = 50;
let startAfter; let startAfter;
for (; ;) { for (;;) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const pagedResponse: PagedMixNodeDetailsResponse = await this.client.getMixNodesDetailed( const pagedResponse: PagedMixNodeDetailsResponse = await this.client.getMixNodesDetailed(
this.mixnetContract, this.mixnetContract,
@@ -284,7 +291,7 @@ export default class ValidatorClient implements INymClient {
let delegations: Delegation[] = []; let delegations: Delegation[] = [];
const limit = 250; const limit = 250;
let startAfter; let startAfter;
for (; ;) { for (;;) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const pagedResponse: PagedMixDelegationsResponse = await this.client.getMixNodeDelegationsPaged( const pagedResponse: PagedMixDelegationsResponse = await this.client.getMixNodeDelegationsPaged(
this.mixnetContract, this.mixnetContract,
@@ -307,7 +314,7 @@ export default class ValidatorClient implements INymClient {
let delegations: Delegation[] = []; let delegations: Delegation[] = [];
const limit = 250; const limit = 250;
let startAfter; let startAfter;
for (; ;) { for (;;) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const pagedResponse: PagedDelegatorDelegationsResponse = await this.client.getDelegatorDelegationsPaged( const pagedResponse: PagedDelegatorDelegationsResponse = await this.client.getDelegatorDelegationsPaged(
this.mixnetContract, this.mixnetContract,
@@ -330,7 +337,7 @@ export default class ValidatorClient implements INymClient {
let delegations: Delegation[] = []; let delegations: Delegation[] = [];
const limit = 250; const limit = 250;
let startAfter; let startAfter;
for (; ;) { for (;;) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const pagedResponse: PagedAllDelegationsResponse = await this.client.getAllDelegationsPaged( const pagedResponse: PagedAllDelegationsResponse = await this.client.getAllDelegationsPaged(
this.mixnetContract, this.mixnetContract,
@@ -518,11 +525,9 @@ export default class ValidatorClient implements INymClient {
return (this.client as ISigningClient).updateContractStateParams(this.mixnetContract, newParams, fee, memo); return (this.client as ISigningClient).updateContractStateParams(this.mixnetContract, newParams, fee, memo);
} }
// VESTING
// VESTING
// TODO - MOVE TO A DIFFERENT FILE // TODO - MOVE TO A DIFFERENT FILE
public async getVestingAccountsPaged(): Promise<VestingAccountsPaged> { public async getVestingAccountsPaged(): Promise<VestingAccountsPaged> {
return this.client.getVestingAccountsPaged(this.vestingContract); return this.client.getVestingAccountsPaged(this.vestingContract);
} }
@@ -608,9 +613,9 @@ export default class ValidatorClient implements INymClient {
} }
public async getDelegation(address: string, mix_id: number): Promise<DelegationBlock> { public async getDelegation(address: string, mix_id: number): Promise<DelegationBlock> {
return this.client.getDelegation(this.vestingContract, address, mix_id ); return this.client.getDelegation(this.vestingContract, address, mix_id);
} }
public async getTotalDelegationAmount(address: string, mix_id: number, block_timestamp_sec: number): Promise<Coin> { public async getTotalDelegationAmount(address: string, mix_id: number, block_timestamp_sec: number): Promise<Coin> {
return this.client.getTotalDelegationAmount(this.vestingContract, address, mix_id, block_timestamp_sec); return this.client.getTotalDelegationAmount(this.vestingContract, address, mix_id, block_timestamp_sec);
} }
@@ -618,4 +623,10 @@ export default class ValidatorClient implements INymClient {
public async getCurrentVestingPeriod(address: string): Promise<Period> { public async getCurrentVestingPeriod(address: string): Promise<Period> {
return this.client.getCurrentVestingPeriod(this.vestingContract, address); return this.client.getCurrentVestingPeriod(this.vestingContract, address);
} }
// SIMULATE
public async simulateSend(signingAddress: string, from: string, to: string, amount: Coin[]) {
return (this.client as SigningClient).simulateSend(signingAddress, from, to, amount);
}
} }
+48 -9
View File
@@ -40,9 +40,18 @@ import {
RewardingParams, RewardingParams,
UnbondedMixnodeResponse, UnbondedMixnodeResponse,
VestingAccountInfo, VestingAccountInfo,
ContractState, VestingAccountsCoinPaged, VestingAccountsPaged, DelegationTimes, Delegations, Period, VestingAccountNode, DelegationBlock ContractState,
VestingAccountsCoinPaged,
VestingAccountsPaged,
DelegationTimes,
Delegations,
Period,
VestingAccountNode,
DelegationBlock,
} from '@nymproject/types'; } from '@nymproject/types';
import NymApiQuerier from './nym-api-querier'; import NymApiQuerier from './nym-api-querier';
import { makeBankMsgSend } from './utils';
import { ISimulateClient } from './types/simulate';
// methods exposed by `SigningCosmWasmClient` // methods exposed by `SigningCosmWasmClient`
export interface ICosmWasmSigning { export interface ICosmWasmSigning {
@@ -148,7 +157,7 @@ export interface INymSigning {
clientAddress: string; clientAddress: string;
} }
export interface ISigningClient extends IQueryClient, ICosmWasmSigning, INymSigning { export interface ISigningClient extends IQueryClient, ICosmWasmSigning, INymSigning, ISimulateClient {
bondMixNode( bondMixNode(
mixnetContractAddress: string, mixnetContractAddress: string,
mixNode: MixNode, mixNode: MixNode,
@@ -511,11 +520,11 @@ export default class SigningClient extends SigningCosmWasmClient implements ISig
); );
} }
// vesting related // vesting related
getVestingAccountsPaged(vestingContractAddress: string): Promise<VestingAccountsPaged> { getVestingAccountsPaged(vestingContractAddress: string): Promise<VestingAccountsPaged> {
return this.nyxdQuerier.getVestingAccountsPaged(vestingContractAddress); return this.nyxdQuerier.getVestingAccountsPaged(vestingContractAddress);
}; }
getVestingAmountsAccountsPaged(vestingContractAddress: string): Promise<VestingAccountsCoinPaged> { getVestingAmountsAccountsPaged(vestingContractAddress: string): Promise<VestingAccountsCoinPaged> {
return this.nyxdQuerier.getVestingAmountsAccountsPaged(vestingContractAddress); return this.nyxdQuerier.getVestingAmountsAccountsPaged(vestingContractAddress);
@@ -569,7 +578,10 @@ export default class SigningClient extends SigningCosmWasmClient implements ISig
return this.nyxdQuerier.getEndTime(vestingContractAddress, vestingAccountAddress); return this.nyxdQuerier.getEndTime(vestingContractAddress, vestingAccountAddress);
} }
getOriginalVestingDetails(vestingContractAddress: string, vestingAccountAddress: string): Promise<OriginalVestingResponse> { getOriginalVestingDetails(
vestingContractAddress: string,
vestingAccountAddress: string,
): Promise<OriginalVestingResponse> {
return this.nyxdQuerier.getOriginalVestingDetails(vestingContractAddress, vestingAccountAddress); return this.nyxdQuerier.getOriginalVestingDetails(vestingContractAddress, vestingAccountAddress);
} }
@@ -589,7 +601,11 @@ export default class SigningClient extends SigningCosmWasmClient implements ISig
return this.nyxdQuerier.getGateway(vestingContractAddress, address); return this.nyxdQuerier.getGateway(vestingContractAddress, address);
} }
getDelegationTimes(vestingContractAddress: string, mix_id: number, delegatorAddress: string): Promise<DelegationTimes> { getDelegationTimes(
vestingContractAddress: string,
mix_id: number,
delegatorAddress: string,
): Promise<DelegationTimes> {
return this.nyxdQuerier.getDelegationTimes(vestingContractAddress, mix_id, delegatorAddress); return this.nyxdQuerier.getDelegationTimes(vestingContractAddress, mix_id, delegatorAddress);
} }
@@ -597,15 +613,38 @@ export default class SigningClient extends SigningCosmWasmClient implements ISig
return this.nyxdQuerier.getAllDelegations(vestingContractAddress); return this.nyxdQuerier.getAllDelegations(vestingContractAddress);
} }
getDelegation(vestingContractAddress: string, vestingAccountAddress: string, mix_id: number): Promise<DelegationBlock> { getDelegation(
vestingContractAddress: string,
vestingAccountAddress: string,
mix_id: number,
): Promise<DelegationBlock> {
return this.nyxdQuerier.getDelegation(vestingContractAddress, vestingAccountAddress, mix_id); return this.nyxdQuerier.getDelegation(vestingContractAddress, vestingAccountAddress, mix_id);
} }
getTotalDelegationAmount(vestingContractAddress: string, vestingAccountAddress: string, mix_id: number, block_timestamp_sec: number): Promise<Coin> { getTotalDelegationAmount(
return this.nyxdQuerier.getTotalDelegationAmount(vestingContractAddress, vestingAccountAddress, mix_id, block_timestamp_sec); vestingContractAddress: string,
vestingAccountAddress: string,
mix_id: number,
block_timestamp_sec: number,
): Promise<Coin> {
return this.nyxdQuerier.getTotalDelegationAmount(
vestingContractAddress,
vestingAccountAddress,
mix_id,
block_timestamp_sec,
);
} }
getCurrentVestingPeriod(vestingContractAddress: string, address: string): Promise<Period> { getCurrentVestingPeriod(vestingContractAddress: string, address: string): Promise<Period> {
return this.nyxdQuerier.getCurrentVestingPeriod(vestingContractAddress, address); return this.nyxdQuerier.getCurrentVestingPeriod(vestingContractAddress, address);
} }
// simulation
// TODO consider adding multipling factor
simulateSend(signingAddress: string, from: string, to: string, amount: Coin[]) {
const sendMsg = makeBankMsgSend(from, to, amount);
return this.simulate(signingAddress, [sendMsg], 'simulate send tx');
}
} }
@@ -0,0 +1,31 @@
import expect from 'expect';
import ValidatorClient from '../..';
const dotenv = require('dotenv');
dotenv.config();
// TODO: implement for QA with .env for mnemonics
describe('Simualtions', () => {
let client: ValidatorClient;
beforeEach(async () => {
client = await ValidatorClient.connect(
process.env.mnemonic || '',
process.env.rpcAddress || '',
process.env.validatorAddress || '',
process.env.prefix || '',
process.env.mixnetContractAddress || '',
process.env.vestingContractAddress || '',
process.env.denom || '',
);
});
it('can simulate sending tokens', async () => {
const res = await client.simulateSend(client.address, client.address, client.address, [
{ amount: '400000', denom: 'unym' },
]);
expect(typeof res).toBe('number');
}).timeout(10000);
});
+5
View File
@@ -0,0 +1,5 @@
import { Coin } from '@cosmjs/proto-signing';
export interface ISimulateClient {
simulateSend(signingAddress: string, from: string, to: string, amount: Coin[]): Promise<number>;
}
+2
View File
@@ -0,0 +1,2 @@
[build]
target = "wasm32-unknown-unknown"
+8 -1
View File
@@ -17,6 +17,7 @@ default = ["console_error_panic_hook"]
offline-test = [] offline-test = []
[dependencies] [dependencies]
bs58 = "0.4.0"
futures = "0.3" futures = "0.3"
js-sys = "0.3" js-sys = "0.3"
rand = { version = "0.7.3", features = ["wasm-bindgen"] } rand = { version = "0.7.3", features = ["wasm-bindgen"] }
@@ -28,8 +29,12 @@ tokio = { version = "1.24.1", features = ["sync"] }
url = "2.2" url = "2.2"
wasm-bindgen = { version = "=0.2.83", features = ["serde-serialize"] } wasm-bindgen = { version = "=0.2.83", features = ["serde-serialize"] }
wasm-bindgen-futures = "0.4" wasm-bindgen-futures = "0.4"
thiserror = "1.0.40"
wasm-timer = { git = "https://github.com/mmsinclair/wasm-timer", rev = "b9d1a54ad514c2f230a026afe0dde341e98cd7b6"}
# internal # internal
nym-node-tester-utils = { path = "../../common/node-tester-utils" }
nym-client-core = { path = "../../common/client-core", default-features = false, features = ["wasm"] } nym-client-core = { path = "../../common/client-core", default-features = false, features = ["wasm"] }
nym-bandwidth-controller = { path = "../../common/bandwidth-controller" } nym-bandwidth-controller = { path = "../../common/bandwidth-controller" }
nym-coconut-interface = { path = "../../common/coconut-interface" } nym-coconut-interface = { path = "../../common/coconut-interface" }
@@ -37,6 +42,8 @@ nym-credentials = { path = "../../common/credentials" }
nym-credential-storage = { path = "../../common/credential-storage" } nym-credential-storage = { path = "../../common/credential-storage" }
nym-crypto = { path = "../../common/crypto" } nym-crypto = { path = "../../common/crypto" }
nym-sphinx = { path = "../../common/nymsphinx" } nym-sphinx = { path = "../../common/nymsphinx" }
nym-topology = { path = "../../common/topology" }
nym-gateway-client = { path = "../../common/client-libs/gateway-client", default-features = false, features = ["wasm"] }
nym-validator-client = { path = "../../common/client-libs/validator-client", default-features = false } nym-validator-client = { path = "../../common/client-libs/validator-client", default-features = false }
wasm-utils = { path = "../../common/wasm-utils" } wasm-utils = { path = "../../common/wasm-utils" }
nym-task = { path = "../../common/task" } nym-task = { path = "../../common/task" }
@@ -57,7 +64,7 @@ wee_alloc = { version = "0.4", optional = true }
wasm-bindgen-test = "0.3" wasm-bindgen-test = "0.3"
[package.metadata.wasm-pack.profile.release] [package.metadata.wasm-pack.profile.release]
wasm-opt = true wasm-opt = false
[profile.release] [profile.release]
lto = true lto = true
@@ -0,0 +1,2 @@
node_modules
dist
+5
View File
@@ -0,0 +1,5 @@
// A dependency graph that contains any wasm must all be imported
// asynchronously. This `bootstrap.js` file does the single async import, so
// that no one else needs to worry about it again.
import('./index.js')
.catch(e => console.error('Error importing `index.js`:', e));
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nym WebAssembly Demo</title>
</head>
<body>
<p>
<label>Sender: </label><input disabled="true" size="85" type="text" id="sender" value="">
</p>
<p>
<label>Recipient: </label><input size="85" type="text" id="recipient" value="">
</p>
<p>
<label>Message: </label><input type="text" id="message" value="Hello mixnet!">
</p>
<p>
<button id="send-button">Send</button>
</p>
<div>
<label>Mixnode Identity: </label>
<input type="text" size = "60" id="mixnode_identity" value="...">
<button id="magic-button">✨ Magic Test Button ✨</button>
</div>
<p>Send messages from your browser, through the mixnet, and to the recipient using the "send" button.</p>
<p><span style='color: blue;'>Sent</span> messages show in blue, <span style='color: green;'>received</span>
messages show in green.</p>
<hr>
<p>
<span id="output"></span>
</p>
<script src="./bootstrap.js"></script>
</body>
</html>
+170
View File
@@ -0,0 +1,170 @@
// Copyright 2020-2023 Nym Technologies SA
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
class WebWorkerClient {
worker = null;
constructor() {
this.worker = new Worker('./worker.js');
this.worker.onmessage = (ev) => {
if (ev.data && ev.data.kind) {
switch (ev.data.kind) {
case 'Ready':
const {selfAddress} = ev.data.args;
displaySenderAddress(selfAddress);
break;
case 'ReceiveMessage':
const {message, senderTag, isTestPacket } = ev.data.args;
displayReceived(message, senderTag, isTestPacket);
break;
case 'DisableMagicTestButton':
const magicButton = document.querySelector('#magic-button');
magicButton.setAttribute('disabled', "true")
break;
case 'DisplayTesterResults':
const {score, sentPackets, receivedPackets, receivedAcks, duplicatePackets, duplicateAcks} = ev.data.args;
const resultText = `Test score: ${score}. Sent ${sentPackets} packets. Received ${receivedPackets} packets and ${receivedAcks} acks back. We also got ${duplicatePackets} duplicate packets and ${duplicateAcks} duplicate acks.`
displayReceivedRawString(resultText)
break;
}
}
};
}
sendMessage = (message, recipient) => {
if (!this.worker) {
console.error('Could not send message because worker does not exist');
return;
}
this.worker.postMessage({
kind: 'SendMessage',
args: {
message, recipient,
},
});
};
sendTestPacket = (mixnodeIdentity) => {
if (!this.worker) {
console.error('Could not send message because worker does not exist');
return;
}
this.worker.postMessage({
kind: 'TestPacket',
args: {
mixnodeIdentity,
},
});
}
}
let client = null;
async function main() {
client = new WebWorkerClient();
const sendButton = document.querySelector('#send-button');
sendButton.onclick = function () {
sendMessageTo();
};
const magicButton = document.querySelector('#magic-button');
magicButton.onclick = function () {
sendTestPacket();
}
}
/**
* Create a Sphinx packet and send it to the mixnet through the gateway node.
*
* Message and recipient are taken from the values in the user interface.
*
*/
async function sendMessageTo() {
const message = document.getElementById('message').value;
const recipient = document.getElementById('recipient').value;
await client.sendMessage(message, recipient);
displaySend(message);
}
async function sendTestPacket() {
const mixnodeIdentity = document.getElementById('mixnode_identity').value;
await client.sendTestPacket(mixnodeIdentity)
displaySend(`sending test packets to: ${mixnodeIdentity}...`);
}
/**
* Display messages that have been sent up the websocket. Colours them blue.
*
* @param {string} message
*/
function displaySend(message) {
let timestamp = new Date().toISOString().substr(11, 12);
let sendDiv = document.createElement('div');
let paragraph = document.createElement('p');
paragraph.setAttribute('style', 'color: blue');
let paragraphContent = document.createTextNode(timestamp + ' sent >>> ' + message);
paragraph.appendChild(paragraphContent);
sendDiv.appendChild(paragraph);
document.getElementById('output').appendChild(sendDiv);
}
/**
* Display received text messages in the browser. Colour them green.
*
* @param {Uint8Array} raw
*/
function displayReceived(raw, sender_tag, isTestPacket) {
let content = new TextDecoder().decode(raw);
if (sender_tag !== undefined) {
console.log("this message also contained some surbs from", sender_tag)
}
if (isTestPacket) {
const decoded = JSON.parse(content)
content = `Received packet ${decoded.msg_id} / ${decoded.total_msgs} for node ${decoded.encoded_node_identity} (test: ${decoded.test_id})`
}
displayReceivedRawString(content)
}
function displayReceivedRawString(raw) {
let timestamp = new Date().toISOString().substr(11, 12);
let receivedDiv = document.createElement('div');
let paragraph = document.createElement('p');
paragraph.setAttribute('style', 'color: green');
let paragraphContent = document.createTextNode(timestamp + ' received >>> ' + raw);
paragraph.appendChild(paragraphContent);
receivedDiv.appendChild(paragraph);
document.getElementById('output').appendChild(receivedDiv);
}
/**
* Display the nymClient's sender address in the user interface
*
* @param {String} address
*/
function displaySenderAddress(address) {
document.getElementById('sender').value = address;
}
main();
@@ -0,0 +1,39 @@
{
"name": "create-wasm-app",
"version": "0.1.0",
"description": "create an app to consume rust-generated wasm packages",
"main": "index.js",
"bin": {
"create-wasm-app": ".bin/create-wasm-app.js"
},
"scripts": {
"build": "webpack --config webpack.config.js",
"start": "webpack-dev-server --port 8001"
},
"repository": {
"type": "git",
"url": "git+https://github.com/rustwasm/create-wasm-app.git"
},
"keywords": [
"webassembly",
"wasm",
"rust",
"webpack"
],
"author": "Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/nymtech/nym/issues"
},
"homepage": "https://nymtech.net/docs",
"devDependencies": {
"copy-webpack-plugin": "^10.2.4",
"hello-wasm-pack": "^0.1.0",
"webpack": "^5.70.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.7.4"
},
"dependencies": {
"@nymproject/nym-client-wasm": "file:../pkg"
}
}
@@ -0,0 +1,33 @@
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',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
// mode: 'development',
mode: 'production',
plugins: [
new CopyWebpackPlugin({
patterns: [
'index.html',
{
from: 'node_modules/@nymproject/nym-client-wasm/*.(js|wasm)',
to: '[name][ext]',
},
],
}),
],
experiments: { syncWebAssembly: true },
};
+294
View File
@@ -0,0 +1,294 @@
// Copyright 2020-2023 Nym Technologies SA
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
importScripts('nym_client_wasm.js');
console.log('Initializing worker');
// wasm_bindgen creates a global variable (with the exports attached) that is in scope after `importScripts`
const {
NymNodeTester,
WasmGateway,
WasmMixNode,
WasmNymTopology,
default_debug,
NymClientBuilder,
NymClient,
set_panic_hook,
Config,
GatewayEndpointConfig,
current_network_topology,
} = wasm_bindgen;
let client = null;
let tester = null;
function dummyTopology() {
const l1Mixnode = new WasmMixNode(
1,
'n1fzv4jc7fanl9s0qj02ge2ezk3kts545kjtek47',
'178.79.143.65',
1789,
'4Yr4qmEHd9sgsuQ83191FR2hD88RfsbMmB4tzhhZWriz',
'8ndjk5oZ6HxUZNScLJJ7hk39XtUqGexdKgW7hSX6kpWG',
1,
'1.10.0',
);
const l2Mixnode = new WasmMixNode(
2,
'n1z93z44vf8ssvdhujjvxcj4rd5e3lz0l60wdk70',
'109.74.197.180',
1789,
'7sVjiMrPYZrDWRujku9QLxgE8noT7NTgBAqizCsu7AoK',
'GepXwRnKZDd8x2nBWAajGGBVvF3mrpVMQBkgfrGuqRCN',
2,
'1.10.0',
);
const l3Mixnode = new WasmMixNode(
3,
'n1ptg680vnmef2cd8l0s9uyc4f0hgf3x8sed6w77',
'176.58.101.80',
1789,
'FoM5Mx9Pxk1g3zEqkS3APgtBeTtTo3M8k7Yu4bV6kK1R',
'DeYjrDC2AcQRVFshiKnbUo6bRvPyZ33QGYR2DLeFJ9qD',
3,
'1.10.0',
);
const gateway = new WasmGateway(
'n16evnn8glr0sham3matj8rg2s24m6x56ayk87ts',
'85.159.212.96',
1789,
9000,
'336yuXAeGEgedRfqTJZsG2YV7P13QH1bHv1SjCZYarc9',
'BtYjoWihiuFihGKQypmpSspbhmWDPxzqeTVSd8ciCpWL',
'1.10.1',
);
const mixnodes = new Map();
mixnodes.set(1, [l1Mixnode]);
mixnodes.set(2, [l2Mixnode]);
mixnodes.set(3, [l3Mixnode]);
const gateways = [gateway];
return new WasmNymTopology(mixnodes, gateways)
}
function printAndDisplayTestResult(result) {
result.log_details();
self.postMessage({
kind: 'DisplayTesterResults',
args: {
score: result.score(),
sentPackets: result.sent_packets,
receivedPackets: result.received_packets,
receivedAcks: result.received_acks,
duplicatePackets: result.duplicate_packets,
duplicateAcks: result.duplicate_acks,
},
});
}
function dummyGatewayConfig() {
return new GatewayEndpointConfig(
'336yuXAeGEgedRfqTJZsG2YV7P13QH1bHv1SjCZYarc9',
'n1rqqw8km7a0rvf8lr6k8dsdqvvkyn2mglj7xxfm',
'ws://85.159.212.96:9000',
)
}
async function testWithTester() {
const gatewayConfig = dummyGatewayConfig();
// A) construct with hardcoded topology
const topology = dummyTopology()
const nodeTester = await new NymNodeTester(gatewayConfig, topology);
// B) first get topology directly from nym-api
// const validator = 'https://qwerty-validator-api.qa.nymte.ch/api';
// const topology = await current_network_topology(validator)
// const nodeTester = await new NymNodeTester(gatewayConfig, topology);
//
// C) use nym-api in the constructor (note: it does no filtering for 'good' nodes on other layers)
// const validator = 'https://qwerty-validator-api.qa.nymte.ch/api';
// const nodeTester = await NymNodeTester.new_with_api(gatewayConfig, validator)
self.onmessage = async event => {
if (event.data && event.data.kind) {
switch (event.data.kind) {
case 'TestPacket': {
const {mixnodeIdentity} = event.data.args;
console.log("starting node test...");
let result = await nodeTester.test_node(mixnodeIdentity);
printAndDisplayTestResult(result)
}
}
}
};
}
async function testWithNymClient() {
const gatewayConfig = dummyGatewayConfig();
const topology = dummyTopology()
let received = 0
const onMessageHandler = (message) => {
received += 1;
self.postMessage({
kind: 'ReceiveMessage',
args: {
message,
senderTag: undefined,
isTestPacket: true,
},
});
// it's really up to the user to create proper callback here...
console.log(`received ${received} packets so far`)
};
console.log('Instantiating WASM client...');
let clientBuilder = NymClientBuilder.new_tester(gatewayConfig, topology, onMessageHandler)
console.log('Web worker creating WASM client...');
let local_client = await clientBuilder.start_client();
console.log('WASM client running!');
const selfAddress = local_client.self_address();
// set the global (I guess we don't have to anymore?)
client = local_client;
console.log(`Client address is ${selfAddress}`);
self.postMessage({
kind: 'Ready',
args: {
selfAddress,
},
});
// Set callback to handle messages passed to the worker.
self.onmessage = async event => {
console.log(event)
if (event.data && event.data.kind) {
switch (event.data.kind) {
case 'SendMessage': {
const {message, recipient} = event.data.args;
let uint8Array = new TextEncoder().encode(message);
await client.send_regular_message(uint8Array, recipient);
break;
}
case 'TestPacket': {
const {mixnodeIdentity} = event.data.args;
const req = await client.try_construct_test_packet_request(mixnodeIdentity);
await client.change_hardcoded_topology(req.injectable_topology());
await client.try_send_test_packets(req);
break;
}
}
}
};
}
async function normalNymClientUsage() {
self.postMessage({kind: 'DisableMagicTestButton'});
// only really useful if you want to adjust some settings like traffic rate
// (if not needed you can just pass a null)
const debug = default_debug();
debug.disable_main_poisson_packet_distribution = true;
debug.disable_loop_cover_traffic_stream = true;
debug.use_extended_packet_size = false;
// debug.average_packet_delay_ms = BigInt(10);
// debug.average_ack_delay_ms = BigInt(10);
// debug.ack_wait_addition_ms = BigInt(3000);
// debug.ack_wait_multiplier = 10;
debug.topology_refresh_rate_ms = BigInt(60000)
const gatewayConfig = dummyGatewayConfig();
const validator = 'https://qwerty-validator-api.qa.nymte.ch/api';
const config = new Config('my-awesome-wasm-client', validator, gatewayConfig, debug);
const onMessageHandler = (message) => {
console.log(message);
self.postMessage({
kind: 'ReceiveMessage',
args: {
message,
},
});
};
console.log('Instantiating WASM client...');
let localClient = await new NymClient(config, onMessageHandler)
console.log('WASM client running!');
const selfAddress = localClient.self_address();
// set the global (I guess we don't have to anymore?)
client = localClient;
console.log(`Client address is ${selfAddress}`);
self.postMessage({
kind: 'Ready',
args: {
selfAddress,
},
});
// Set callback to handle messages passed to the worker.
self.onmessage = async event => {
console.log(event)
if (event.data && event.data.kind) {
switch (event.data.kind) {
case 'SendMessage': {
const {message, recipient} = event.data.args;
let uint8Array = new TextEncoder().encode(message);
await client.send_regular_message(uint8Array, recipient);
break;
}
}
}
};
}
async function main() {
// load WASM package
await wasm_bindgen('nym_client_wasm_bg.wasm');
console.log('Loaded WASM');
// sets up better stack traces in case of in-rust panics
set_panic_hook();
// run test on simplified and dedicated tester:
await testWithTester()
// hook-up the whole client for testing
// await testWithNymClient()
// 'Normal' client setup (to send 'normal' messages)
// await normalNymClientUsage()
}
// Let's get started!
main();
File diff suppressed because it is too large Load Diff
+13 -4
View File
@@ -25,7 +25,7 @@ pub struct Config {
/// ID specifies the human readable ID of this particular client. /// ID specifies the human readable ID of this particular client.
pub(crate) id: String, pub(crate) id: String,
pub(crate) nym_api_url: Url, pub(crate) nym_api_url: Option<Url>,
pub(crate) disabled_credentials_mode: bool, pub(crate) disabled_credentials_mode: bool,
@@ -46,9 +46,11 @@ impl Config {
) -> Self { ) -> Self {
Config { Config {
id, id,
nym_api_url: validator_server nym_api_url: Some(
.parse() validator_server
.expect("provided url was malformed"), .parse()
.expect("provided url was malformed"),
),
disabled_credentials_mode: true, disabled_credentials_mode: true,
gateway_endpoint, gateway_endpoint,
debug: debug.map(Into::into).unwrap_or_default(), debug: debug.map(Into::into).unwrap_or_default(),
@@ -229,6 +231,11 @@ pub struct Topology {
/// path. This timeout determines waiting period until it is decided that the packet /// path. This timeout determines waiting period until it is decided that the packet
/// did not reach its destination. /// did not reach its destination.
pub topology_resolution_timeout_ms: u64, pub topology_resolution_timeout_ms: u64,
/// Specifies whether the client should not refresh the network topology after obtaining
/// the first valid instance.
/// Supersedes `topology_refresh_rate_ms`.
pub disable_refreshing: bool,
} }
impl From<Topology> for ConfigTopology { impl From<Topology> for ConfigTopology {
@@ -238,6 +245,7 @@ impl From<Topology> for ConfigTopology {
topology_resolution_timeout: Duration::from_millis( topology_resolution_timeout: Duration::from_millis(
topology.topology_resolution_timeout_ms, topology.topology_resolution_timeout_ms,
), ),
disable_refreshing: topology.disable_refreshing,
} }
} }
} }
@@ -247,6 +255,7 @@ impl From<ConfigTopology> for Topology {
Topology { Topology {
topology_refresh_rate_ms: topology.topology_refresh_rate.as_millis() as u64, topology_refresh_rate_ms: topology.topology_refresh_rate.as_millis() as u64,
topology_resolution_timeout_ms: topology.topology_resolution_timeout.as_millis() as u64, topology_resolution_timeout_ms: topology.topology_resolution_timeout.as_millis() as u64,
disable_refreshing: topology.disable_refreshing,
} }
} }
} }
+135 -7
View File
@@ -1,16 +1,42 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net> // Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use crate::error::WasmClientError;
use crate::tester::helpers::WasmTestMessageExt;
use crate::tester::{NodeTestMessage, DEFAULT_TEST_PACKETS};
use crate::topology::WasmNymTopology;
use js_sys::Promise; use js_sys::Promise;
use nym_client_core::client::base_client::ClientInput; use nym_client_core::client::base_client::{ClientInput, ClientState};
use nym_client_core::client::inbound_messages::InputMessage; use nym_client_core::client::inbound_messages::InputMessage;
use nym_topology::{MixLayer, NymTopology};
use std::sync::Arc; use std::sync::Arc;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsValue; use wasm_bindgen::JsValue;
use wasm_bindgen_futures::future_to_promise; use wasm_bindgen_futures::future_to_promise;
use wasm_utils::{console_log, simple_js_error};
#[wasm_bindgen]
pub struct NymClientTestRequest {
// serialized NodeTestMessage
pub(crate) test_msgs: Vec<Vec<u8>>,
// specially constructed network topology that only contains the target
// node on the tested layer
pub(crate) testable_topology: NymTopology,
}
#[wasm_bindgen]
impl NymClientTestRequest {
pub fn injectable_topology(&self) -> WasmNymTopology {
self.testable_topology.clone().into()
}
}
// defining helper trait as we could directly call the method on the wrapper // defining helper trait as we could directly call the method on the wrapper
pub(crate) trait InputSender { pub(crate) trait InputSender {
fn send_message(&self, message: InputMessage) -> Promise; fn send_message(&self, message: InputMessage) -> Promise;
fn send_messages(&self, messages: Vec<InputMessage>) -> Promise;
} }
impl InputSender for Arc<ClientInput> { impl InputSender for Arc<ClientInput> {
@@ -19,12 +45,114 @@ impl InputSender for Arc<ClientInput> {
future_to_promise(async move { future_to_promise(async move {
match this.input_sender.send(message).await { match this.input_sender.send(message).await {
Ok(_) => Ok(JsValue::null()), Ok(_) => Ok(JsValue::null()),
Err(_) => { Err(_) => Err(simple_js_error(
let js_error = "InputMessageReceiver has stopped receiving!",
js_sys::Error::new("InputMessageReceiver has stopped receiving!"); )),
Err(JsValue::from(js_error))
}
} }
}) })
} }
fn send_messages(&self, messages: Vec<InputMessage>) -> Promise {
let this = Arc::clone(self);
future_to_promise(async move {
for message in messages {
if this.input_sender.send(message).await.is_err() {
return Err(simple_js_error(
"InputMessageReceiver has stopped receiving!",
));
}
}
Ok(JsValue::null())
})
}
}
pub(crate) trait WasmTopologyExt {
/// Changes the current network topology to the provided value.
fn change_hardcoded_topology(&self, topology: WasmNymTopology) -> Promise;
/// Returns the current network topology.
fn current_topology(&self) -> Promise;
/// Checks whether the provided node exists in the known network topology and if so, returns its layer.
fn check_for_mixnode_existence(&self, mixnode_identity: String) -> Promise;
/// Creates a `NymClientTestRequest` with a variant of `this` topology where the target node is the only one on its layer.
fn mix_test_request(
&self,
test_id: u32,
mixnode_identity: String,
num_test_packets: Option<u32>,
) -> Promise;
}
impl WasmTopologyExt for Arc<ClientState> {
fn change_hardcoded_topology(&self, topology: WasmNymTopology) -> Promise {
let this = Arc::clone(self);
future_to_promise(async move {
let nym_topology: NymTopology = topology.into();
console_log!("changing topology to {nym_topology:?}");
this.topology_accessor
.manually_change_topology(nym_topology)
.await;
Ok(JsValue::null())
})
}
fn current_topology(&self) -> Promise {
let this = Arc::clone(self);
future_to_promise(async move {
match this.topology_accessor.current_topology().await {
Some(topology) => Ok(JsValue::from(WasmNymTopology::from(topology))),
None => Err(WasmClientError::UnavailableNetworkTopology.into()),
}
})
}
/// Checks whether the target mixnode exists in the known network topology and returns its layer.
fn check_for_mixnode_existence(&self, mixnode_identity: String) -> Promise {
let this = Arc::clone(self);
future_to_promise(async move {
let Some(current_topology) = this.topology_accessor.current_topology().await else {
return Err(WasmClientError::UnavailableNetworkTopology.into())
};
match current_topology.find_mix_by_identity(&mixnode_identity) {
None => Err(WasmClientError::NonExistentMixnode { mixnode_identity }.into()),
Some(node) => Ok(JsValue::from(MixLayer::from(node.layer))),
}
})
}
fn mix_test_request(
&self,
test_id: u32,
mixnode_identity: String,
num_test_packets: Option<u32>,
) -> Promise {
let num_test_packets = num_test_packets.unwrap_or(DEFAULT_TEST_PACKETS);
let this = Arc::clone(self);
future_to_promise(async move {
let Some(current_topology) = this.topology_accessor.current_topology().await else {
return Err(WasmClientError::UnavailableNetworkTopology.into())
};
let Some(mix) = current_topology.find_mix_by_identity(&mixnode_identity) else {
return Err(WasmClientError::NonExistentMixnode { mixnode_identity }.into());
};
let ext = WasmTestMessageExt::new(test_id);
let test_msgs = NodeTestMessage::mix_plaintexts(mix, num_test_packets, ext)
.map_err(WasmClientError::from)?;
let mut updated = current_topology.clone();
updated.set_mixes_in_layer(mix.layer.into(), vec![mix.to_owned()]);
Ok(JsValue::from(NymClientTestRequest {
test_msgs,
testable_topology: updated,
}))
})
}
} }
+178 -97
View File
@@ -1,27 +1,36 @@
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net> // Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use self::config::Config; use self::config::Config;
use crate::client::helpers::InputSender; use crate::client::helpers::{InputSender, NymClientTestRequest, WasmTopologyExt};
use crate::client::response_pusher::ResponsePusher; use crate::client::response_pusher::ResponsePusher;
use crate::error::WasmClientError;
use crate::helpers::{
parse_recipient, parse_sender_tag, setup_new_key_manager, setup_reply_surb_storage_backend,
};
use crate::topology::WasmNymTopology;
use js_sys::Promise; use js_sys::Promise;
use nym_bandwidth_controller::wasm_mockups::{Client as FakeClient, DirectSigningNyxdClient}; use nym_bandwidth_controller::wasm_mockups::{Client as FakeClient, DirectSigningNyxdClient};
use nym_bandwidth_controller::BandwidthController; use nym_bandwidth_controller::BandwidthController;
use nym_client_core::client::base_client::{ use nym_client_core::client::base_client::{
BaseClientBuilder, ClientInput, ClientOutput, CredentialsToggle, BaseClientBuilder, ClientInput, ClientOutput, ClientState, CredentialsToggle,
}; };
use nym_client_core::client::replies::reply_storage::browser_backend; use nym_client_core::client::replies::reply_storage::browser_backend;
use nym_client_core::client::{inbound_messages::InputMessage, key_manager::KeyManager}; use nym_client_core::client::{inbound_messages::InputMessage, key_manager::KeyManager};
use nym_client_core::config::{
CoverTraffic, DebugConfig, GatewayEndpointConfig, Topology, Traffic,
};
use nym_credential_storage::ephemeral_storage::EphemeralStorage; use nym_credential_storage::ephemeral_storage::EphemeralStorage;
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_task::connections::TransmissionLane; use nym_task::connections::TransmissionLane;
use nym_task::TaskManager; use nym_task::TaskManager;
use nym_topology::provider_trait::{HardcodedTopologyProvider, TopologyProvider};
use nym_topology::NymTopology;
use rand::rngs::OsRng; use rand::rngs::OsRng;
use rand::RngCore;
use std::sync::Arc; use std::sync::Arc;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::future_to_promise; use wasm_bindgen_futures::future_to_promise;
use wasm_utils::{console_error, console_log}; use wasm_utils::{check_promise_result, console_log, PromisableResult};
pub mod config; pub mod config;
mod helpers; mod helpers;
@@ -31,6 +40,11 @@ mod response_pusher;
pub struct NymClient { pub struct NymClient {
self_address: String, self_address: String,
client_input: Arc<ClientInput>, client_input: Arc<ClientInput>,
client_state: Arc<ClientState>,
// keep track of the "old" topology for the purposes of node tester
// so that it could be restored after the check is done
_full_topology: Option<NymTopology>,
// even though we don't use graceful shutdowns, other components rely on existence of this struct // even though we don't use graceful shutdowns, other components rely on existence of this struct
// and if it's dropped, everything will start going offline // and if it's dropped, everything will start going offline
@@ -40,6 +54,7 @@ pub struct NymClient {
#[wasm_bindgen] #[wasm_bindgen]
pub struct NymClientBuilder { pub struct NymClientBuilder {
config: Config, config: Config,
custom_topology: Option<NymTopology>,
/// KeyManager object containing smart pointers to all relevant keys used by the client. /// KeyManager object containing smart pointers to all relevant keys used by the client.
key_manager: KeyManager, key_manager: KeyManager,
@@ -60,118 +75,190 @@ impl NymClientBuilder {
pub fn new(config: Config, on_message: js_sys::Function) -> Self { pub fn new(config: Config, on_message: js_sys::Function) -> Self {
//, key_manager: Option<KeyManager>) { //, key_manager: Option<KeyManager>) {
NymClientBuilder { NymClientBuilder {
reply_surb_storage_backend: Self::setup_reply_surb_storage_backend(&config), reply_surb_storage_backend: setup_reply_surb_storage_backend(config.debug.reply_surbs),
config, config,
key_manager: Self::setup_key_manager(), custom_topology: None,
key_manager: setup_new_key_manager(),
on_message, on_message,
bandwidth_controller: None, bandwidth_controller: None,
disabled_credentials: true, disabled_credentials: true,
} }
} }
// TODO: once we make keys persistent, we'll require some kind of `init` method to generate // no cover traffic
// a prior shared keypair between the client and the gateway // no poisson delay
// hardcoded topology
// NOTE: you most likely want to use `[NymNodeTester]` instead.
pub fn new_tester(
gateway_config: GatewayEndpointConfig,
topology: WasmNymTopology,
on_message: js_sys::Function,
) -> Self {
if !topology.ensure_contains(&gateway_config) {
panic!("the specified topology does not contain the gateway used by the client")
}
// perhaps this should be public? let full_config = Config {
fn setup_key_manager() -> KeyManager { id: "ephemeral-id".to_string(),
let mut rng = OsRng; nym_api_url: None,
// for time being generate new keys each time... disabled_credentials_mode: true,
console_log!("generated new set of keys"); gateway_endpoint: gateway_config,
KeyManager::new(&mut rng) debug: DebugConfig {
} traffic: Traffic {
disable_main_poisson_packet_distribution: true,
..Default::default()
},
cover_traffic: CoverTraffic {
disable_loop_cover_traffic_stream: true,
..Default::default()
},
topology: Topology {
disable_refreshing: true,
..Default::default()
},
..Default::default()
},
};
// don't get too excited about the name, under the hood it's just a big fat placeholder NymClientBuilder {
// with no persistence reply_surb_storage_backend: setup_reply_surb_storage_backend(
fn setup_reply_surb_storage_backend(config: &Config) -> browser_backend::Backend { full_config.debug.reply_surbs,
browser_backend::Backend::new( ),
config config: full_config,
.debug custom_topology: Some(topology.into()),
.reply_surbs // TODO: once we make keys persistent, we'll require some kind of `init` method to generate
.minimum_reply_surb_storage_threshold, // a prior shared keypair between the client and the gateway
config key_manager: setup_new_key_manager(),
.debug on_message,
.reply_surbs bandwidth_controller: None,
.maximum_reply_surb_storage_threshold, disabled_credentials: true,
) }
} }
fn start_reconstructed_pusher(client_output: ClientOutput, on_message: js_sys::Function) { fn start_reconstructed_pusher(client_output: ClientOutput, on_message: js_sys::Function) {
ResponsePusher::new(client_output, on_message).start() ResponsePusher::new(client_output, on_message).start()
} }
pub async fn start_client(self) -> Promise { fn topology_provider(&mut self) -> Option<Box<dyn TopologyProvider>> {
future_to_promise(async move { if let Some(hardcoded_topology) = self.custom_topology.take() {
console_log!("Starting the wasm client"); Some(Box::new(HardcodedTopologyProvider::new(hardcoded_topology)))
} else {
None
}
}
let disabled_credentials = if self.disabled_credentials { async fn start_client_async(mut self) -> Result<NymClient, WasmClientError> {
CredentialsToggle::Disabled console_log!("Starting the wasm client");
} else {
CredentialsToggle::Enabled
};
let base_builder = BaseClientBuilder::new( let maybe_topology_provider = self.topology_provider();
&self.config.gateway_endpoint,
&self.config.debug,
self.key_manager,
self.bandwidth_controller,
self.reply_surb_storage_backend,
disabled_credentials,
vec![self.config.nym_api_url.clone()],
);
let self_address = base_builder.as_mix_recipient().to_string(); let disabled_credentials = if self.disabled_credentials {
let mut started_client = match base_builder.start_base().await { CredentialsToggle::Disabled
Ok(base_client) => base_client, } else {
Err(err) => { CredentialsToggle::Enabled
let error_msg = format!("failed to start the base client components - {err}"); };
console_error!("{}", error_msg);
let js_error = js_sys::Error::new(&error_msg);
return Err(JsValue::from(js_error));
}
};
let client_input = started_client.client_input.register_producer(); let nym_api_endpoints = match self.config.nym_api_url {
let client_output = started_client.client_output.register_consumer(); Some(endpoint) => vec![endpoint],
None => Vec::new(),
};
let mut base_builder = BaseClientBuilder::new(
&self.config.gateway_endpoint,
&self.config.debug,
self.key_manager,
self.bandwidth_controller,
self.reply_surb_storage_backend,
disabled_credentials,
nym_api_endpoints,
);
if let Some(topology_provider) = maybe_topology_provider {
base_builder = base_builder.with_topology_provider(topology_provider);
}
Self::start_reconstructed_pusher(client_output, self.on_message); let self_address = base_builder.as_mix_recipient().to_string();
let mut started_client = base_builder.start_base().await?;
Ok(JsValue::from(NymClient { let client_input = started_client.client_input.register_producer();
self_address, let client_output = started_client.client_output.register_consumer();
client_input: Arc::new(client_input),
_task_manager: started_client.task_manager, Self::start_reconstructed_pusher(client_output, self.on_message);
}))
Ok(NymClient {
self_address,
client_input: Arc::new(client_input),
client_state: Arc::new(started_client.client_state),
_full_topology: None,
_task_manager: started_client.task_manager,
}) })
} }
pub fn start_client(self) -> Promise {
future_to_promise(async move { self.start_client_async().await.into_promise_result() })
}
} }
#[wasm_bindgen] #[wasm_bindgen]
impl NymClient { impl NymClient {
async fn _new(
config: Config,
on_message: js_sys::Function,
) -> Result<NymClient, WasmClientError> {
NymClientBuilder::new(config, on_message)
.start_client_async()
.await
}
#[wasm_bindgen(constructor)]
#[allow(clippy::new_ret_no_self)]
pub fn new(config: Config, on_message: js_sys::Function) -> Promise {
future_to_promise(async move { Self::_new(config, on_message).await.into_promise_result() })
}
pub fn self_address(&self) -> String { pub fn self_address(&self) -> String {
self.self_address.clone() self.self_address.clone()
} }
fn parse_recipient(recipient: &str) -> Result<Recipient, JsValue> { pub fn try_construct_test_packet_request(
match Recipient::try_from_base58_string(recipient) { &self,
Ok(recipient) => Ok(recipient), mixnode_identity: String,
Err(err) => { num_test_packets: Option<u32>,
let error_msg = format!("{recipient} is not a valid Nym network recipient - {err}"); ) -> Promise {
console_error!("{}", error_msg); // TODO: improve the source of rng (i.e. don't make it ephemeral...)
let js_error = js_sys::Error::new(&error_msg); let mut ephemeral_rng = OsRng;
Err(JsValue::from(js_error)) let test_id = ephemeral_rng.next_u32();
} self.client_state
} .mix_test_request(test_id, mixnode_identity, num_test_packets)
} }
fn parse_sender_tag(tag: &str) -> Result<AnonymousSenderTag, JsValue> { pub fn change_hardcoded_topology(&self, topology: WasmNymTopology) -> Promise {
match AnonymousSenderTag::try_from_base58_string(tag) { self.client_state.change_hardcoded_topology(topology)
Ok(tag) => Ok(tag), }
Err(err) => {
let error_msg = format!("{tag} is not a valid Nym AnonymousSenderTag - {err}"); pub fn current_network_topology(&self) -> Promise {
console_error!("{}", error_msg); self.client_state.current_topology()
let js_error = js_sys::Error::new(&error_msg); }
Err(JsValue::from(js_error))
} /// Sends a test packet through the current network topology.
} /// It's the responsibility of the caller to ensure the correct topology has been injected and
/// correct onmessage handlers have been setup.
pub fn try_send_test_packets(&mut self, request: NymClientTestRequest) -> Promise {
// TOOD: use the premade packets instead
console_log!(
"Attempting to send {} test packets",
request.test_msgs.len()
);
// our address MUST BE valid
let recipient = parse_recipient(&self.self_address()).unwrap();
let lane = TransmissionLane::General;
let input_msgs = request
.test_msgs
.into_iter()
.map(|p| InputMessage::new_regular(recipient, p, lane))
.collect();
self.client_input.send_messages(input_msgs)
} }
/// The simplest message variant where no additional information is attached. /// The simplest message variant where no additional information is attached.
@@ -184,10 +271,8 @@ impl NymClient {
message.len() as f64 / 1024.0 message.len() as f64 / 1024.0
); );
let recipient = match Self::parse_recipient(&recipient) { let recipient = check_promise_result!(parse_recipient(&recipient));
Ok(recipient) => recipient,
Err(err) => return Promise::reject(&err),
};
let lane = TransmissionLane::General; let lane = TransmissionLane::General;
let input_msg = InputMessage::new_regular(recipient, message, lane); let input_msg = InputMessage::new_regular(recipient, message, lane);
@@ -213,10 +298,8 @@ impl NymClient {
message.len() as f64 / 1024.0 message.len() as f64 / 1024.0
); );
let recipient = match Self::parse_recipient(&recipient) { let recipient = check_promise_result!(parse_recipient(&recipient));
Ok(recipient) => recipient,
Err(err) => return Promise::reject(&err),
};
let lane = TransmissionLane::General; let lane = TransmissionLane::General;
let input_msg = InputMessage::new_anonymous(recipient, message, reply_surbs, lane); let input_msg = InputMessage::new_anonymous(recipient, message, reply_surbs, lane);
@@ -233,10 +316,8 @@ impl NymClient {
message.len() as f64 / 1024.0 message.len() as f64 / 1024.0
); );
let sender_tag = match Self::parse_sender_tag(&recipient_tag) { let sender_tag = check_promise_result!(parse_sender_tag(&recipient_tag));
Ok(recipient) => recipient,
Err(err) => return Promise::reject(&err),
};
let lane = TransmissionLane::General; let lane = TransmissionLane::General;
let input_msg = InputMessage::new_reply(sender_tag, message, lane); let input_msg = InputMessage::new_reply(sender_tag, message, lane);
+99
View File
@@ -0,0 +1,99 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::topology::WasmTopologyError;
use js_sys::Promise;
use nym_client_core::error::ClientCoreError;
use nym_crypto::asymmetric::identity::Ed25519RecoveryError;
use nym_gateway_client::error::GatewayClientError;
use nym_node_tester_utils::error::NetworkTestingError;
use nym_sphinx::addressing::clients::RecipientFormattingError;
use nym_sphinx::anonymous_replies::requests::InvalidAnonymousSenderTagRepresentation;
use nym_validator_client::ValidatorClientError;
use thiserror::Error;
use wasm_bindgen::JsValue;
use wasm_utils::simple_js_error;
// might as well start using well-defined error enum...
#[derive(Debug, Error)]
pub enum WasmClientError {
#[error(
"A node test is already in progress. Wait for it to finish before starting another one."
)]
TestInProgress,
#[error("experienced an issue with internal client components: {source}")]
BaseClientError {
#[from]
source: ClientCoreError,
},
#[error("The provided gateway identity is invalid: {source}")]
InvalidGatewayIdentity { source: Ed25519RecoveryError },
#[error("Gateway communication failure: {source}")]
GatewayClientError {
#[from]
source: GatewayClientError,
},
#[error("failed to query nym api: {source}")]
NymApiError {
#[from]
source: ValidatorClientError,
},
#[error("The provided topology was invalid: {source}")]
WasmTopologyError {
#[from]
source: WasmTopologyError,
},
#[error("failed to test the node: {source}")]
NodeTestingFailure {
#[from]
source: NetworkTestingError,
},
#[error("{raw} is not a valid url: {source}")]
MalformedUrl {
raw: String,
source: url::ParseError,
},
#[error("Network topology is currently unavailable")]
UnavailableNetworkTopology,
#[error("Mixnode {mixnode_identity} is not present in the current network topology")]
NonExistentMixnode { mixnode_identity: String },
#[error("{raw} is not a valid Nym network recipient: {source}")]
MalformedRecipient {
raw: String,
source: RecipientFormattingError,
},
#[error("{raw} is not a valid Nym AnonymousSenderTag: {source}")]
MalformedSenderTag {
raw: String,
source: InvalidAnonymousSenderTagRepresentation,
},
}
impl WasmClientError {
pub fn into_rejected_promise(self) -> Promise {
self.into()
}
}
impl From<WasmClientError> for JsValue {
fn from(value: WasmClientError) -> Self {
simple_js_error(value.to_string())
}
}
impl From<WasmClientError> for Promise {
fn from(value: WasmClientError) -> Self {
Promise::reject(&value.into())
}
}
+82
View File
@@ -0,0 +1,82 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::WasmClientError;
use crate::topology::WasmNymTopology;
use js_sys::Promise;
use nym_client_core::client::key_manager::KeyManager;
use nym_client_core::client::replies::reply_storage::browser_backend;
use nym_client_core::config;
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_topology::NymTopology;
use nym_validator_client::NymApiClient;
use rand::rngs::OsRng;
use url::Url;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen_futures::future_to_promise;
use wasm_utils::{console_log, PromisableResult};
pub(crate) fn setup_new_key_manager() -> KeyManager {
let mut rng = OsRng;
console_log!("generated new set of keys");
KeyManager::new(&mut rng)
}
// don't get too excited about the name, under the hood it's just a big fat placeholder
// with no persistence
pub(crate) fn setup_reply_surb_storage_backend(
config: config::ReplySurbs,
) -> browser_backend::Backend {
browser_backend::Backend::new(
config.minimum_reply_surb_storage_threshold,
config.maximum_reply_surb_storage_threshold,
)
}
pub(crate) fn parse_recipient(recipient: &str) -> Result<Recipient, WasmClientError> {
Recipient::try_from_base58_string(recipient).map_err(|source| {
WasmClientError::MalformedRecipient {
raw: recipient.to_string(),
source,
}
})
}
pub(crate) fn parse_sender_tag(tag: &str) -> Result<AnonymousSenderTag, WasmClientError> {
AnonymousSenderTag::try_from_base58_string(tag).map_err(|source| {
WasmClientError::MalformedSenderTag {
raw: tag.to_string(),
source,
}
})
}
pub(crate) async fn current_network_topology_async(
nym_api_url: String,
) -> Result<WasmNymTopology, WasmClientError> {
let url: Url = match nym_api_url.parse() {
Ok(url) => url,
Err(source) => {
return Err(WasmClientError::MalformedUrl {
raw: nym_api_url,
source,
})
}
};
let api_client = NymApiClient::new(url);
let mixnodes = api_client.get_cached_active_mixnodes().await?;
let gateways = api_client.get_cached_gateways().await?;
Ok(NymTopology::from_detailed(mixnodes, gateways).into())
}
#[wasm_bindgen]
pub fn current_network_topology(nym_api_url: String) -> Promise {
future_to_promise(async move {
current_network_topology_async(nym_api_url)
.await
.into_promise_result()
})
}
+8
View File
@@ -7,11 +7,19 @@ use wasm_bindgen::prelude::*;
mod client; mod client;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub mod encoded_payload_helper; pub mod encoded_payload_helper;
pub mod error;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub mod gateway_selector; pub mod gateway_selector;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub mod tester;
#[cfg(target_arch = "wasm32")]
pub mod topology;
#[cfg(target_arch = "wasm32")]
pub mod validation; pub mod validation;
#[cfg(target_arch = "wasm32")]
mod helpers;
#[wasm_bindgen] #[wasm_bindgen]
pub fn set_panic_hook() { pub fn set_panic_hook() {
// When the `console_error_panic_hook` feature is enabled, we can call the // When the `console_error_panic_hook` feature is enabled, we can call the
@@ -0,0 +1,112 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::tester::helpers::{NodeTestResult, WasmTestMessageExt};
use futures::StreamExt;
use nym_node_tester_utils::processor::Received;
use nym_node_tester_utils::receiver::ReceivedReceiver;
use nym_sphinx::chunking::fragment::FragmentIdentifier;
use std::collections::HashSet;
use std::time::Duration;
use tokio::sync::MutexGuard as AsyncMutexGuard;
use wasm_utils::{console_error, console_log, console_warn};
pub(crate) struct EphemeralTestReceiver<'a> {
sent_packets: u32,
expected_acks: HashSet<FragmentIdentifier>,
received_valid_messages: HashSet<u32>,
received_valid_acks: HashSet<FragmentIdentifier>,
duplicate_packets: u32,
duplicate_acks: u32,
timeout_duration: Duration,
receiver_permit: AsyncMutexGuard<'a, ReceivedReceiver<WasmTestMessageExt>>,
}
impl<'a> EphemeralTestReceiver<'a> {
pub(crate) fn finish(self) -> NodeTestResult {
NodeTestResult {
sent_packets: self.sent_packets,
received_packets: self.received_valid_messages.len() as u32,
received_acks: self.received_valid_acks.len() as u32,
duplicate_packets: self.duplicate_packets,
duplicate_acks: self.duplicate_acks,
}
}
pub(crate) fn new(
sent_packets: u32,
expected_acks: HashSet<FragmentIdentifier>,
receiver_permit: AsyncMutexGuard<'a, ReceivedReceiver<WasmTestMessageExt>>,
timeout: Duration,
) -> Self {
EphemeralTestReceiver {
sent_packets,
expected_acks,
received_valid_messages: Default::default(),
received_valid_acks: Default::default(),
duplicate_packets: 0,
duplicate_acks: 0,
timeout_duration: timeout,
receiver_permit,
}
}
fn on_next_received_packet(&mut self, packet: Option<Received<WasmTestMessageExt>>) -> bool {
let Some(received_packet) = packet else {
// can't do anything more...
console_error!("packet receiver has stopped processing results!");
return true
};
match received_packet {
Received::Message(msg) => {
if !self.received_valid_messages.insert(msg.msg_id) {
self.duplicate_packets += 1;
}
}
Received::Ack(frag_id) => {
if self.expected_acks.contains(&frag_id) {
if !self.received_valid_acks.insert(frag_id) {
self.duplicate_acks += 1
}
} else {
console_warn!("received an ack that was not part of the test! (id: {frag_id})")
}
}
}
if self.received_all() {
console_log!("already received all the packets! finishing the test...");
true
} else {
false
}
}
fn received_all(&self) -> bool {
self.received_valid_acks.len() == self.received_valid_messages.len()
&& self.received_valid_acks.len() == self.sent_packets as usize
}
pub(crate) async fn perform_test(mut self) -> NodeTestResult {
let mut timeout_fut = wasm_timer::Delay::new(self.timeout_duration);
loop {
tokio::select! {
_ = &mut timeout_fut => {
console_warn!("reached test timeout before receiving all packets.");
break
}
received_packet = self.receiver_permit.next() => {
let is_done = self.on_next_received_packet(received_packet);
if is_done {
break
}
}
}
}
self.finish()
}
}
+109
View File
@@ -0,0 +1,109 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// due to expansion of #[wasm_bindgen] macro on NodeTestResult
#![allow(clippy::drop_non_drop)]
use nym_node_tester_utils::processor::Received;
use nym_node_tester_utils::receiver::ReceivedReceiver;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::sync::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard};
use wasm_bindgen::prelude::*;
use wasm_utils::{console_log, console_warn};
#[derive(Clone)]
pub(super) struct ReceivedReceiverWrapper(Arc<AsyncMutex<ReceivedReceiver<WasmTestMessageExt>>>);
impl ReceivedReceiverWrapper {
pub(super) fn new(inner: ReceivedReceiver<WasmTestMessageExt>) -> Self {
ReceivedReceiverWrapper(Arc::new(AsyncMutex::new(inner)))
}
pub(super) async fn clear_received_channel(&self) {
let mut lost_msgs = 0;
let mut lost_acks = 0;
let mut permit = self.0.lock().await;
while let Ok(Some(received)) = permit.try_next() {
match received {
Received::Message(_) => lost_msgs += 1,
Received::Ack(_) => lost_acks += 1,
}
}
if lost_msgs > 0 || lost_acks > 0 {
console_warn!("while preparing for the test run, we cleared {lost_msgs} messages and {lost_acks} acks that were received in the meantime.")
}
}
pub(super) async fn lock(&self) -> AsyncMutexGuard<'_, ReceivedReceiver<WasmTestMessageExt>> {
self.0.lock().await
}
}
#[derive(Serialize, Deserialize, Copy, Clone)]
pub struct WasmTestMessageExt {
pub test_id: u32,
}
impl WasmTestMessageExt {
pub fn new(test_id: u32) -> Self {
WasmTestMessageExt { test_id }
}
}
// TODO: maybe put it in the tester utils
#[wasm_bindgen]
pub struct NodeTestResult {
pub sent_packets: u32,
pub received_packets: u32,
pub received_acks: u32,
pub duplicate_packets: u32,
pub duplicate_acks: u32,
}
impl Display for NodeTestResult {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Test results: ")?;
writeln!(f, "Total score: {:.2}%", self.score())?;
writeln!(f, "Sent packets: {}", self.sent_packets)?;
writeln!(f, "Received (valid) packets: {}", self.received_packets)?;
writeln!(f, "Received (valid) acks: {}", self.received_acks)?;
writeln!(f, "Received duplicate packets: {}", self.duplicate_packets)?;
write!(f, "Received duplicate acks: {}", self.duplicate_acks)
}
}
#[wasm_bindgen]
impl NodeTestResult {
pub fn log_details(&self) {
console_log!("{}", self)
}
pub fn score(&self) -> f32 {
let expected = self.sent_packets * 2;
let actual = (self.received_packets + self.received_acks)
.saturating_sub(self.duplicate_packets + self.duplicate_acks);
actual as f32 / expected as f32 * 100.
}
}
pub(crate) struct TestMarker {
value: Arc<AtomicBool>,
}
impl TestMarker {
pub fn new(value: Arc<AtomicBool>) -> Self {
Self { value }
}
}
impl Drop for TestMarker {
// make sure to clear the test flag when the marker is dropped
fn drop(&mut self) {
self.value.store(false, Ordering::SeqCst)
}
}
+321
View File
@@ -0,0 +1,321 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::WasmClientError;
use crate::helpers::{current_network_topology_async, setup_new_key_manager};
use crate::tester::ephemeral_receiver::EphemeralTestReceiver;
use crate::tester::helpers::{
NodeTestResult, ReceivedReceiverWrapper, TestMarker, WasmTestMessageExt,
};
use crate::topology::WasmNymTopology;
use futures::channel::mpsc;
use js_sys::Promise;
use nym_bandwidth_controller::wasm_mockups::{Client as FakeClient, DirectSigningNyxdClient};
use nym_bandwidth_controller::BandwidthController;
use nym_client_core::client::key_manager::KeyManager;
use nym_client_core::config::GatewayEndpointConfig;
use nym_credential_storage::ephemeral_storage::EphemeralStorage;
use nym_crypto::asymmetric::identity;
use nym_gateway_client::GatewayClient;
use nym_node_tester_utils::receiver::SimpleMessageReceiver;
use nym_node_tester_utils::{NodeTester, TestMessage};
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::addressing::nodes::NodeIdentity;
use nym_sphinx::params::PacketSize;
use nym_sphinx::preparer::PreparedFragment;
use nym_task::TaskManager;
use nym_topology::NymTopology;
use rand::rngs::OsRng;
use std::collections::HashSet;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::{Arc, Mutex as SyncMutex};
use std::time::Duration;
use tokio::sync::Mutex as AsyncMutex;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::future_to_promise;
use wasm_utils::{check_promise_result, console_log, console_warn, PromisableResult};
mod ephemeral_receiver;
pub(crate) mod helpers;
pub type NodeTestMessage = TestMessage<WasmTestMessageExt>;
type LockedGatewayClient =
Arc<AsyncMutex<GatewayClient<FakeClient<DirectSigningNyxdClient>, EphemeralStorage>>>;
pub(crate) const DEFAULT_TEST_TIMEOUT: Duration = Duration::from_secs(10);
pub(crate) const DEFAULT_TEST_PACKETS: u32 = 20;
#[wasm_bindgen]
pub struct NymNodeTester {
test_in_progress: Arc<AtomicBool>,
// we need to increment the nonce between tests to distinguish the packets
// but we can't make the tester mutable because of wasm...
// so we're using the atomics
current_test_nonce: AtomicU32,
// blame all those mutexes on being unable to have an async method with internal mutability...
tester: Arc<SyncMutex<NodeTester<OsRng>>>,
gateway_client: LockedGatewayClient,
// we have to put it behind the lock due to wasm limitations and borrowing...
// the mutex acquisition should be instant as there aren't going to be any threads attempting
// to get simultaneous access
processed_receiver: ReceivedReceiverWrapper,
// even though we don't use graceful shutdowns, other components rely on existence of this struct
// and if it's dropped, everything will start going offline
_task_manager: TaskManager,
}
#[wasm_bindgen]
pub struct NymNodeTesterBuilder {
gateway_config: GatewayEndpointConfig,
base_topology: NymTopology,
/// KeyManager object containing smart pointers to all relevant keys used by the client.
key_manager: KeyManager,
// unimplemented
bandwidth_controller:
Option<BandwidthController<FakeClient<DirectSigningNyxdClient>, EphemeralStorage>>,
}
fn address(keys: &KeyManager, gateway_identity: NodeIdentity) -> Recipient {
Recipient::new(
*keys.identity_keypair().public_key(),
*keys.encryption_keypair().public_key(),
gateway_identity,
)
}
#[wasm_bindgen]
impl NymNodeTesterBuilder {
#[wasm_bindgen(constructor)]
pub fn new(
gateway_config: GatewayEndpointConfig,
base_topology: WasmNymTopology,
) -> NymNodeTesterBuilder {
NymNodeTesterBuilder {
gateway_config,
base_topology: base_topology.into(),
key_manager: setup_new_key_manager(),
bandwidth_controller: None,
}
}
async fn _new_with_api(
gateway_config: GatewayEndpointConfig,
api_url: String,
) -> Result<Self, WasmClientError> {
let topology = current_network_topology_async(api_url).await?;
Ok(NymNodeTesterBuilder::new(gateway_config, topology))
}
pub fn new_with_api(gateway_config: GatewayEndpointConfig, api_url: String) -> Promise {
future_to_promise(async move {
Self::_new_with_api(gateway_config, api_url)
.await
.into_promise_result()
})
}
async fn _setup_client(mut self) -> Result<NymNodeTester, WasmClientError> {
let rng = OsRng;
let task_manager = TaskManager::default();
let gateway_identity =
identity::PublicKey::from_base58_string(self.gateway_config.gateway_id)
.map_err(|source| WasmClientError::InvalidGatewayIdentity { source })?;
// we **REALLY** need persistence...
let shared_key = if self.key_manager.is_gateway_key_set() {
Some(self.key_manager.gateway_shared_key())
} else {
console_warn!("Gateway key not set - will derive a fresh one.");
None
};
let (mixnet_message_sender, mixnet_message_receiver) = mpsc::unbounded();
let (ack_sender, ack_receiver) = mpsc::unbounded();
let mut gateway_client = GatewayClient::new(
self.gateway_config.gateway_listener,
self.key_manager.identity_keypair(),
gateway_identity,
shared_key,
mixnet_message_sender,
ack_sender,
Duration::from_secs(10),
self.bandwidth_controller.take(),
task_manager.subscribe(),
);
gateway_client.set_disabled_credentials_mode(true);
let shared_keys = gateway_client.authenticate_and_start().await?;
// currently pointless but might as well do it for the future ¯\_(ツ)_/¯
self.key_manager.insert_gateway_shared_key(shared_keys);
// TODO: make those values configurable later
let tester = NodeTester::new(
rng,
self.base_topology,
Some(address(&self.key_manager, gateway_identity)),
PacketSize::default(),
Duration::from_millis(5),
Duration::from_millis(5),
self.key_manager.ack_key(),
);
let (processed_sender, processed_receiver) = mpsc::unbounded();
let mut receiver = SimpleMessageReceiver::new_sphinx_receiver(
self.key_manager.encryption_keypair(),
self.key_manager.ack_key(),
mixnet_message_receiver,
ack_receiver,
processed_sender,
task_manager.subscribe(),
);
nym_task::spawn(async move { receiver.run().await });
Ok(NymNodeTester {
test_in_progress: Arc::new(AtomicBool::new(false)),
current_test_nonce: Default::default(),
tester: Arc::new(SyncMutex::new(tester)),
gateway_client: Arc::new(AsyncMutex::new(gateway_client)),
processed_receiver: ReceivedReceiverWrapper::new(processed_receiver),
_task_manager: task_manager,
})
}
pub fn setup_client(self) -> Promise {
future_to_promise(async move { self._setup_client().await.into_promise_result() })
}
}
async fn test_mixnode(
test_packets: Vec<PreparedFragment>,
gateway_client: LockedGatewayClient,
processed_receiver: ReceivedReceiverWrapper,
_test_marker: TestMarker,
timeout: Duration,
) -> Result<NodeTestResult, WasmClientError> {
let num_test_packets = test_packets.len() as u32;
let expected_ack_ids = test_packets
.iter()
.map(|p| p.fragment_identifier)
.collect::<HashSet<_>>();
let mix_packets = test_packets.into_iter().map(|p| p.mix_packet).collect();
// start by clearing any messages that might have been received between tests
processed_receiver.clear_received_channel().await;
// locking the gateway client so that we could get mutable access to data without having to declare
// self mutable
let mut gateway_permit = gateway_client.lock().await;
gateway_permit.batch_send_mix_packets(mix_packets).await?;
let receiver_permit = processed_receiver.lock().await;
let result =
EphemeralTestReceiver::new(num_test_packets, expected_ack_ids, receiver_permit, timeout)
.perform_test()
.await;
Ok(result)
}
#[wasm_bindgen]
impl NymNodeTester {
#[wasm_bindgen(constructor)]
#[allow(clippy::new_ret_no_self)]
pub fn new(gateway_config: GatewayEndpointConfig, topology: WasmNymTopology) -> Promise {
console_log!("constructing node tester!");
NymNodeTesterBuilder::new(gateway_config, topology).setup_client()
}
async fn _new_with_api(
gateway_config: GatewayEndpointConfig,
api_url: String,
) -> Result<Self, WasmClientError> {
NymNodeTesterBuilder::_new_with_api(gateway_config, api_url)
.await?
._setup_client()
.await
}
pub fn new_with_api(gateway_config: GatewayEndpointConfig, api_url: String) -> Promise {
future_to_promise(async move {
Self::_new_with_api(gateway_config, api_url)
.await
.into_promise_result()
})
}
fn prepare_test_packets(
&self,
mixnode_identity: String,
test_nonce: u32,
num_test_packets: u32,
) -> Result<Vec<PreparedFragment>, WasmClientError> {
let test_ext = WasmTestMessageExt::new(test_nonce);
let mut tester_permit = self.tester.lock().expect("mutex got poisoned");
tester_permit
.existing_identity_mixnode_test_packets(
mixnode_identity,
test_ext,
num_test_packets,
None,
)
.map_err(Into::into)
}
pub fn test_node(
&self,
mixnode_identity: String,
timeout_millis: Option<u64>,
num_test_packets: Option<u32>,
) -> Promise {
// establish test parameters
let timeout = timeout_millis
.map(Duration::from_millis)
.unwrap_or(DEFAULT_TEST_TIMEOUT);
let num_test_packets = num_test_packets.unwrap_or(DEFAULT_TEST_PACKETS);
// mark start of the test
if self.test_in_progress.swap(true, Ordering::SeqCst) {
return WasmClientError::TestInProgress.into_rejected_promise();
}
// prepare test packets
// (I simultaneously feel both disgusted and amazed by this workaround)
let test_nonce = self.current_test_nonce.fetch_add(1, Ordering::Relaxed);
let test_packets = check_promise_result!(self.prepare_test_packets(
mixnode_identity,
test_nonce,
num_test_packets
));
let processed_receiver_clone = self.processed_receiver.clone();
let gateway_client_clone = Arc::clone(&self.gateway_client);
let tester_marker = TestMarker::new(Arc::clone(&self.test_in_progress));
// start doing async things (send packets and watch for anything coming back)
future_to_promise(async move {
test_mixnode(
test_packets,
gateway_client_clone,
processed_receiver_clone,
tester_marker,
timeout,
)
.await
.into_promise_result()
})
}
}
+262
View File
@@ -0,0 +1,262 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_client_core::config::GatewayEndpointConfig;
use nym_crypto::asymmetric::{encryption, identity};
use nym_topology::gateway::GatewayConversionError;
use nym_topology::mix::{Layer, MixnodeConversionError};
use nym_topology::{gateway, mix, MixLayer, NymTopology};
use nym_validator_client::client::MixId;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use thiserror::Error;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;
use wasm_utils::{console_log, simple_js_error};
#[derive(Debug, Error)]
pub enum WasmTopologyError {
#[error("got invalid mix layer {value}. Expected 1, 2 or 3.")]
InvalidMixLayer { value: u8 },
#[error(transparent)]
GatewayConversion(#[from] GatewayConversionError),
#[error(transparent)]
MixnodeConversion(#[from] MixnodeConversionError),
#[error("The provided mixnode map was malformed: {source}")]
MalformedMixnodeMap { source: serde_wasm_bindgen::Error },
#[error("The provided gateway list was malformed: {source}")]
MalformedGatewayList { source: serde_wasm_bindgen::Error },
}
impl From<WasmTopologyError> for JsValue {
fn from(value: WasmTopologyError) -> Self {
simple_js_error(value.to_string())
}
}
#[wasm_bindgen]
#[derive(Debug)]
pub struct WasmNymTopology {
inner: NymTopology,
}
#[wasm_bindgen]
impl WasmNymTopology {
#[wasm_bindgen(constructor)]
pub fn new(
// expected: BTreeMap<MixLayer, Vec<WasmMixNode>>,
// HashMap<MixLayer, Vec<WasmMixNode>> will also work because it has the same json representation
mixnodes: JsValue,
// expected: Vec<WasmGateway>
gateways: JsValue,
) -> Result<WasmNymTopology, WasmTopologyError> {
let mixnodes: BTreeMap<MixLayer, Vec<WasmMixNode>> =
serde_wasm_bindgen::from_value(mixnodes)
.map_err(|source| WasmTopologyError::MalformedMixnodeMap { source })?;
let gateways: Vec<WasmGateway> = serde_wasm_bindgen::from_value(gateways)
.map_err(|source| WasmTopologyError::MalformedGatewayList { source })?;
let mut converted_mixes = BTreeMap::new();
for (layer, nodes) in mixnodes {
let layer_nodes = nodes
.into_iter()
.map(TryInto::try_into)
.collect::<Result<_, _>>()?;
converted_mixes.insert(layer, layer_nodes);
}
let gateways = gateways
.into_iter()
.map(TryInto::try_into)
.collect::<Result<_, _>>()?;
Ok(WasmNymTopology {
inner: NymTopology::new(converted_mixes, gateways),
})
}
pub(crate) fn ensure_contains(&self, gateway_config: &GatewayEndpointConfig) -> bool {
self.inner
.gateways()
.iter()
.any(|g| g.identity_key.to_base58_string() == gateway_config.gateway_id)
}
pub fn print(&self) {
if !self.inner.mixes().is_empty() {
console_log!("mixnodes:");
for (layer, nodes) in self.inner.mixes() {
console_log!("\tlayer {layer}:");
for node in nodes {
console_log!("\t\t{} - {}", node.mix_id, node.identity_key)
}
}
} else {
console_log!("NO MIXNODES")
}
if !self.inner.gateways().is_empty() {
console_log!("gateways:");
for gateway in self.inner.gateways() {
console_log!("\t{}", gateway.identity_key)
}
} else {
console_log!("NO GATEWAYS")
}
}
}
impl From<WasmNymTopology> for NymTopology {
fn from(value: WasmNymTopology) -> Self {
value.inner
}
}
impl From<NymTopology> for WasmNymTopology {
fn from(value: NymTopology) -> Self {
WasmNymTopology { inner: value }
}
}
#[wasm_bindgen]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct WasmMixNode {
pub mix_id: MixId,
#[wasm_bindgen(getter_with_clone)]
pub owner: String,
#[wasm_bindgen(getter_with_clone)]
pub host: String,
pub mix_port: u16,
#[wasm_bindgen(getter_with_clone)]
pub identity_key: String,
#[wasm_bindgen(getter_with_clone)]
pub sphinx_key: String,
pub layer: MixLayer,
#[wasm_bindgen(getter_with_clone)]
pub version: String,
}
#[wasm_bindgen]
impl WasmMixNode {
#[wasm_bindgen(constructor)]
#[allow(clippy::too_many_arguments)]
pub fn new(
mix_id: MixId,
owner: String,
host: String,
mix_port: u16,
identity_key: String,
sphinx_key: String,
layer: MixLayer,
version: String,
) -> Self {
Self {
mix_id,
owner,
host,
mix_port,
identity_key,
sphinx_key,
layer,
version,
}
}
}
impl TryFrom<WasmMixNode> for mix::Node {
type Error = WasmTopologyError;
fn try_from(value: WasmMixNode) -> Result<Self, Self::Error> {
let host = mix::Node::parse_host(&value.host)?;
// try to completely resolve the host in the mix situation to avoid doing it every
// single time we want to construct a path
let mix_host = mix::Node::extract_mix_host(&host, value.mix_port)?;
Ok(mix::Node {
mix_id: value.mix_id,
owner: value.owner,
host,
mix_host,
identity_key: identity::PublicKey::from_base58_string(&value.identity_key)
.map_err(MixnodeConversionError::from)?,
sphinx_key: encryption::PublicKey::from_base58_string(&value.sphinx_key)
.map_err(MixnodeConversionError::from)?,
layer: Layer::try_from(value.layer)
.map_err(|_| WasmTopologyError::InvalidMixLayer { value: value.layer })?,
version: value.version,
})
}
}
#[wasm_bindgen]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct WasmGateway {
#[wasm_bindgen(getter_with_clone)]
pub owner: String,
#[wasm_bindgen(getter_with_clone)]
pub host: String,
pub mix_port: u16,
pub clients_port: u16,
#[wasm_bindgen(getter_with_clone)]
pub identity_key: String,
#[wasm_bindgen(getter_with_clone)]
pub sphinx_key: String,
#[wasm_bindgen(getter_with_clone)]
pub version: String,
}
#[wasm_bindgen]
impl WasmGateway {
#[wasm_bindgen(constructor)]
pub fn new(
owner: String,
host: String,
mix_port: u16,
clients_port: u16,
identity_key: String,
sphinx_key: String,
version: String,
) -> Self {
Self {
owner,
host,
mix_port,
clients_port,
identity_key,
sphinx_key,
version,
}
}
}
impl TryFrom<WasmGateway> for gateway::Node {
type Error = WasmTopologyError;
fn try_from(value: WasmGateway) -> Result<Self, Self::Error> {
let host = gateway::Node::parse_host(&value.host)?;
// try to completely resolve the host in the mix situation to avoid doing it every
// single time we want to construct a path
let mix_host = gateway::Node::extract_mix_host(&host, value.mix_port)?;
Ok(gateway::Node {
owner: value.owner,
host,
mix_host,
clients_port: value.clients_port,
identity_key: identity::PublicKey::from_base58_string(&value.identity_key)
.map_err(GatewayConversionError::from)?,
sphinx_key: encryption::PublicKey::from_base58_string(&value.sphinx_key)
.map_err(GatewayConversionError::from)?,
version: value.version,
})
}
}
@@ -22,7 +22,7 @@ use crate::client::topology_control::{
}; };
use crate::config::{Config, DebugConfig, GatewayEndpointConfig}; use crate::config::{Config, DebugConfig, GatewayEndpointConfig};
use crate::error::ClientCoreError; use crate::error::ClientCoreError;
use crate::spawn_future; use crate::{config, spawn_future};
use futures::channel::mpsc; use futures::channel::mpsc;
use log::{debug, info}; use log::{debug, info};
use nym_bandwidth_controller::BandwidthController; use nym_bandwidth_controller::BandwidthController;
@@ -39,7 +39,6 @@ use nym_task::connections::{ConnectionCommandReceiver, ConnectionCommandSender,
use nym_task::{TaskClient, TaskManager}; use nym_task::{TaskClient, TaskManager};
use nym_topology::provider_trait::TopologyProvider; use nym_topology::provider_trait::TopologyProvider;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use tap::TapFallible; use tap::TapFallible;
use url::Url; use url::Url;
@@ -371,11 +370,12 @@ where
// the current global view of topology // the current global view of topology
async fn start_topology_refresher( async fn start_topology_refresher(
topology_provider: Box<dyn TopologyProvider>, topology_provider: Box<dyn TopologyProvider>,
refresh_rate: Duration, topology_config: config::Topology,
topology_accessor: TopologyAccessor, topology_accessor: TopologyAccessor,
shutdown: TaskClient, mut shutdown: TaskClient,
) -> Result<(), ClientCoreError> { ) -> Result<(), ClientCoreError> {
let topology_refresher_config = TopologyRefresherConfig::new(refresh_rate); let topology_refresher_config =
TopologyRefresherConfig::new(topology_config.topology_refresh_rate);
let mut topology_refresher = TopologyRefresher::new( let mut topology_refresher = TopologyRefresher::new(
topology_refresher_config, topology_refresher_config,
@@ -395,8 +395,17 @@ where
return Err(ClientCoreError::InsufficientNetworkTopology(err)); return Err(ClientCoreError::InsufficientNetworkTopology(err));
} }
info!("Starting topology refresher..."); if topology_config.disable_refreshing {
topology_refresher.start_with_shutdown(shutdown); // if we're not spawning the refresher, don't cause shutdown immediately
info!("The topology refesher is not going to be started");
shutdown.mark_as_success();
} else {
// don't spawn the refresher if we don't want to be refreshing the topology.
// only use the initial values obtained
info!("Starting topology refresher...");
topology_refresher.start_with_shutdown(shutdown);
}
Ok(()) Ok(())
} }
@@ -500,7 +509,7 @@ where
); );
Self::start_topology_refresher( Self::start_topology_refresher(
topology_provider, topology_provider,
self.debug_config.topology.topology_refresh_rate, self.debug_config.topology,
shared_topology_accessor.clone(), shared_topology_accessor.clone(),
task_manager.subscribe(), task_manager.subscribe(),
) )
@@ -1,5 +1,9 @@
// Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::forwarding::packet::MixPacket;
use nym_task::connections::TransmissionLane; use nym_task::connections::TransmissionLane;
pub type InputMessageSender = tokio::sync::mpsc::Sender<InputMessage>; pub type InputMessageSender = tokio::sync::mpsc::Sender<InputMessage>;
@@ -7,6 +11,14 @@ pub type InputMessageReceiver = tokio::sync::mpsc::Receiver<InputMessage>;
#[derive(Debug)] #[derive(Debug)]
pub enum InputMessage { pub enum InputMessage {
/// Fire an already prepared mix packets into the network.
/// No guarantees are made about it. For example no retransmssion
/// will be attempted if it gets dropped.
Premade {
msgs: Vec<MixPacket>,
lane: TransmissionLane,
},
/// The simplest message variant where no additional information is attached. /// The simplest message variant where no additional information is attached.
/// You're simply sending your `data` to specified `recipient` without any tagging. /// You're simply sending your `data` to specified `recipient` without any tagging.
/// ///
@@ -44,6 +56,10 @@ pub enum InputMessage {
} }
impl InputMessage { impl InputMessage {
pub fn new_premade(msgs: Vec<MixPacket>, lane: TransmissionLane) -> Self {
InputMessage::Premade { msgs, lane }
}
pub fn new_regular(recipient: Recipient, data: Vec<u8>, lane: TransmissionLane) -> Self { pub fn new_regular(recipient: Recipient, data: Vec<u8>, lane: TransmissionLane) -> Self {
InputMessage::Regular { InputMessage::Regular {
recipient, recipient,
@@ -82,7 +98,8 @@ impl InputMessage {
match self { match self {
InputMessage::Regular { lane, .. } InputMessage::Regular { lane, .. }
| InputMessage::Anonymous { lane, .. } | InputMessage::Anonymous { lane, .. }
| InputMessage::Reply { lane, .. } => lane, | InputMessage::Reply { lane, .. }
| InputMessage::Premade { lane, .. } => lane,
} }
} }
} }
@@ -3,10 +3,12 @@
use crate::client::inbound_messages::{InputMessage, InputMessageReceiver}; use crate::client::inbound_messages::{InputMessage, InputMessageReceiver};
use crate::client::real_messages_control::message_handler::MessageHandler; use crate::client::real_messages_control::message_handler::MessageHandler;
use crate::client::real_messages_control::real_traffic_stream::RealMessage;
use crate::client::replies::reply_controller::ReplyControllerSender; use crate::client::replies::reply_controller::ReplyControllerSender;
use log::*; use log::*;
use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::forwarding::packet::MixPacket;
use nym_task::connections::TransmissionLane; use nym_task::connections::TransmissionLane;
use rand::{CryptoRng, Rng}; use rand::{CryptoRng, Rng};
@@ -41,6 +43,18 @@ where
} }
} }
async fn handle_premade_packets(&mut self, packets: Vec<MixPacket>, lane: TransmissionLane) {
self.message_handler
.send_premade_mix_packets(
packets
.into_iter()
.map(|p| RealMessage::new(p, None))
.collect(),
lane,
)
.await
}
async fn handle_reply( async fn handle_reply(
&mut self, &mut self,
recipient_tag: AnonymousSenderTag, recipient_tag: AnonymousSenderTag,
@@ -106,6 +120,7 @@ where
} => { } => {
self.handle_reply(recipient_tag, data, lane).await; self.handle_reply(recipient_tag, data, lane).await;
} }
InputMessage::Premade { msgs, lane } => self.handle_premade_packets(msgs, lane).await,
}; };
} }
@@ -131,7 +131,10 @@ where
// send to `OutQueueControl` to eventually send to the mix network // send to `OutQueueControl` to eventually send to the mix network
self.message_handler self.message_handler
.forward_messages( .forward_messages(
vec![RealMessage::new(prepared_fragment.mix_packet, frag_id)], vec![RealMessage::new(
prepared_fragment.mix_packet,
Some(frag_id),
)],
TransmissionLane::Retransmission, TransmissionLane::Retransmission,
) )
.await .await
@@ -291,8 +291,10 @@ where
.try_prepare_single_reply_chunk_for_sending(reply_surb, chunk_clone) .try_prepare_single_reply_chunk_for_sending(reply_surb, chunk_clone)
.await?; .await?;
let real_messages = let real_messages = RealMessage::new(
RealMessage::new(prepared_fragment.mix_packet, chunk.fragment_identifier()); prepared_fragment.mix_packet,
Some(chunk.fragment_identifier()),
);
let delay = prepared_fragment.total_delay; let delay = prepared_fragment.total_delay;
let pending_ack = let pending_ack =
PendingAcknowledgement::new_anonymous(chunk, delay, target, is_extra_surb_request); PendingAcknowledgement::new_anonymous(chunk, delay, target, is_extra_surb_request);
@@ -384,7 +386,8 @@ where
let lane = raw.0; let lane = raw.0;
let fragment = raw.1; let fragment = raw.1;
let real_message = RealMessage::new(prepared.mix_packet, prepared.fragment_identifier); let real_message =
RealMessage::new(prepared.mix_packet, Some(prepared.fragment_identifier));
let delay = prepared.total_delay; let delay = prepared.total_delay;
let pending_ack = PendingAcknowledgement::new_anonymous(fragment, delay, target, false); let pending_ack = PendingAcknowledgement::new_anonymous(fragment, delay, target, false);
@@ -401,6 +404,14 @@ where
Ok(()) Ok(())
} }
pub(crate) async fn send_premade_mix_packets(
&mut self,
msgs: Vec<RealMessage>,
lane: TransmissionLane,
) {
self.forward_messages(msgs, lane).await;
}
pub(crate) async fn try_send_plain_message( pub(crate) async fn try_send_plain_message(
&mut self, &mut self,
recipient: Recipient, recipient: Recipient,
@@ -444,8 +455,10 @@ where
&recipient, &recipient,
)?; )?;
let real_message = let real_message = RealMessage::new(
RealMessage::new(prepared_fragment.mix_packet, fragment.fragment_identifier()); prepared_fragment.mix_packet,
Some(fragment.fragment_identifier()),
);
let delay = prepared_fragment.total_delay; let delay = prepared_fragment.total_delay;
let pending_ack = PendingAcknowledgement::new_known(fragment, delay, recipient); let pending_ack = PendingAcknowledgement::new_known(fragment, delay, recipient);
@@ -121,7 +121,7 @@ where
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct RealMessage { pub(crate) struct RealMessage {
mix_packet: MixPacket, mix_packet: MixPacket,
fragment_id: FragmentIdentifier, fragment_id: Option<FragmentIdentifier>,
// TODO: add info about it being constructed with reply-surb // TODO: add info about it being constructed with reply-surb
} }
@@ -129,7 +129,7 @@ impl From<PreparedFragment> for RealMessage {
fn from(fragment: PreparedFragment) -> Self { fn from(fragment: PreparedFragment) -> Self {
RealMessage { RealMessage {
mix_packet: fragment.mix_packet, mix_packet: fragment.mix_packet,
fragment_id: fragment.fragment_identifier, fragment_id: Some(fragment.fragment_identifier),
} }
} }
} }
@@ -139,7 +139,7 @@ impl RealMessage {
self.mix_packet.sphinx_packet().len() self.mix_packet.sphinx_packet().len()
} }
pub(crate) fn new(mix_packet: MixPacket, fragment_id: FragmentIdentifier) -> Self { pub(crate) fn new(mix_packet: MixPacket, fragment_id: Option<FragmentIdentifier>) -> Self {
RealMessage { RealMessage {
mix_packet, mix_packet,
fragment_id, fragment_id,
@@ -255,7 +255,7 @@ where
) )
} }
StreamMessage::Real(real_message) => { StreamMessage::Real(real_message) => {
(real_message.mix_packet, Some(real_message.fragment_id)) (real_message.mix_packet, real_message.fragment_id)
} }
}; };
+6
View File
@@ -743,6 +743,11 @@ pub struct Topology {
/// did not reach its destination. /// did not reach its destination.
#[serde(with = "humantime_serde")] #[serde(with = "humantime_serde")]
pub topology_resolution_timeout: Duration, pub topology_resolution_timeout: Duration,
/// Specifies whether the client should not refresh the network topology after obtaining
/// the first valid instance.
/// Supersedes `topology_refresh_rate_ms`.
pub disable_refreshing: bool,
} }
impl Default for Topology { impl Default for Topology {
@@ -750,6 +755,7 @@ impl Default for Topology {
Topology { Topology {
topology_refresh_rate: DEFAULT_TOPOLOGY_REFRESH_RATE, topology_refresh_rate: DEFAULT_TOPOLOGY_REFRESH_RATE,
topology_resolution_timeout: DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT, topology_resolution_timeout: DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT,
disable_refreshing: false,
} }
} }
} }
@@ -142,6 +142,7 @@ impl From<OldDebugConfigV1_1_13> for DebugConfig {
topology: Topology { topology: Topology {
topology_refresh_rate: value.topology_refresh_rate, topology_refresh_rate: value.topology_refresh_rate,
topology_resolution_timeout: value.topology_resolution_timeout, topology_resolution_timeout: value.topology_resolution_timeout,
disable_refreshing: false,
}, },
reply_surbs: ReplySurbs { reply_surbs: ReplySurbs {
minimum_reply_surb_storage_threshold: value.minimum_reply_surb_storage_threshold, minimum_reply_surb_storage_threshold: value.minimum_reply_surb_storage_threshold,
@@ -17,7 +17,9 @@ nym-mixnet-contract-common = { path = "../../cosmwasm-smart-contracts/mixnet-con
nym-vesting-contract-common = { path = "../../cosmwasm-smart-contracts/vesting-contract" } nym-vesting-contract-common = { path = "../../cosmwasm-smart-contracts/vesting-contract" }
nym-coconut-bandwidth-contract-common = { path = "../../cosmwasm-smart-contracts/coconut-bandwidth-contract" } nym-coconut-bandwidth-contract-common = { path = "../../cosmwasm-smart-contracts/coconut-bandwidth-contract" }
nym-multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" } nym-multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" }
nym-name-service-common = { path = "../../cosmwasm-smart-contracts/name-service" }
nym-group-contract-common = { path = "../../cosmwasm-smart-contracts/group-contract" } nym-group-contract-common = { path = "../../cosmwasm-smart-contracts/group-contract" }
nym-service-provider-directory-common = { path = "../../cosmwasm-smart-contracts/service-provider-directory" }
nym-vesting-contract = { path = "../../../contracts/vesting" } nym-vesting-contract = { path = "../../../contracts/vesting" }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
@@ -63,6 +65,14 @@ name = "offline_signing"
# (traits would need to be moved around and refactored themselves) # (traits would need to be moved around and refactored themselves)
required-features = ["nyxd-client"] required-features = ["nyxd-client"]
[[example]]
name = "query_service_provider_directory"
required-features = ["nyxd-client"]
[[example]]
name = "query_name_service"
required-features = ["nyxd-client"]
[features] [features]
nyxd-client = [ nyxd-client = [
"async-trait", "async-trait",
@@ -0,0 +1,35 @@
use std::str::FromStr;
use cosmrs::AccountId;
use nym_name_service_common::Address;
use nym_network_defaults::{setup_env, NymNetworkDetails};
use nym_validator_client::nyxd::traits::NameServiceQueryClient;
#[tokio::main]
async fn main() {
setup_env(Some(&"../../../envs/qa-qwerty.env".parse().unwrap()));
let network_details = NymNetworkDetails::new_from_env();
let config =
nym_validator_client::Config::try_from_nym_network_details(&network_details).unwrap();
let client = nym_validator_client::Client::new_query(config).unwrap();
let config = client.nyxd.get_name_service_config().await.unwrap();
println!("config: {config:?}");
let names_paged = client.nyxd.get_names_paged(None, None).await.unwrap();
println!("names (paged): {names_paged:#?}");
let names = client.nyxd.get_all_names().await.unwrap();
println!("names: {names:#?}");
let owner = AccountId::from_str("n1hmf957kc7arcd39rl7xq8l0a4zyg7kxnv7su87").unwrap();
let names_by_owner = client.nyxd.get_names_by_owner(owner).await.unwrap();
println!("names (by owner): {names_by_owner:#?}");
let nym_address = Address::new("client_id.client_key@gateway_id");
let names_by_address = client.nyxd.get_names_by_address(nym_address).await.unwrap();
println!("names (by address): {names_by_address:#?}");
let service_info = client.nyxd.get_name_entry(1).await;
println!("service info: {service_info:#?}");
}
@@ -0,0 +1,43 @@
use std::str::FromStr;
use cosmrs::AccountId;
use nym_network_defaults::{setup_env, NymNetworkDetails};
use nym_service_provider_directory_common::NymAddress;
use nym_validator_client::nyxd::traits::SpDirectoryQueryClient;
#[tokio::main]
async fn main() {
setup_env(Some(&"../../../envs/qa-qwerty.env".parse().unwrap()));
let network_details = NymNetworkDetails::new_from_env();
let config =
nym_validator_client::Config::try_from_nym_network_details(&network_details).unwrap();
let client = nym_validator_client::Client::new_query(config).unwrap();
let config = client.nyxd.get_service_config().await.unwrap();
println!("config: {config:?}");
let services_paged = client.nyxd.get_services_paged(None, None).await.unwrap();
println!("services (paged): {services_paged:#?}");
let services = client.nyxd.get_all_services().await.unwrap();
println!("services: {services:#?}");
let announcer = AccountId::from_str("n1hmf957kc7arcd39rl7xq8l0a4zyg7kxnv7su87").unwrap();
let services_by_announcer = client
.nyxd
.get_services_by_announcer(announcer)
.await
.unwrap();
println!("services (by announcer): {services_by_announcer:#?}");
let nym_address = NymAddress::new("foo.bar@gateway");
let services_by_nym_address = client
.nyxd
.get_services_by_nym_address(nym_address)
.await
.unwrap();
assert_eq!(services_by_announcer, services_by_nym_address);
let service_info = client.nyxd.get_service_info(1).await;
println!("service info: {service_info:#?}");
}
@@ -1,6 +1,6 @@
use crate::nyxd::error::NyxdError; use crate::nyxd::error::NyxdError;
use crate::nyxd::{Config as ClientConfig, NyxdClient, QueryNyxdClient}; use crate::nyxd::{Config as ClientConfig, NyxdClient, QueryNyxdClient};
use crate::NymApiClient; use crate::{NymApiClient, ValidatorClientError};
use crate::nyxd::traits::MixnetQueryClient; use crate::nyxd::traits::MixnetQueryClient;
use colored::Colorize; use colored::Colorize;
@@ -45,6 +45,23 @@ pub async fn run_validator_connection_test<H: BuildHasher + 'static>(
) )
} }
pub async fn test_nyxd_url_connection(
network: NymNetworkDetails,
nyxd_url: Url,
address: cosmrs::AccountId,
) -> Result<bool, ValidatorClientError> {
let config = ClientConfig::try_from_nym_network_details(&network)
.expect("failed to create valid nyxd client config");
let mut nyxd_client = NyxdClient::<QueryNyxdClient>::connect(config, nyxd_url.as_str())?;
// possibly redundant, but lets just leave it here
nyxd_client.set_mixnet_contract_address(address);
match test_nyxd_connection(network, &nyxd_url, &nyxd_client).await {
ConnectionResult::Nyxd(_, _, res) => Ok(res),
_ => Ok(false), // ✶ not possible to happens
}
}
fn setup_connection_tests<H: BuildHasher + 'static>( fn setup_connection_tests<H: BuildHasher + 'static>(
nyxd_urls: impl Iterator<Item = (NymNetworkDetails, Url)>, nyxd_urls: impl Iterator<Item = (NymNetworkDetails, Url)>,
api_urls: impl Iterator<Item = (NymNetworkDetails, Url)>, api_urls: impl Iterator<Item = (NymNetworkDetails, Url)>,
@@ -105,7 +122,7 @@ async fn test_nyxd_connection(
{ {
Ok(Err(NyxdError::TendermintError(e))) => { Ok(Err(NyxdError::TendermintError(e))) => {
// If we get a tendermint-rpc error, we classify the node as not contactable // If we get a tendermint-rpc error, we classify the node as not contactable
log::debug!("Checking: nyxd url: {url}: {}: {}", "failed".red(), e); log::warn!("Checking: nyxd url: {url}: {}: {}", "failed".red(), e);
false false
} }
Ok(Err(NyxdError::AbciError { code, log, .. })) => { Ok(Err(NyxdError::AbciError { code, log, .. })) => {
@@ -117,13 +134,13 @@ async fn test_nyxd_connection(
); );
code == 18 code == 18
} }
Ok(Err(error @ NyxdError::NoContractAddressAvailable)) => { Ok(Err(error @ NyxdError::NoContractAddressAvailable(_))) => {
log::debug!("Checking: nyxd url: {url}: {}: {error}", "failed".red()); log::warn!("Checking: nyxd url: {url}: {}: {error}", "failed".red());
false false
} }
Ok(Err(e)) => { Ok(Err(e)) => {
// For any other error, we're optimistic and just try anyway. // For any other error, we're optimistic and just try anyway.
log::debug!( log::warn!(
"Checking: nyxd_url: {url}: {}, but with error: {e}", "Checking: nyxd_url: {url}: {}, but with error: {e}",
"success".green() "success".green()
); );
@@ -134,7 +151,7 @@ async fn test_nyxd_connection(
true true
} }
Err(e) => { Err(e) => {
log::debug!("Checking: nyxd_url: {url}: {}: {e}", "failed".red()); log::warn!("Checking: nyxd_url: {url}: {}: {e}", "failed".red());
false false
} }
}; };
@@ -13,7 +13,8 @@ pub mod nyxd;
pub mod signing; pub mod signing;
pub use crate::error::ValidatorClientError; pub use crate::error::ValidatorClientError;
pub use client::NymApiClient;
pub use nym_api_requests::*; pub use nym_api_requests::*;
#[cfg(feature = "nyxd-client")] #[cfg(feature = "nyxd-client")]
pub use client::{Client, CoconutApiClient, Config, NymApiClient}; pub use client::{Client, CoconutApiClient, Config};
@@ -9,6 +9,9 @@ pub enum NymAPIError {
source: reqwest::Error, source: reqwest::Error,
}, },
#[error("Not found")]
NotFound,
#[error("Request failed with error message - {0}")] #[error("Request failed with error message - {0}")]
GenericRequestFailure(String), GenericRequestFailure(String),
@@ -15,7 +15,8 @@ use nym_api_requests::models::{
}; };
use nym_mixnet_contract_common::mixnode::MixNodeDetails; use nym_mixnet_contract_common::mixnode::MixNodeDetails;
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId}; use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId};
use reqwest::Response; use nym_service_provider_directory_common::ServiceInfo;
use reqwest::{Response, StatusCode};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
@@ -76,6 +77,8 @@ impl Client {
let res = self.send_get_request(path, params).await?; let res = self.send_get_request(path, params).await?;
if res.status().is_success() { if res.status().is_success() {
Ok(res.json().await?) Ok(res.json().await?)
} else if res.status() == StatusCode::NOT_FOUND {
Err(NymAPIError::NotFound)
} else { } else {
Err(NymAPIError::GenericRequestFailure(res.text().await?)) Err(NymAPIError::GenericRequestFailure(res.text().await?))
} }
@@ -480,6 +483,11 @@ impl Client {
) )
.await .await
} }
pub async fn get_service_providers(&self) -> Result<Vec<ServiceInfo>, NymAPIError> {
self.query_nym_api(&[routes::API_VERSION, routes::SERVICE_PROVIDERS], NO_PARAMS)
.await
}
} }
// utility function that should solve the double slash problem in validator API forever. // utility function that should solve the double slash problem in validator API forever.
@@ -32,3 +32,5 @@ pub const COMPUTE_REWARD_ESTIMATION: &str = "compute-reward-estimation";
pub const AVG_UPTIME: &str = "avg_uptime"; pub const AVG_UPTIME: &str = "avg_uptime";
pub const STAKE_SATURATION: &str = "stake-saturation"; pub const STAKE_SATURATION: &str = "stake-saturation";
pub const INCLUSION_CHANCE: &str = "inclusion-probability"; pub const INCLUSION_CHANCE: &str = "inclusion-probability";
pub const SERVICE_PROVIDERS: &str = "service-providers";
@@ -21,8 +21,8 @@ use std::{io, time::Duration};
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum NyxdError { pub enum NyxdError {
#[error("No contract address is available to perform the call")] #[error("No contract address is available to perform the call: {0}")]
NoContractAddressAvailable, NoContractAddressAvailable(String),
#[error(transparent)] #[error(transparent)]
WalletError(#[from] DirectSecp256k1HdWalletError), WalletError(#[from] DirectSecp256k1HdWalletError),
@@ -162,7 +162,7 @@ fn try_parse_abci_log(log: &abci::Log) -> Option<String> {
.value() .value()
.contains("Maximum amount of locked coins has already been pledged") .contains("Maximum amount of locked coins has already been pledged")
{ {
Some("Maximum amount of locked tokens has alredy been used. You can only use up to 10% of your locked tokens for bonding and delegating.".to_string()) Some("Maximum amount of locked tokens has already been used. You can only use up to 10% of your locked tokens for bonding and delegating.".to_string())
} else { } else {
None None
} }
@@ -67,6 +67,8 @@ pub struct Config {
pub(crate) group_contract_address: Option<AccountId>, pub(crate) group_contract_address: Option<AccountId>,
pub(crate) multisig_contract_address: Option<AccountId>, pub(crate) multisig_contract_address: Option<AccountId>,
pub(crate) coconut_dkg_contract_address: Option<AccountId>, pub(crate) coconut_dkg_contract_address: Option<AccountId>,
pub(crate) service_provider_contract_address: Option<AccountId>,
pub(crate) name_service_contract_address: Option<AccountId>,
// TODO: add this in later commits // TODO: add this in later commits
// pub(crate) gas_price: GasPrice, // pub(crate) gas_price: GasPrice,
} }
@@ -131,6 +133,17 @@ impl Config {
details.contracts.coconut_dkg_contract_address.as_ref(), details.contracts.coconut_dkg_contract_address.as_ref(),
prefix, prefix,
)?, )?,
service_provider_contract_address: Self::parse_optional_account(
details
.contracts
.service_provider_directory_contract_address
.as_ref(),
prefix,
)?,
name_service_contract_address: Self::parse_optional_account(
details.contracts.name_service_contract_address.as_ref(),
prefix,
)?,
}) })
} }
} }
@@ -246,6 +259,10 @@ impl<C> NyxdClient<C> {
self.config.multisig_contract_address = Some(address); self.config.multisig_contract_address = Some(address);
} }
pub fn set_service_provider_contract_address(&mut self, address: AccountId) {
self.config.service_provider_contract_address = Some(address);
}
// TODO: this should get changed into Result<&AccountId, NyxdError> (or Option<&AccountId> in future commits // TODO: this should get changed into Result<&AccountId, NyxdError> (or Option<&AccountId> in future commits
// note: what unwrap is doing here is just moving a failure that would have normally // note: what unwrap is doing here is just moving a failure that would have normally
// occurred in `connect` when attempting to parse an empty address, // occurred in `connect` when attempting to parse an empty address,
@@ -304,6 +321,16 @@ impl<C> NyxdClient<C> {
self.config.coconut_dkg_contract_address.as_ref().unwrap() self.config.coconut_dkg_contract_address.as_ref().unwrap()
} }
// The service provider directory contract is optional, so we return an Option not a Result
pub fn service_provider_contract_address(&self) -> Option<&AccountId> {
self.config.service_provider_contract_address.as_ref()
}
// The name service contract is optional, so we return an Option not a Result
pub fn name_service_contract_address(&self) -> Option<&AccountId> {
self.config.name_service_contract_address.as_ref()
}
pub fn set_simulated_gas_multiplier(&mut self, multiplier: f32) { pub fn set_simulated_gas_multiplier(&mut self, multiplier: f32) {
self.simulated_gas_multiplier = multiplier; self.simulated_gas_multiplier = multiplier;
} }
@@ -16,15 +16,25 @@ mod mixnet_signing_client;
mod multisig_signing_client; mod multisig_signing_client;
mod vesting_signing_client; mod vesting_signing_client;
mod sp_directory_query_client;
mod sp_directory_signing_client;
mod name_service_query_client;
mod name_service_signing_client;
pub use coconut_bandwidth_query_client::CoconutBandwidthQueryClient; pub use coconut_bandwidth_query_client::CoconutBandwidthQueryClient;
pub use dkg_query_client::DkgQueryClient; pub use dkg_query_client::DkgQueryClient;
pub use group_query_client::GroupQueryClient; pub use group_query_client::GroupQueryClient;
pub use mixnet_query_client::MixnetQueryClient; pub use mixnet_query_client::MixnetQueryClient;
pub use multisig_query_client::MultisigQueryClient; pub use multisig_query_client::MultisigQueryClient;
pub use name_service_query_client::NameServiceQueryClient;
pub use sp_directory_query_client::SpDirectoryQueryClient;
pub use vesting_query_client::VestingQueryClient; pub use vesting_query_client::VestingQueryClient;
pub use coconut_bandwidth_signing_client::CoconutBandwidthSigningClient; pub use coconut_bandwidth_signing_client::CoconutBandwidthSigningClient;
pub use dkg_signing_client::DkgSigningClient; pub use dkg_signing_client::DkgSigningClient;
pub use mixnet_signing_client::MixnetSigningClient; pub use mixnet_signing_client::MixnetSigningClient;
pub use multisig_signing_client::MultisigSigningClient; pub use multisig_signing_client::MultisigSigningClient;
pub use name_service_signing_client::NameServiceSigningClient;
pub use sp_directory_signing_client::SpDirectorySigningClient;
pub use vesting_signing_client::VestingSigningClient; pub use vesting_signing_client::VestingSigningClient;
@@ -0,0 +1,109 @@
use async_trait::async_trait;
use cosmrs::AccountId;
use nym_contracts_common::ContractBuildInformation;
use nym_name_service_common::{
msg::QueryMsg as NameQueryMsg,
response::{ConfigResponse, NamesListResponse, PagedNamesListResponse},
Address, NameEntry, NameId,
};
use serde::Deserialize;
use crate::nyxd::{error::NyxdError, CosmWasmClient, NyxdClient};
#[async_trait]
pub trait NameServiceQueryClient {
async fn query_name_service_contract<T>(&self, query: NameQueryMsg) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>;
async fn get_name_service_config(&self) -> Result<ConfigResponse, NyxdError> {
self.query_name_service_contract(NameQueryMsg::Config {})
.await
}
async fn get_name_entry(&self, name_id: NameId) -> Result<NameEntry, NyxdError> {
self.query_name_service_contract(NameQueryMsg::NameId { name_id })
.await
}
async fn get_names_paged(
&self,
start_after: Option<NameId>,
limit: Option<u32>,
) -> Result<PagedNamesListResponse, NyxdError> {
self.query_name_service_contract(NameQueryMsg::All { limit, start_after })
.await
}
async fn get_names_by_owner(&self, owner: AccountId) -> Result<NamesListResponse, NyxdError> {
self.query_name_service_contract(NameQueryMsg::ByOwner {
owner: owner.to_string(),
})
.await
}
async fn get_names_by_address(&self, address: Address) -> Result<NamesListResponse, NyxdError> {
self.query_name_service_contract(NameQueryMsg::ByAddress { address })
.await
}
async fn get_name_service_contract_version(
&self,
) -> Result<ContractBuildInformation, NyxdError> {
self.query_name_service_contract(NameQueryMsg::GetContractVersion {})
.await
}
async fn get_all_names(&self) -> Result<Vec<NameEntry>, NyxdError> {
let mut services = Vec::new();
let mut start_after = None;
loop {
let mut paged_response = self.get_names_paged(start_after.take(), None).await?;
let last_id = paged_response.names.last().map(|serv| serv.name_id);
services.append(&mut paged_response.names);
if let Some(start_after_res) = last_id {
start_after = Some(start_after_res)
} else {
break;
}
}
Ok(services)
}
}
#[async_trait]
impl<C> NameServiceQueryClient for NyxdClient<C>
where
C: CosmWasmClient + Send + Sync,
{
async fn query_name_service_contract<T>(&self, query: NameQueryMsg) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>,
{
self.client
.query_contract_smart(
self.name_service_contract_address().ok_or(
NyxdError::NoContractAddressAvailable("name service contract".to_string()),
)?,
&query,
)
.await
}
}
#[async_trait]
impl<C> NameServiceQueryClient for crate::Client<C>
where
C: CosmWasmClient + Send + Sync,
{
async fn query_name_service_contract<T>(&self, query: NameQueryMsg) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>,
{
self.nyxd.query_name_service_contract(query).await
}
}
@@ -0,0 +1,96 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use async_trait::async_trait;
use nym_name_service_common::{msg::ExecuteMsg as NameExecuteMsg, Address, NameId, NymName};
use crate::nyxd::{
coin::Coin, cosmwasm_client::types::ExecuteResult, error::NyxdError, Fee, NyxdClient,
SigningCosmWasmClient,
};
#[async_trait]
pub trait NameServiceSigningClient {
async fn execute_name_service_contract(
&self,
fee: Option<Fee>,
msg: NameExecuteMsg,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError>;
async fn register_name(
&self,
name: NymName,
address: Address,
deposit: Coin,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_name_service_contract(
fee,
NameExecuteMsg::Register { name, address },
vec![deposit],
)
.await
}
async fn delete_name_by_id(
&self,
name_id: NameId,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_name_service_contract(fee, NameExecuteMsg::DeleteId { name_id }, vec![])
.await
}
async fn delete_service_provider_by_name(
&self,
name: NymName,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_name_service_contract(fee, NameExecuteMsg::DeleteName { name }, vec![])
.await
}
async fn update_deposit_required(
&self,
deposit_required: Coin,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_name_service_contract(
fee,
NameExecuteMsg::UpdateDepositRequired {
deposit_required: deposit_required.into(),
},
vec![],
)
.await
}
}
#[async_trait]
impl<C> NameServiceSigningClient for NyxdClient<C>
where
C: SigningCosmWasmClient + Sync + Send,
{
async fn execute_name_service_contract(
&self,
fee: Option<Fee>,
msg: NameExecuteMsg,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError> {
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier)));
let memo = msg.default_memo();
self.client
.execute(
self.address(),
self.name_service_contract_address().ok_or(
NyxdError::NoContractAddressAvailable("name service contract".to_string()),
)?,
&msg,
fee,
memo,
funds,
)
.await
}
}
@@ -0,0 +1,120 @@
use async_trait::async_trait;
use cosmrs::AccountId;
use nym_contracts_common::ContractBuildInformation;
use nym_service_provider_directory_common::{
msg::QueryMsg as SpQueryMsg,
response::{
ConfigResponse, PagedServicesListResponse, ServiceInfoResponse, ServicesListResponse,
},
NymAddress, ServiceId, ServiceInfo,
};
use serde::Deserialize;
use crate::nyxd::{error::NyxdError, CosmWasmClient, NyxdClient};
#[async_trait]
pub trait SpDirectoryQueryClient {
async fn query_service_provider_contract<T>(&self, query: SpQueryMsg) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>;
async fn get_service_config(&self) -> Result<ConfigResponse, NyxdError> {
self.query_service_provider_contract(SpQueryMsg::Config {})
.await
}
async fn get_service_info(
&self,
service_id: ServiceId,
) -> Result<ServiceInfoResponse, NyxdError> {
self.query_service_provider_contract(SpQueryMsg::ServiceId { service_id })
.await
}
async fn get_services_paged(
&self,
start_after: Option<ServiceId>,
limit: Option<u32>,
) -> Result<PagedServicesListResponse, NyxdError> {
self.query_service_provider_contract(SpQueryMsg::All { limit, start_after })
.await
}
async fn get_services_by_announcer(
&self,
announcer: AccountId,
) -> Result<ServicesListResponse, NyxdError> {
self.query_service_provider_contract(SpQueryMsg::ByAnnouncer {
announcer: announcer.to_string(),
})
.await
}
async fn get_services_by_nym_address(
&self,
nym_address: NymAddress,
) -> Result<ServicesListResponse, NyxdError> {
self.query_service_provider_contract(SpQueryMsg::ByNymAddress { nym_address })
.await
}
async fn get_sp_contract_version(&self) -> Result<ContractBuildInformation, NyxdError> {
self.query_service_provider_contract(SpQueryMsg::GetContractVersion {})
.await
}
async fn get_all_services(&self) -> Result<Vec<ServiceInfo>, NyxdError> {
let mut services = Vec::new();
let mut start_after = None;
loop {
let mut paged_response = self.get_services_paged(start_after.take(), None).await?;
let last_id = paged_response.services.last().map(|serv| serv.service_id);
services.append(&mut paged_response.services);
if let Some(start_after_res) = last_id {
start_after = Some(start_after_res)
} else {
break;
}
}
Ok(services)
}
}
#[async_trait]
impl<C> SpDirectoryQueryClient for NyxdClient<C>
where
C: CosmWasmClient + Send + Sync,
{
async fn query_service_provider_contract<T>(&self, query: SpQueryMsg) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>,
{
self.client
.query_contract_smart(
self.service_provider_contract_address().ok_or(
NyxdError::NoContractAddressAvailable(
"service provider directory contract".to_string(),
),
)?,
&query,
)
.await
}
}
#[async_trait]
impl<C> SpDirectoryQueryClient for crate::Client<C>
where
C: CosmWasmClient + Send + Sync,
{
async fn query_service_provider_contract<T>(&self, query: SpQueryMsg) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>,
{
self.nyxd.query_service_provider_contract(query).await
}
}
@@ -0,0 +1,111 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use async_trait::async_trait;
use nym_service_provider_directory_common::{
msg::ExecuteMsg as SpExecuteMsg, NymAddress, ServiceId, ServiceType,
};
use crate::nyxd::{
coin::Coin, cosmwasm_client::types::ExecuteResult, error::NyxdError, Fee, NyxdClient,
SigningCosmWasmClient,
};
#[async_trait]
pub trait SpDirectorySigningClient {
async fn execute_service_provider_directory_contract(
&self,
fee: Option<Fee>,
msg: SpExecuteMsg,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError>;
async fn announce_service_provider(
&self,
nym_address: NymAddress,
service_type: ServiceType,
deposit: Coin,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_service_provider_directory_contract(
fee,
SpExecuteMsg::Announce {
nym_address,
service_type,
},
vec![deposit],
)
.await
}
async fn delete_service_provider_by_id(
&self,
service_id: ServiceId,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_service_provider_directory_contract(
fee,
SpExecuteMsg::DeleteId { service_id },
vec![],
)
.await
}
async fn delete_service_provider_by_nym_address(
&self,
nym_address: NymAddress,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_service_provider_directory_contract(
fee,
SpExecuteMsg::DeleteNymAddress { nym_address },
vec![],
)
.await
}
async fn update_deposit_required(
&self,
deposit_required: Coin,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_service_provider_directory_contract(
fee,
SpExecuteMsg::UpdateDepositRequired {
deposit_required: deposit_required.into(),
},
vec![],
)
.await
}
}
#[async_trait]
impl<C> SpDirectorySigningClient for NyxdClient<C>
where
C: SigningCosmWasmClient + Sync + Send,
{
async fn execute_service_provider_directory_contract(
&self,
fee: Option<Fee>,
msg: SpExecuteMsg,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError> {
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier)));
let memo = msg.default_memo();
self.client
.execute(
self.address(),
self.service_provider_contract_address().ok_or(
NyxdError::NoContractAddressAvailable(
"service provider directory contract".to_string(),
),
)?,
&msg,
fee,
memo,
funds,
)
.await
}
}
+1
View File
@@ -38,3 +38,4 @@ nym-vesting-contract-common = { path = "../cosmwasm-smart-contracts/vesting-cont
nym-coconut-bandwidth-contract-common = { path = "../cosmwasm-smart-contracts/coconut-bandwidth-contract" } nym-coconut-bandwidth-contract-common = { path = "../cosmwasm-smart-contracts/coconut-bandwidth-contract" }
nym-coconut-dkg-common = { path = "../cosmwasm-smart-contracts/coconut-dkg" } nym-coconut-dkg-common = { path = "../cosmwasm-smart-contracts/coconut-dkg" }
nym-multisig-contract-common = { path = "../cosmwasm-smart-contracts/multisig-contract" } nym-multisig-contract-common = { path = "../cosmwasm-smart-contracts/multisig-contract" }
nym-service-provider-directory-common = { path = "../cosmwasm-smart-contracts/service-provider-directory" }
@@ -5,6 +5,7 @@ use clap::{Args, Subcommand};
pub mod gateway; pub mod gateway;
pub mod mixnode; pub mod mixnode;
pub mod service;
#[derive(Debug, Args)] #[derive(Debug, Args)]
#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] #[clap(args_conflicts_with_subcommands = true, subcommand_required = true)]
@@ -19,4 +20,6 @@ pub enum MixnetOperatorsCommands {
Mixnode(mixnode::MixnetOperatorsMixnode), Mixnode(mixnode::MixnetOperatorsMixnode),
/// Manage your gateway /// Manage your gateway
Gateway(gateway::MixnetOperatorsGateway), Gateway(gateway::MixnetOperatorsGateway),
/// Manage your service
ServiceProvider(service::MixnetOperatorsService),
} }
@@ -0,0 +1,33 @@
use clap::Parser;
use log::info;
use nym_service_provider_directory_common::{Coin, NymAddress, ServiceType};
use nym_validator_client::nyxd::traits::SpDirectorySigningClient;
use crate::context::SigningClient;
#[derive(Debug, Parser)]
pub struct Args {
#[clap(long)]
pub nym_address: String,
/// Deposit to be made to the service provider directory, in curent DENOMINATION (e.g. 'unym')
#[clap(long)]
pub deposit: u128,
}
pub async fn announce(args: Args, client: SigningClient) {
info!("Annoucing service provider");
let nym_address = NymAddress::Address(args.nym_address);
let service_type = ServiceType::NetworkRequester;
let denom = client.current_chain_details().mix_denom.base.as_str();
let deposit = Coin::new(args.deposit, denom);
let res = client
.announce_service_provider(nym_address, service_type, deposit.into(), None)
.await
.expect("Failed to announce service provider");
info!("Announced service provider: {res:?}");
}
@@ -0,0 +1,23 @@
use clap::Parser;
use log::info;
use nym_service_provider_directory_common::ServiceId;
use nym_validator_client::nyxd::traits::SpDirectorySigningClient;
use crate::context::SigningClient;
#[derive(Debug, Parser)]
pub struct Args {
#[clap(long)]
pub id: ServiceId,
}
pub async fn delete(args: Args, client: SigningClient) {
info!("Deleting service provider with id {}", args.id);
let res = client
.delete_service_provider_by_id(args.id, None)
.await
.expect("Failed to delete service provider");
info!("Deleted: {res:?}");
}
@@ -0,0 +1,19 @@
use clap::{Args, Subcommand};
pub mod announce;
pub mod delete;
#[derive(Debug, Args)]
#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)]
pub struct MixnetOperatorsService {
#[clap(subcommand)]
pub command: MixnetOperatorsServiceCommands,
}
#[derive(Debug, Subcommand)]
pub enum MixnetOperatorsServiceCommands {
/// Announce service provider to the world
Announce(announce::Args),
/// Delete entry for service provider from the directory
Delete(delete::Args),
}
@@ -5,6 +5,7 @@ use clap::{Args, Subcommand};
pub mod query_all_gateways; pub mod query_all_gateways;
pub mod query_all_mixnodes; pub mod query_all_mixnodes;
pub mod query_all_service_providers;
#[derive(Debug, Args)] #[derive(Debug, Args)]
#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] #[clap(args_conflicts_with_subcommands = true, subcommand_required = true)]
@@ -19,4 +20,6 @@ pub enum MixnetQueryCommands {
Mixnodes(query_all_mixnodes::Args), Mixnodes(query_all_mixnodes::Args),
/// Query gateways /// Query gateways
Gateways(query_all_gateways::Args), Gateways(query_all_gateways::Args),
/// Query announced service-providers
ServiceProviders(query_all_service_providers::Args),
} }
@@ -0,0 +1,55 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::Parser;
use comfy_table::Table;
use nym_validator_client::nym_api::error::NymAPIError;
use crate::context::QueryClientWithNyxd;
use crate::utils::show_error;
#[derive(Debug, Parser)]
pub struct Args {
#[clap(value_parser)]
#[clap(help = "Optionally, the service provider to display")]
pub nym_address: Option<String>,
}
pub async fn query(args: Args, client: &QueryClientWithNyxd) {
match client.nym_api.get_service_providers().await {
Ok(res) => {
if let Some(nym_address) = args.nym_address {
let service = res.iter().find(|service| {
service
.service
.nym_address
.to_string()
.eq_ignore_ascii_case(&nym_address)
});
println!(
"{}",
::serde_json::to_string_pretty(&service).expect("json formatting error")
);
} else {
let mut table = Table::new();
table.set_header(vec!["Service Id", "Announcer", "Nym Address"]);
for service in res {
table.add_row(vec![
service.service_id.to_string(),
service.service.announcer.to_string(),
service.service.service_type.to_string(),
service.service.nym_address.to_string(),
]);
}
println!("The service providers in the directory are:");
println!("{table}");
}
}
Err(NymAPIError::NotFound) => {
println!("nym-api reports no service provider endpoint available");
}
Err(e) => show_error(e),
}
}
@@ -1,6 +1,6 @@
[package] [package]
name = "nym-mixnet-contract-common" name = "nym-mixnet-contract-common"
version = "0.4.0" version = "0.5.0"
description = "Common library for the Nym mixnet contract" description = "Common library for the Nym mixnet contract"
rust-version = "1.62" rust-version = "1.62"
edition = { workspace = true } edition = { workspace = true }
@@ -0,0 +1,11 @@
[package]
name = "nym-name-service-common"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cosmwasm-std = { workspace = true }
schemars = "0.8"
serde = { workspace = true, features = ["derive"] }
@@ -0,0 +1,66 @@
use cosmwasm_std::{Coin, Event};
use crate::{NameId, RegisteredName};
pub enum NameEventType {
Register,
DeleteId,
DeleteName,
UpdateDepositRequired,
}
impl std::fmt::Display for NameEventType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NameEventType::Register => write!(f, "register"),
NameEventType::DeleteId => write!(f, "delete_id"),
NameEventType::DeleteName => write!(f, "delete_name"),
NameEventType::UpdateDepositRequired => write!(f, "update_deposit_required"),
}
}
}
impl From<NameEventType> for String {
fn from(event_type: NameEventType) -> Self {
event_type.to_string()
}
}
pub const ACTION: &str = "action";
pub const NAME_ID: &str = "name_id";
pub const NAME: &str = "name";
pub const OWNER: &str = "owner";
pub const DEPOSIT_REQUIRED: &str = "deposit_required";
pub fn new_register_event(name_id: NameId, name: RegisteredName) -> Event {
Event::new(NameEventType::Register)
.add_attribute(ACTION, NameEventType::Register)
.add_attribute(NAME_ID, name_id.to_string())
.add_attribute(NAME, name.name.to_string())
.add_attribute(name.address.event_tag(), name.address.to_string())
.add_attribute(OWNER, name.owner.to_string())
}
pub fn new_delete_id_event(name_id: NameId, name: RegisteredName) -> Event {
Event::new(NameEventType::DeleteId)
.add_attribute(ACTION, NameEventType::DeleteId)
.add_attribute(NAME_ID, name_id.to_string())
.add_attribute(NAME, name.name.to_string())
.add_attribute(name.address.event_tag(), name.address.to_string())
}
pub fn new_delete_name_event(name_id: NameId, name: RegisteredName) -> Event {
Event::new(NameEventType::DeleteId)
.add_attribute(ACTION, NameEventType::DeleteName)
.add_attribute(NAME_ID, name_id.to_string())
.add_attribute(NAME, name.name.to_string())
.add_attribute(name.address.event_tag(), name.address.to_string())
}
pub fn new_update_deposit_required_event(deposit_required: Coin) -> Event {
Event::new(NameEventType::UpdateDepositRequired)
.add_attribute(ACTION, NameEventType::UpdateDepositRequired)
.add_attribute(DEPOSIT_REQUIRED, deposit_required.to_string())
}
@@ -0,0 +1,7 @@
pub mod events;
pub mod msg;
pub mod response;
pub mod types;
// Re-export all types at the top-level
pub use types::*;
@@ -0,0 +1,91 @@
use crate::{Address, NameId, NymName};
use cosmwasm_std::Coin;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub struct InstantiateMsg {
pub deposit_required: Coin,
}
impl InstantiateMsg {
pub fn new(deposit_required: Coin) -> Self {
Self { deposit_required }
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub struct MigrateMsg {}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
/// Announcing a name pointing to a nym-address
Register { name: NymName, address: Address },
/// Delete a name entry by id
DeleteId { name_id: NameId },
/// Delete a name entry by name
DeleteName { name: NymName },
/// Change the deposit required for announcing a name
UpdateDepositRequired { deposit_required: Coin },
}
impl ExecuteMsg {
pub fn delete_id(name_id: NameId) -> Self {
ExecuteMsg::DeleteId { name_id }
}
pub fn default_memo(&self) -> String {
match self {
ExecuteMsg::Register { name, address } => {
format!("registering {address} as name: {name}")
}
ExecuteMsg::DeleteId { name_id } => {
format!("deleting name with id {name_id}")
}
ExecuteMsg::DeleteName { name } => {
format!("deleting name: {name}")
}
ExecuteMsg::UpdateDepositRequired { deposit_required } => {
format!("updating the deposit required to {deposit_required}")
}
}
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
/// Query the name by it's assigned id
NameId {
name_id: NameId,
},
// Query the names by the registrator
ByOwner {
owner: String,
},
ByName {
name: NymName,
},
ByAddress {
address: Address,
},
All {
limit: Option<u32>,
start_after: Option<NameId>,
},
Config {},
GetContractVersion {},
#[serde(rename = "get_cw2_contract_version")]
GetCW2ContractVersion {},
}
impl QueryMsg {
pub fn all() -> QueryMsg {
QueryMsg::All {
limit: None,
start_after: None,
}
}
}
@@ -0,0 +1,79 @@
use crate::{msg::ExecuteMsg, NameEntry, NameId, RegisteredName};
use cosmwasm_std::Coin;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// Like [`NameEntry`] but since it's a response type the name is an option depending on if
/// the name exists or not.
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub struct NameEntryResponse {
pub name_id: NameId,
pub name: Option<RegisteredName>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct NamesListResponse {
pub names: Vec<NameEntry>,
}
impl NamesListResponse {
pub fn new(names: Vec<(NameId, RegisteredName)>) -> NamesListResponse {
NamesListResponse {
names: names
.into_iter()
.map(|(name_id, name)| NameEntry::new(name_id, name))
.collect(),
}
}
}
impl From<&[NameEntry]> for NamesListResponse {
fn from(names: &[NameEntry]) -> Self {
NamesListResponse {
names: names.to_vec(),
}
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub struct PagedNamesListResponse {
pub names: Vec<NameEntry>,
pub per_page: usize,
pub start_next_after: Option<NameId>,
}
impl PagedNamesListResponse {
pub fn new(
names: Vec<(NameId, RegisteredName)>,
per_page: usize,
start_next_after: Option<NameId>,
) -> PagedNamesListResponse {
let names = names
.into_iter()
.map(|(name_id, name)| NameEntry::new(name_id, name))
.collect();
PagedNamesListResponse {
names,
per_page,
start_next_after,
}
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub struct ConfigResponse {
pub deposit_required: Coin,
}
impl From<RegisteredName> for ExecuteMsg {
fn from(name: RegisteredName) -> Self {
ExecuteMsg::Register {
name: name.name,
address: name.address,
}
}
}
@@ -0,0 +1,165 @@
use std::fmt::{Display, Formatter};
use cosmwasm_std::{Addr, Coin};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// The directory of services are indexed by [`ServiceId`].
pub type NameId = u32;
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug, JsonSchema)]
pub struct RegisteredName {
/// The name pointing to the nym address
pub name: NymName,
/// The address of the service.
pub address: Address,
/// Service owner.
pub owner: Addr,
/// Block height at which the service was added.
pub block_height: u64,
/// The deposit used to announce the service.
pub deposit: Coin,
}
/// String representation of a nym address, which is of the form
/// client_id.client_enc@gateway_id.
/// NOTE: entirely unvalidated.
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Address {
NymAddress(String),
// Possible extension:
//Gateway(String)
}
impl Address {
/// Create a new nym address.
pub fn new(address: &str) -> Self {
Self::NymAddress(address.to_string())
}
pub fn as_str(&self) -> &str {
match self {
Address::NymAddress(address) => address,
}
}
pub fn event_tag(&self) -> &str {
match self {
Address::NymAddress(_) => "nym_address",
//Address::Gateway(_) => "gatway_address",
}
}
}
impl Display for Address {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
/// Name stored and pointing a to a nym-address
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct NymName(String);
#[derive(Debug)]
pub enum NymNameError {
InvalidName,
}
fn is_valid_name_char(c: char) -> bool {
// Normal lowercase letters
(c.is_alphabetic() && c.is_lowercase())
// or numbers
|| c.is_numeric()
// special case hyphen or underscore
|| c == '-' || c == '_'
}
impl NymName {
pub fn new(name: &str) -> Result<NymName, NymNameError> {
// We are a bit restrictive in which names we allow, to start out with. Consider relaxing
// this in the future.
if !name.chars().all(is_valid_name_char) {
return Err(NymNameError::InvalidName);
}
Ok(Self(name.to_string()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Display for NymName {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
/// [`RegisterdName`] together with the assigned [`NameId`].
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct NameEntry {
pub name_id: NameId,
pub name: RegisteredName,
}
impl NameEntry {
pub fn new(name_id: NameId, name: RegisteredName) -> Self {
Self { name_id, name }
}
}
#[cfg(test)]
mod tests {
use super::NymName;
#[test]
fn parse_nym_name() {
// Test some valid cases
assert!(NymName::new("foo").is_ok());
assert!(NymName::new("foo-bar").is_ok());
assert!(NymName::new("foo-bar-123").is_ok());
assert!(NymName::new("foo_bar").is_ok());
assert!(NymName::new("foo_bar_123").is_ok());
// And now test all some invalid ones
assert!(NymName::new("Foo").is_err());
assert!(NymName::new("foo bar").is_err());
assert!(NymName::new("foo!bar").is_err());
assert!(NymName::new("foo#bar").is_err());
assert!(NymName::new("foo$bar").is_err());
assert!(NymName::new("foo%bar").is_err());
assert!(NymName::new("foo&bar").is_err());
assert!(NymName::new("foo'bar").is_err());
assert!(NymName::new("foo(bar").is_err());
assert!(NymName::new("foo)bar").is_err());
assert!(NymName::new("foo*bar").is_err());
assert!(NymName::new("foo+bar").is_err());
assert!(NymName::new("foo,bar").is_err());
assert!(NymName::new("foo.bar").is_err());
assert!(NymName::new("foo.bar").is_err());
assert!(NymName::new("foo/bar").is_err());
assert!(NymName::new("foo/bar").is_err());
assert!(NymName::new("foo:bar").is_err());
assert!(NymName::new("foo;bar").is_err());
assert!(NymName::new("foo<bar").is_err());
assert!(NymName::new("foo=bar").is_err());
assert!(NymName::new("foo>bar").is_err());
assert!(NymName::new("foo?bar").is_err());
assert!(NymName::new("foo@bar").is_err());
assert!(NymName::new("fooBar").is_err());
assert!(NymName::new("foo[bar").is_err());
assert!(NymName::new("foo\"bar").is_err());
assert!(NymName::new("foo\\bar").is_err());
assert!(NymName::new("foo]bar").is_err());
assert!(NymName::new("foo^bar").is_err());
assert!(NymName::new("foo`bar").is_err());
assert!(NymName::new("foo{bar").is_err());
assert!(NymName::new("foo|bar").is_err());
assert!(NymName::new("foo}bar").is_err());
assert!(NymName::new("foo~bar").is_err());
}
}
@@ -7,4 +7,5 @@ edition = "2021"
[dependencies] [dependencies]
cosmwasm-std = { workspace = true } cosmwasm-std = { workspace = true }
serde = { workspace = true, default-features = false, features = ["derive"] } schemars = "0.8"
serde = { workspace = true, features = ["derive"] }
@@ -5,3 +5,5 @@ pub mod types;
// Re-export all types at the top-level // Re-export all types at the top-level
pub use types::*; pub use types::*;
pub use cosmwasm_std::{Addr, Coin, Decimal, Fraction};
@@ -40,6 +40,24 @@ impl ExecuteMsg {
pub fn delete_id(service_id: ServiceId) -> Self { pub fn delete_id(service_id: ServiceId) -> Self {
ExecuteMsg::DeleteId { service_id } ExecuteMsg::DeleteId { service_id }
} }
pub fn default_memo(&self) -> String {
match self {
ExecuteMsg::Announce {
nym_address,
service_type,
} => format!("announcing {nym_address} as type {service_type}"),
ExecuteMsg::DeleteId { service_id } => {
format!("deleting service with service id {service_id}")
}
ExecuteMsg::DeleteNymAddress { nym_address } => {
format!("deleting service with nym address {nym_address}")
}
ExecuteMsg::UpdateDepositRequired { deposit_required } => {
format!("updating the deposit required to {deposit_required}")
}
}
}
} }
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
@@ -1,5 +1,6 @@
use crate::{msg::ExecuteMsg, Service, ServiceId, ServiceInfo}; use crate::{msg::ExecuteMsg, Service, ServiceId, ServiceInfo};
use cosmwasm_std::Coin; use cosmwasm_std::Coin;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
@@ -9,7 +10,7 @@ pub struct ServiceInfoResponse {
pub service: Option<Service>, pub service: Option<Service>,
} }
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] #[derive(Serialize, Deserialize, PartialEq, Debug, Clone, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub struct ServicesListResponse { pub struct ServicesListResponse {
pub services: Vec<ServiceInfo>, pub services: Vec<ServiceInfo>,
@@ -26,6 +27,14 @@ impl ServicesListResponse {
} }
} }
impl From<&[ServiceInfo]> for ServicesListResponse {
fn from(services: &[ServiceInfo]) -> Self {
Self {
services: services.to_vec(),
}
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub struct PagedServicesListResponse { pub struct PagedServicesListResponse {
@@ -1,12 +1,13 @@
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use cosmwasm_std::{Addr, Coin}; use cosmwasm_std::{Addr, Coin};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// The directory of services are indexed by [`ServiceId`]. /// The directory of services are indexed by [`ServiceId`].
pub type ServiceId = u32; pub type ServiceId = u32;
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] #[derive(Serialize, Deserialize, Clone, PartialEq, Debug, JsonSchema)]
pub struct Service { pub struct Service {
/// The address of the service. /// The address of the service.
pub nym_address: NymAddress, pub nym_address: NymAddress,
@@ -21,7 +22,7 @@ pub struct Service {
} }
/// The types of addresses supported. /// The types of addresses supported.
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum NymAddress { pub enum NymAddress {
/// String representation of a nym address, which is of the form /// String representation of a nym address, which is of the form
@@ -51,7 +52,7 @@ impl Display for NymAddress {
} }
/// The type of services provider supported /// The type of services provider supported
#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq, Debug)] #[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq, Debug, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum ServiceType { pub enum ServiceType {
NetworkRequester, NetworkRequester,
@@ -66,7 +67,7 @@ impl std::fmt::Display for ServiceType {
} }
} }
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] #[derive(Serialize, Deserialize, PartialEq, Debug, Clone, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub struct ServiceInfo { pub struct ServiceInfo {
pub service_id: ServiceId, pub service_id: ServiceId,
@@ -1,6 +1,6 @@
[package] [package]
name = "nym-vesting-contract-common" name = "nym-vesting-contract-common"
version = "0.5.0" version = "0.6.0"
description = "Common library for the Nym vesting contract" description = "Common library for the Nym vesting contract"
edition = { workspace = true } edition = { workspace = true }
authors = { workspace = true } authors = { workspace = true }
@@ -9,7 +9,7 @@ repository = { workspace = true }
[dependencies] [dependencies]
cosmwasm-std = { workspace = true } cosmwasm-std = { workspace = true }
mixnet-contract-common = { path = "../mixnet-contract", package = "nym-mixnet-contract-common", version = "0.4.0" } mixnet-contract-common = { path = "../mixnet-contract", package = "nym-mixnet-contract-common", version = "0.5.0" }
contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.4.0" } contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.4.0" }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
schemars = "0.8" schemars = "0.8"
+2 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "nym-crypto" name = "nym-crypto"
version = "0.2.0" version = "0.3.0"
description = "Crypto library for the nym mixnet" description = "Crypto library for the nym mixnet"
edition = { workspace = true } edition = { workspace = true }
authors = { workspace = true } authors = { workspace = true }
@@ -24,6 +24,7 @@ serde_bytes = { version = "0.11.6", optional = true }
serde_crate = { version = "1.0", optional = true, default_features = false, package = "serde" } serde_crate = { version = "1.0", optional = true, default_features = false, package = "serde" }
subtle-encoding = { version = "0.5", features = ["bech32-preview"]} subtle-encoding = { version = "0.5", features = ["bech32-preview"]}
thiserror = "1.0.37" thiserror = "1.0.37"
zeroize = { version = "1.5.7", optional = true, features = ["zeroize_derive"] }
# internal # internal
nym-sphinx-types = { path = "../nymsphinx/types", version = "0.2.0" } nym-sphinx-types = { path = "../nymsphinx/types", version = "0.2.0" }
+37 -4
View File
@@ -3,15 +3,17 @@
use crate::var_names::{DEPRECATED_API_VALIDATOR, DEPRECATED_NYMD_VALIDATOR, NYM_API, NYXD}; use crate::var_names::{DEPRECATED_API_VALIDATOR, DEPRECATED_NYMD_VALIDATOR, NYM_API, NYXD};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{env::var, ops::Not, path::PathBuf}; use std::{
env::{var, VarError},
ffi::OsStr,
ops::Not,
path::PathBuf,
};
use url::Url; use url::Url;
pub mod mainnet; pub mod mainnet;
pub mod var_names; pub mod var_names;
pub const ETH_CONTRACT_ADDRESS: [u8; 20] = mainnet::_ETH_CONTRACT_ADDRESS;
pub const ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = mainnet::_ETH_ERC20_CONTRACT_ADDRESS;
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct ChainDetails { pub struct ChainDetails {
pub bech32_account_prefix: String, pub bech32_account_prefix: String,
@@ -28,6 +30,8 @@ pub struct NymContracts {
pub group_contract_address: Option<String>, pub group_contract_address: Option<String>,
pub multisig_contract_address: Option<String>, pub multisig_contract_address: Option<String>,
pub coconut_dkg_contract_address: Option<String>, pub coconut_dkg_contract_address: Option<String>,
pub service_provider_directory_contract_address: Option<String>,
pub name_service_contract_address: Option<String>,
} }
// I wanted to use the simpler `NetworkDetails` name, but there's a clash // I wanted to use the simpler `NetworkDetails` name, but there's a clash
@@ -68,6 +72,14 @@ impl NymNetworkDetails {
} }
pub fn new_from_env() -> Self { pub fn new_from_env() -> Self {
fn get_optional_env<K: AsRef<OsStr>>(env: K) -> Option<String> {
match var(env) {
Ok(var) => Some(var),
Err(VarError::NotPresent) => None,
err => panic!("Unable to set: {:?}", err),
}
}
NymNetworkDetails::new_empty() NymNetworkDetails::new_empty()
.with_bech32_account_prefix( .with_bech32_account_prefix(
var(var_names::BECH32_PREFIX).expect("bech32 prefix not set"), var(var_names::BECH32_PREFIX).expect("bech32 prefix not set"),
@@ -117,6 +129,10 @@ impl NymNetworkDetails {
.with_coconut_dkg_contract(Some( .with_coconut_dkg_contract(Some(
var(var_names::COCONUT_DKG_CONTRACT_ADDRESS).expect("coconut dkg contract not set"), var(var_names::COCONUT_DKG_CONTRACT_ADDRESS).expect("coconut dkg contract not set"),
)) ))
.with_service_provider_directory_contract(get_optional_env(
var_names::SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS,
))
.with_name_service_contract(get_optional_env(var_names::NAME_SERVICE_CONTRACT_ADDRESS))
} }
pub fn new_mainnet() -> Self { pub fn new_mainnet() -> Self {
@@ -146,6 +162,8 @@ impl NymNetworkDetails {
coconut_dkg_contract_address: parse_optional_str( coconut_dkg_contract_address: parse_optional_str(
mainnet::COCONUT_DKG_CONTRACT_ADDRESS, mainnet::COCONUT_DKG_CONTRACT_ADDRESS,
), ),
service_provider_directory_contract_address: None,
name_service_contract_address: None,
}, },
} }
} }
@@ -227,6 +245,21 @@ impl NymNetworkDetails {
self.contracts.coconut_dkg_contract_address = contract.map(Into::into); self.contracts.coconut_dkg_contract_address = contract.map(Into::into);
self self
} }
#[must_use]
pub fn with_service_provider_directory_contract<S: Into<String>>(
mut self,
contract: Option<S>,
) -> Self {
self.contracts.service_provider_directory_contract_address = contract.map(Into::into);
self
}
#[must_use]
pub fn with_name_service_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
self.contracts.name_service_contract_address = contract.map(Into::into);
self
}
} }
#[derive(Debug, Copy, Serialize, Deserialize, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Serialize, Deserialize, Clone, PartialEq, Eq)]
-4
View File
@@ -20,10 +20,6 @@ pub(crate) const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str =
pub(crate) const GROUP_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; pub(crate) const GROUP_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0";
pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0";
pub(crate) const COCONUT_DKG_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; pub(crate) const COCONUT_DKG_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0";
pub(crate) const _ETH_CONTRACT_ADDRESS: [u8; 20] =
hex_literal::hex!("0000000000000000000000000000000000000000");
pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] =
hex_literal::hex!("0000000000000000000000000000000000000000");
pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy"; pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy";
pub const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "https://mainnet-stats.nymte.ch:8090/"; pub const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "https://mainnet-stats.nymte.ch:8090/";
+3
View File
@@ -19,6 +19,9 @@ pub const MULTISIG_CONTRACT_ADDRESS: &str = "MULTISIG_CONTRACT_ADDRESS";
pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = "COCONUT_DKG_CONTRACT_ADDRESS"; pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = "COCONUT_DKG_CONTRACT_ADDRESS";
pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS"; pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS";
pub const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "STATISTICS_SERVICE_DOMAIN_ADDRESS"; pub const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "STATISTICS_SERVICE_DOMAIN_ADDRESS";
pub const SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS: &str =
"SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS";
pub const NAME_SERVICE_CONTRACT_ADDRESS: &str = "NAME_SERVICE_CONTRACT_ADDRESS";
pub const NYXD: &str = "NYXD"; pub const NYXD: &str = "NYXD";
pub const NYM_API: &str = "NYM_API"; pub const NYM_API: &str = "NYM_API";
+29
View File
@@ -0,0 +1,29 @@
[package]
name = "nym-node-tester-utils"
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.28"
rand = "0.7.3"
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["macros"]}
nym-crypto = { path = "../crypto", features = ["asymmetric"] }
nym-task = { path = "../task" }
nym-topology = { path = "../topology" }
# TODO: do we need the whole nymsphinx?
nym-sphinx = { path = "../nymsphinx" }
## non-wasm-only dependencies
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.log]
workspace = true
## wasm-only dependencies
[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-utils]
path = "../wasm-utils"
+52
View File
@@ -0,0 +1,52 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::MixId;
use nym_sphinx::chunking::ChunkingError;
use nym_sphinx::receiver::MessageRecoveryError;
use nym_topology::NymTopologyError;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum NetworkTestingError {
#[error(transparent)]
SerializationFailure(#[from] serde_json::Error),
#[error("could not recover received test message: {source}")]
MalformedTestMessageReceived { source: serde_json::Error },
#[error(transparent)]
InvalidTopology(#[from] NymTopologyError),
#[error("The specified mixnode (id: {mix_id}) doesn't exist")]
NonExistentMixnode { mix_id: MixId },
#[error("The specified mixnode (identity: {mix_identity}) doesn't exist")]
NonExistentMixnodeIdentity { mix_identity: String },
#[error("The specified gateway (id: {gateway_identity}) doesn't exist")]
NonExistentGateway { gateway_identity: String },
#[error("The provided test message is too long to fit in a single sphinx packet")]
TestMessageTooLong,
#[error(
"could not recover underlying data from the received packet since it was malformed: {source}"
)]
MalformedPacketReceived {
#[from]
source: MessageRecoveryError,
},
#[error("Received ack packet could not be recovered")]
UnrecoverableAck,
#[error("could not recover ack FragmentIdentifier: {source}")]
MalformedAckIdentifier { source: ChunkingError },
#[error("received a packet that could not be reconstructed into a full message with a single fragment")]
NonReconstructablePacket,
#[error("the recipient of the test packet was never specified")]
UnknownPacketRecipient,
}
+48
View File
@@ -0,0 +1,48 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod error;
pub mod message;
pub mod node;
pub mod processor;
pub mod receiver;
pub mod tester;
pub use message::{Empty, TestMessage};
pub use tester::NodeTester;
// it feels wrong to redefine it, but I don't want to import the whole of contract commons just for this one type
pub(crate) type MixId = u32;
#[macro_export]
macro_rules! log_err {
($($t:tt)*) => {{
#[cfg(target_arch = "wasm32")]
{::wasm_utils::console_error!($($t)*)}
#[cfg(not(target_arch = "wasm32"))]
{::log::error!($($t)*)}
}};
}
#[macro_export]
macro_rules! log_warn {
($($t:tt)*) => {{
#[cfg(target_arch = "wasm32")]
{::wasm_utils::console_warn!($($t)*)}
#[cfg(not(target_arch = "wasm32"))]
{::log::warn!($($t)*)}
}};
}
#[macro_export]
macro_rules! log_info {
($($t:tt)*) => {{
#[cfg(target_arch = "wasm32")]
{::wasm_utils::console_log!($($t)*)}
#[cfg(not(target_arch = "wasm32"))]
{::log::info!($($t)*)}
}};
}
+127
View File
@@ -0,0 +1,127 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::NetworkTestingError;
use crate::node::TestableNode;
use nym_sphinx::message::NymMessage;
use nym_topology::{gateway, mix};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Copy)]
pub struct Empty;
#[derive(Serialize, Deserialize, Clone)]
pub struct TestMessage<T = Empty> {
pub tested_node: TestableNode,
pub msg_id: u32,
pub total_msgs: u32,
// any additional fields that might be required by a specific tester.
// For example nym-api might want to attach route ids
#[serde(flatten)]
pub ext: T,
}
impl<T> TestMessage<T> {
pub fn new<N: Into<TestableNode>>(node: N, msg_id: u32, total_msgs: u32, ext: T) -> Self {
TestMessage {
tested_node: node.into(),
msg_id,
total_msgs,
ext,
}
}
pub fn new_mix(node: &mix::Node, msg_id: u32, total_msgs: u32, ext: T) -> Self {
Self::new(node, msg_id, total_msgs, ext)
}
pub fn new_gateway(node: &gateway::Node, msg_id: u32, total_msgs: u32, ext: T) -> Self {
Self::new(node, msg_id, total_msgs, ext)
}
pub fn new_serialized<N>(
node: N,
msg_id: u32,
total_msgs: u32,
ext: T,
) -> Result<Vec<u8>, NetworkTestingError>
where
N: Into<TestableNode>,
T: Serialize,
{
Self::new(node, msg_id, total_msgs, ext).as_bytes()
}
pub fn new_plaintexts<N>(
node: &N,
total_msgs: u32,
ext: T,
) -> Result<Vec<Vec<u8>>, NetworkTestingError>
where
for<'a> &'a N: Into<TestableNode>,
T: Serialize + Clone,
{
let mut msgs = Vec::with_capacity(total_msgs as usize);
for msg_id in 1..=total_msgs {
msgs.push(Self::new(node, msg_id, total_msgs, ext.clone()).as_bytes()?)
}
Ok(msgs)
}
pub fn mix_plaintexts(
node: &mix::Node,
total_msgs: u32,
ext: T,
) -> Result<Vec<Vec<u8>>, NetworkTestingError>
where
T: Serialize + Clone,
{
Self::new_plaintexts(node, total_msgs, ext)
}
pub fn gateway_plaintexts(
node: &gateway::Node,
total_msgs: u32,
ext: T,
) -> Result<Vec<Vec<u8>>, NetworkTestingError>
where
T: Serialize + Clone,
{
Self::new_plaintexts(node, total_msgs, ext)
}
pub fn as_json_string(&self) -> Result<String, NetworkTestingError>
where
T: Serialize,
{
serde_json::to_string(self).map_err(Into::into)
}
pub fn as_bytes(&self) -> Result<Vec<u8>, NetworkTestingError>
where
T: Serialize,
{
// the test messages are supposed to be rather small so we can use the good old serde_json
// (the performance penalty over bincode or custom serialization should be minimal)
serde_json::to_vec(self).map_err(Into::into)
}
pub fn try_recover(msg: NymMessage) -> Result<Self, NetworkTestingError>
where
T: DeserializeOwned,
{
let inner = msg.into_inner_data();
Self::try_recover_from_bytes(&inner)
}
pub fn try_recover_from_bytes(raw: &[u8]) -> Result<Self, NetworkTestingError>
where
T: DeserializeOwned,
{
serde_json::from_slice(raw)
.map_err(|source| NetworkTestingError::MalformedTestMessageReceived { source })
}
}
+92
View File
@@ -0,0 +1,92 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::MixId;
use nym_topology::{gateway, mix};
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
#[derive(Serialize, Deserialize, Debug, Clone, Hash, Eq, PartialEq)]
pub struct TestableNode {
pub encoded_identity: String,
pub owner: String,
#[serde(rename = "type")]
pub typ: NodeType,
}
impl TestableNode {
pub fn new(encoded_identity: String, owner: String, typ: NodeType) -> Self {
TestableNode {
encoded_identity,
owner,
typ,
}
}
pub fn new_mixnode(encoded_identity: String, owner: String, mix_id: MixId) -> Self {
TestableNode::new(encoded_identity, owner, NodeType::Mixnode { mix_id })
}
pub fn new_gateway(encoded_identity: String, owner: String) -> Self {
TestableNode::new(encoded_identity, owner, NodeType::Gateway)
}
pub fn is_mixnode(&self) -> bool {
self.typ.is_mixnode()
}
}
impl<'a> From<&'a mix::Node> for TestableNode {
fn from(value: &'a mix::Node) -> Self {
TestableNode {
encoded_identity: value.identity_key.to_base58_string(),
owner: value.owner.clone(),
typ: NodeType::Mixnode {
mix_id: value.mix_id,
},
}
}
}
impl<'a> From<&'a gateway::Node> for TestableNode {
fn from(value: &'a gateway::Node) -> Self {
TestableNode {
encoded_identity: value.identity_key.to_base58_string(),
owner: value.owner.clone(),
typ: NodeType::Gateway,
}
}
}
impl Display for TestableNode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} {} owned by {}",
self.typ, self.encoded_identity, self.owner
)
}
}
#[derive(Serialize, Deserialize, Hash, Clone, Copy, Debug, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum NodeType {
Mixnode { mix_id: MixId },
Gateway,
}
impl NodeType {
pub fn is_mixnode(&self) -> bool {
matches!(self, NodeType::Mixnode { .. })
}
}
impl Display for NodeType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
NodeType::Mixnode { mix_id } => write!(f, "mixnode (mix_id {mix_id})"),
NodeType::Gateway => write!(f, "gateway"),
}
}
}
+99
View File
@@ -0,0 +1,99 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::NetworkTestingError;
use crate::TestMessage;
use nym_crypto::asymmetric::encryption;
use nym_sphinx::acknowledgements::identifier::recover_identifier;
use nym_sphinx::acknowledgements::AckKey;
use nym_sphinx::chunking::fragment::FragmentIdentifier;
use nym_sphinx::receiver::{MessageReceiver, SphinxMessageReceiver};
use serde::de::DeserializeOwned;
use std::marker::PhantomData;
use std::sync::Arc;
// simple enum containing aggregated processed results
pub enum Received<T> {
Message(TestMessage<T>),
Ack(FragmentIdentifier),
}
impl<T> From<TestMessage<T>> for Received<T> {
fn from(value: TestMessage<T>) -> Self {
Received::Message(value)
}
}
impl<T> From<FragmentIdentifier> for Received<T> {
fn from(value: FragmentIdentifier) -> Self {
Received::Ack(value)
}
}
pub struct TestPacketProcessor<T, R: MessageReceiver = SphinxMessageReceiver> {
local_encryption_keypair: Arc<encryption::KeyPair>,
ack_key: Arc<AckKey>,
/// Structure responsible for decrypting and recovering plaintext message from received ciphertexts.
message_receiver: R,
_ext_phantom: PhantomData<T>,
}
impl<T> TestPacketProcessor<T, SphinxMessageReceiver> {
pub fn new_sphinx_processor(
local_encryption_keypair: Arc<encryption::KeyPair>,
ack_key: Arc<AckKey>,
) -> Self {
Self::new(local_encryption_keypair, ack_key)
}
}
impl<T, R> TestPacketProcessor<T, R>
where
R: MessageReceiver,
{
pub fn new(local_encryption_keypair: Arc<encryption::KeyPair>, ack_key: Arc<AckKey>) -> Self {
TestPacketProcessor {
local_encryption_keypair,
ack_key,
message_receiver: R::new(),
_ext_phantom: PhantomData,
}
}
pub fn process_mixnet_message(
&mut self,
mut raw_message: Vec<u8>,
) -> Result<TestMessage<T>, NetworkTestingError>
where
T: DeserializeOwned,
{
let plaintext = self
.message_receiver
.recover_plaintext_from_regular_packet(
self.local_encryption_keypair.private_key(),
&mut raw_message,
)?;
let fragment = self.message_receiver.recover_fragment(plaintext)?;
// test messages must consist of a single fragment
let (serialized, _) = self
.message_receiver
.insert_new_fragment(fragment)?
.ok_or(NetworkTestingError::NonReconstructablePacket)?;
TestMessage::try_recover(serialized)
}
pub fn process_ack(
&mut self,
raw_ack: Vec<u8>,
) -> Result<FragmentIdentifier, NetworkTestingError> {
let serialized_ack = recover_identifier(&self.ack_key, &raw_ack)
.ok_or(NetworkTestingError::UnrecoverableAck)?;
FragmentIdentifier::try_from_bytes(serialized_ack)
.map_err(|source| NetworkTestingError::MalformedAckIdentifier { source })
}
}
+129
View File
@@ -0,0 +1,129 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::NetworkTestingError;
use crate::processor::{Received, TestPacketProcessor};
use crate::{log_err, log_info, log_warn};
use futures::channel::mpsc;
use futures::StreamExt;
use nym_crypto::asymmetric::encryption;
use nym_sphinx::acknowledgements::AckKey;
use nym_sphinx::receiver::{MessageReceiver, SphinxMessageReceiver};
use nym_task::TaskClient;
use serde::de::DeserializeOwned;
use std::sync::Arc;
pub type ReceivedSender<T> = mpsc::UnboundedSender<Received<T>>;
pub type ReceivedReceiver<T> = mpsc::UnboundedReceiver<Received<T>>;
// the 'Simple' bit comes from the fact that it expects all received messages to consist of a single `Fragment`
pub struct SimpleMessageReceiver<T, R: MessageReceiver = SphinxMessageReceiver> {
message_processor: TestPacketProcessor<T, R>,
mixnet_message_receiver: mpsc::UnboundedReceiver<Vec<Vec<u8>>>,
acks_receiver: mpsc::UnboundedReceiver<Vec<Vec<u8>>>,
received_sender: ReceivedSender<T>,
shutdown: TaskClient,
}
impl<T> SimpleMessageReceiver<T, SphinxMessageReceiver> {
pub fn new_sphinx_receiver(
local_encryption_keypair: Arc<encryption::KeyPair>,
ack_key: Arc<AckKey>,
mixnet_message_receiver: mpsc::UnboundedReceiver<Vec<Vec<u8>>>,
acks_receiver: mpsc::UnboundedReceiver<Vec<Vec<u8>>>,
received_sender: ReceivedSender<T>,
shutdown: TaskClient,
) -> Self {
Self::new(
local_encryption_keypair,
ack_key,
mixnet_message_receiver,
acks_receiver,
received_sender,
shutdown,
)
}
}
impl<T, R: MessageReceiver> SimpleMessageReceiver<T, R> {
pub fn new(
local_encryption_keypair: Arc<encryption::KeyPair>,
ack_key: Arc<AckKey>,
mixnet_message_receiver: mpsc::UnboundedReceiver<Vec<Vec<u8>>>,
acks_receiver: mpsc::UnboundedReceiver<Vec<Vec<u8>>>,
received_sender: ReceivedSender<T>,
shutdown: TaskClient,
) -> Self {
SimpleMessageReceiver {
message_processor: TestPacketProcessor::new(local_encryption_keypair, ack_key),
mixnet_message_receiver,
acks_receiver,
received_sender,
shutdown,
}
}
fn forward_received<U: Into<Received<T>>>(&self, received: U) {
// TODO: remove the unwrap once/if we do graceful shutdowns here
self.received_sender
.unbounded_send(received.into())
.expect("ReceivedReceiver has stopped receiving");
}
fn on_mixnet_message(&mut self, raw_message: Vec<u8>) -> Result<(), NetworkTestingError>
where
T: DeserializeOwned,
{
let recovered = self.message_processor.process_mixnet_message(raw_message)?;
self.forward_received(recovered);
Ok(())
}
fn on_ack(&mut self, raw_ack: Vec<u8>) -> Result<(), NetworkTestingError> {
let frag_id = self.message_processor.process_ack(raw_ack)?;
self.forward_received(frag_id);
Ok(())
}
pub async fn run(&mut self)
where
T: DeserializeOwned,
{
while !self.shutdown.is_shutdown() {
tokio::select! {
biased;
_ = self.shutdown.recv() => {
log_info!("SimpleMessageReceiver: received shutdown")
}
mixnet_messages = self.mixnet_message_receiver.next() => {
let Some(mixnet_messages) = mixnet_messages else {
log_err!("the mixnet messages stream has terminated!");
// note: this will cause global shutdown, but we have no choice if we stopped receiving mixnet messages
break
};
for message in mixnet_messages {
if let Err(err) = self.on_mixnet_message(message) {
log_warn!("failed to process received mixnet message: {err}")
}
}
}
acks = self.acks_receiver.next() => {
let Some(acks) = acks else {
log_err!("the ack messages stream has terminated!");
// note: this will cause global shutdown, but we have no choice if we stopped receiving mixnet messages
break
};
for ack in acks {
if let Err(err) = self.on_ack(ack) {
log_warn!("failed to process received ack message: {err}")
}
}
}
}
}
log_info!("SimpleMessageReceiver: Exiting")
}
}
+281
View File
@@ -0,0 +1,281 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::NetworkTestingError;
use crate::Empty;
use crate::MixId;
use crate::TestMessage;
use nym_sphinx::acknowledgements::AckKey;
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::message::NymMessage;
use nym_sphinx::params::{PacketSize, DEFAULT_NUM_MIX_HOPS};
use nym_sphinx::preparer::{FragmentPreparer, PreparedFragment};
use nym_topology::{gateway, mix, NymTopology};
use rand::{CryptoRng, Rng};
use serde::Serialize;
use std::sync::Arc;
use std::time::Duration;
pub struct NodeTester<R> {
rng: R,
base_topology: NymTopology,
/// Generally test packets are designed to be sent from ourselves to ourselves,
/// However, one might want to customise this behaviour.
/// In that case an explicit `Recipient` has to be provided when constructing test packets.
self_address: Option<Recipient>,
packet_size: PacketSize,
/// Average delay a data packet is going to get delay at a single mixnode.
average_packet_delay: Duration,
/// Average delay an acknowledgement packet is going to get delay at a single mixnode.
average_ack_delay: Duration,
/// Number of mix hops each packet ('real' message, ack, reply) is expected to take.
/// Note that it does not include gateway hops.
num_mix_hops: u8,
// while acks are going to be ignored they still need to be constructed
// so that the gateway would be able to correctly process and forward the message
ack_key: Arc<AckKey>,
}
impl<R> NodeTester<R>
where
R: Rng + CryptoRng,
{
pub fn new(
rng: R,
base_topology: NymTopology,
self_address: Option<Recipient>,
packet_size: PacketSize,
average_packet_delay: Duration,
average_ack_delay: Duration,
ack_key: Arc<AckKey>,
) -> Self {
Self {
rng,
base_topology,
self_address,
packet_size,
average_packet_delay,
average_ack_delay,
num_mix_hops: DEFAULT_NUM_MIX_HOPS,
ack_key,
}
}
/// Allows setting non-default number of expected mix hops in the network.
#[allow(dead_code)]
pub fn with_mix_hops(mut self, hops: u8) -> Self {
self.num_mix_hops = hops;
self
}
pub fn testable_mix_topology(&self, node: &mix::Node) -> NymTopology {
let mut topology = self.base_topology.clone();
topology.set_mixes_in_layer(node.layer as u8, vec![node.clone()]);
topology
}
pub fn testable_gateway_topology(&self, gateway: &gateway::Node) -> NymTopology {
let mut topology = self.base_topology.clone();
topology.set_gateways(vec![gateway.clone()]);
topology
}
pub fn simple_mixnode_test_packets(
&mut self,
mix: &mix::Node,
test_packets: u32,
) -> Result<Vec<PreparedFragment>, NetworkTestingError> {
self.mixnode_test_packets(mix, Empty, test_packets, None)
}
pub fn mixnode_test_packets<T>(
&mut self,
mix: &mix::Node,
msg_ext: T,
test_packets: u32,
custom_recipient: Option<Recipient>,
) -> Result<Vec<PreparedFragment>, NetworkTestingError>
where
T: Serialize + Clone,
{
let ephemeral_topology = self.testable_mix_topology(mix);
let mut packets = Vec::with_capacity(test_packets as usize);
for plaintext in TestMessage::mix_plaintexts(mix, test_packets, msg_ext)? {
packets.push(self.wrap_plaintext_data(
plaintext,
&ephemeral_topology,
custom_recipient,
)?);
}
Ok(packets)
}
pub fn mixnodes_test_packets<T>(
&mut self,
nodes: &[mix::Node],
msg_ext: T,
test_packets: u32,
custom_recipient: Option<Recipient>,
) -> Result<Vec<PreparedFragment>, NetworkTestingError>
where
T: Serialize + Clone,
{
let mut packets = Vec::new();
for node in nodes {
packets.append(&mut self.mixnode_test_packets(
node,
msg_ext.clone(),
test_packets,
custom_recipient,
)?)
}
Ok(packets)
}
pub fn existing_mixnode_test_packets<T>(
&mut self,
mix_id: MixId,
msg_ext: T,
test_packets: u32,
custom_recipient: Option<Recipient>,
) -> Result<Vec<PreparedFragment>, NetworkTestingError>
where
T: Serialize + Clone,
{
let Some(node) = self.base_topology.find_mix(mix_id) else {
return Err(NetworkTestingError::NonExistentMixnode {mix_id})
};
self.mixnode_test_packets(&node.clone(), msg_ext, test_packets, custom_recipient)
}
pub fn existing_identity_mixnode_test_packets<T>(
&mut self,
encoded_mix_identity: String,
msg_ext: T,
test_packets: u32,
custom_recipient: Option<Recipient>,
) -> Result<Vec<PreparedFragment>, NetworkTestingError>
where
T: Serialize + Clone,
{
let Some(node) = self.base_topology.find_mix_by_identity(&encoded_mix_identity) else {
return Err(NetworkTestingError::NonExistentMixnodeIdentity { mix_identity: encoded_mix_identity })
};
self.mixnode_test_packets(&node.clone(), msg_ext, test_packets, custom_recipient)
}
pub fn gateway_test_packets<T>(
&mut self,
gateway: &gateway::Node,
msg_ext: T,
test_packets: u32,
custom_recipient: Option<Recipient>,
) -> Result<Vec<PreparedFragment>, NetworkTestingError>
where
T: Serialize + Clone,
{
let ephemeral_topology = self.testable_gateway_topology(gateway);
let mut packets = Vec::with_capacity(test_packets as usize);
for plaintext in TestMessage::gateway_plaintexts(gateway, test_packets, msg_ext)? {
packets.push(self.wrap_plaintext_data(
plaintext,
&ephemeral_topology,
custom_recipient,
)?);
}
Ok(packets)
}
pub fn existing_gateway_test_packets<T>(
&mut self,
encoded_gateway_identity: String,
msg_ext: T,
test_packets: u32,
custom_recipient: Option<Recipient>,
) -> Result<Vec<PreparedFragment>, NetworkTestingError>
where
T: Serialize + Clone,
{
let Some(node) = self.base_topology.find_gateway(&encoded_gateway_identity) else {
return Err(NetworkTestingError::NonExistentGateway { gateway_identity: encoded_gateway_identity })
};
self.gateway_test_packets(&node.clone(), msg_ext, test_packets, custom_recipient)
}
pub fn wrap_plaintext_data(
&mut self,
plaintext: Vec<u8>,
topology: &NymTopology,
custom_recipient: Option<Recipient>,
) -> Result<PreparedFragment, NetworkTestingError> {
let message = NymMessage::new_plain(plaintext);
let mut fragments = self.pad_and_split_message(message, self.packet_size);
if fragments.len() != 1 {
return Err(NetworkTestingError::TestMessageTooLong);
}
// SAFETY: the unwrap here is fine as if the vec was somehow empty
// we would have returned the error when checking for its length
let fragment = fragments.pop().unwrap();
// either `self_address` or `custom_recipient` has to be specified.
let address = custom_recipient.unwrap_or(
self.self_address
.ok_or(NetworkTestingError::UnknownPacketRecipient)?,
);
// TODO: can we avoid this arc clone?
let ack_key = Arc::clone(&self.ack_key);
Ok(self.prepare_chunk_for_sending(fragment, topology, &ack_key, &address, &address)?)
}
pub fn create_test_packet<T>(
&mut self,
message: &TestMessage<T>,
topology: &NymTopology,
custom_recipient: Option<Recipient>,
) -> Result<PreparedFragment, NetworkTestingError>
where
T: Serialize,
{
let serialized = message.as_bytes()?;
self.wrap_plaintext_data(serialized, topology, custom_recipient)
}
}
impl<R: CryptoRng + Rng> FragmentPreparer for NodeTester<R> {
type Rng = R;
fn rng(&mut self) -> &mut Self::Rng {
&mut self.rng
}
fn num_mix_hops(&self) -> u8 {
self.num_mix_hops
}
fn average_packet_delay(&self) -> Duration {
self.average_packet_delay
}
fn average_ack_delay(&self) -> Duration {
self.average_ack_delay
}
}
+3 -2
View File
@@ -20,11 +20,12 @@ nym-sphinx-chunking = { path = "chunking" }
nym-sphinx-cover = { path = "cover" } nym-sphinx-cover = { path = "cover" }
nym-sphinx-forwarding = { path = "forwarding" } nym-sphinx-forwarding = { path = "forwarding" }
nym-sphinx-params = { path = "params" } nym-sphinx-params = { path = "params" }
nym-sphinx-routing = { path = "routing" }
nym-sphinx-types = { path = "types" } nym-sphinx-types = { path = "types" }
# those dependencies are due to intriducing preparer and receiver. Perpaphs that indicates they should be moved # those dependencies are due to intriducing preparer and receiver. Perpaphs that indicates they should be moved
# to separate crate? # to separate crate?
nym-crypto = { path = "../crypto", version = "0.2.0" } nym-crypto = { path = "../crypto", version = "0.3.0" }
nym-topology = { path = "../topology" } nym-topology = { path = "../topology" }
# outfox # outfox
@@ -32,7 +33,7 @@ nym-outfox = { path = "../../nym-outfox" }
[dev-dependencies] [dev-dependencies]
nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" } nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" }
nym-crypto = { path = "../crypto", version = "0.2.0", features = ["asymmetric"] } nym-crypto = { path = "../crypto", version = "0.3.0", features = ["asymmetric"] }
# do not include this when compiling into wasm as it somehow when combined together with reqwest, it will require # do not include this when compiling into wasm as it somehow when combined together with reqwest, it will require
# net2 via tokio-util -> tokio -> mio -> net2 # net2 via tokio-util -> tokio -> mio -> net2
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "nym-sphinx-routing"
version = "0.1.0"
description = "Sphinx packet routing as Nym mix packets"
edition = { workspace = true }
authors = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
[dependencies]
thiserror = { workspace = true }
nym-sphinx-addressing = { path = "../addressing" }
nym-sphinx-types = { path = "../types" }
+43
View File
@@ -0,0 +1,43 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_sphinx_addressing::clients::Recipient;
use nym_sphinx_types::Node;
use thiserror::Error;
pub trait SphinxRouteMaker {
type Error;
fn sphinx_route(&mut self, hops: u8, destination: &Recipient)
-> Result<Vec<Node>, Self::Error>;
}
#[derive(Debug, Error, Clone, Copy)]
#[error("the route vector contains {available} nodes while {requested} hops are required")]
pub struct InvalidNumberOfHops {
available: usize,
requested: u8,
}
// if one wants to provide a hardcoded route, they can
impl SphinxRouteMaker for Vec<Node> {
type Error = InvalidNumberOfHops;
fn sphinx_route(
&mut self,
hops: u8,
_destination: &Recipient,
) -> Result<Vec<Node>, InvalidNumberOfHops> {
// it's the responsibility of the caller to ensure the hardcoded route has correct number of hops
// and that it's final hop include the recipient's gateway.
if self.len() != hops as usize {
Err(InvalidNumberOfHops {
available: self.len(),
requested: hops,
})
} else {
Ok(self.clone())
}
}
}
+4 -2
View File
@@ -13,10 +13,12 @@ pub use nym_sphinx_anonymous_replies as anonymous_replies;
pub use nym_sphinx_chunking as chunking; pub use nym_sphinx_chunking as chunking;
pub use nym_sphinx_cover as cover; pub use nym_sphinx_cover as cover;
pub use nym_sphinx_forwarding as forwarding; pub use nym_sphinx_forwarding as forwarding;
pub use nym_sphinx_params as params;
pub use nym_sphinx_routing as routing;
pub use nym_sphinx_types::*;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub use nym_sphinx_framing as framing; pub use nym_sphinx_framing as framing;
pub use nym_sphinx_params as params;
pub use nym_sphinx_types::*;
// TEMP UNTIL FURTHER REFACTORING // TEMP UNTIL FURTHER REFACTORING
pub use preparer::payload::NymsphinxPayloadBuilder; pub use preparer::payload::NymsphinxPayloadBuilder;
+236 -125
View File
@@ -38,6 +38,206 @@ pub struct PreparedFragment {
pub fragment_identifier: FragmentIdentifier, pub fragment_identifier: FragmentIdentifier,
} }
impl From<PreparedFragment> for MixPacket {
fn from(value: PreparedFragment) -> Self {
value.mix_packet
}
}
// this is extracted into a trait with default implementation to remove duplicate code
// (which we REALLY want to avoid with crypto)
pub trait FragmentPreparer {
type Rng: CryptoRng + Rng;
fn rng(&mut self) -> &mut Self::Rng;
fn num_mix_hops(&self) -> u8;
fn average_packet_delay(&self) -> Duration;
fn average_ack_delay(&self) -> Duration;
fn generate_reply_surbs(
&mut self,
amount: usize,
topology: &NymTopology,
reply_recipient: &Recipient,
) -> Result<Vec<ReplySurb>, NymTopologyError> {
let mut reply_surbs = Vec::with_capacity(amount);
let packet_delay = self.average_packet_delay();
for _ in 0..amount {
let reply_surb =
ReplySurb::construct(self.rng(), reply_recipient, packet_delay, topology)?;
reply_surbs.push(reply_surb)
}
Ok(reply_surbs)
}
fn generate_surb_ack(
&mut self,
recipient: &Recipient,
fragment_id: FragmentIdentifier,
topology: &NymTopology,
ack_key: &AckKey,
) -> Result<SurbAck, NymTopologyError> {
let ack_delay = self.average_ack_delay();
SurbAck::construct(
self.rng(),
recipient,
ack_key,
fragment_id.to_bytes(),
ack_delay,
topology,
)
}
/// The procedure is as follows:
/// For each fragment:
/// - compute SURB_ACK
/// - generate (x, g^x)
/// - obtain key k from the reply-surb which was computed as follows:
/// k = KDF(remote encryption key ^ x) this is equivalent to KDF( dh(remote, x) )
/// - compute v_b = AES-128-CTR(k, serialized_fragment)
/// - compute vk_b = H(k) || v_b
/// - compute sphinx_plaintext = SURB_ACK || H(k) || v_b
/// - compute sphinx_packet by applying the reply surb on the sphinx_plaintext
fn prepare_reply_chunk_for_sending(
&mut self,
fragment: Fragment,
topology: &NymTopology,
ack_key: &AckKey,
reply_surb: ReplySurb,
packet_sender: &Recipient,
) -> Result<PreparedFragment, NymTopologyError> {
// each reply attaches the digest of the encryption key so that the recipient could
// lookup correct key for decryption,
let reply_overhead = ReplySurbKeyDigestAlgorithm::output_size();
let expected_plaintext = fragment.serialized_size() + ACK_OVERHEAD + reply_overhead;
// the reason we're unwrapping (or rather 'expecting') here rather than handling the error
// more gracefully is that this error should never be reached as it implies incorrect chunking
let packet_size = PacketSize::get_type_from_plaintext(expected_plaintext)
.expect("the message has been incorrectly fragmented");
// this is not going to be accurate by any means. but that's the best estimation we can do
let expected_forward_delay = Delay::new_from_millis(
(self.average_packet_delay().as_millis() * self.num_mix_hops() as u128) as u64,
);
let fragment_identifier = fragment.fragment_identifier();
// create an ack
let surb_ack =
self.generate_surb_ack(packet_sender, fragment_identifier, topology, ack_key)?;
let ack_delay = surb_ack.expected_total_delay();
let packet_payload = NymsphinxPayloadBuilder::new(fragment, surb_ack)
.build_reply(reply_surb.encryption_key());
// the unwrap here is fine as the failures can only originate from attempting to use invalid payload lengths
// and we just very carefully constructed a (presumably) valid one
let (sphinx_packet, first_hop_address) =
reply_surb.apply_surb(packet_payload, packet_size).unwrap();
Ok(PreparedFragment {
// the round-trip delay is the sum of delays of all hops on the forward route as
// well as the total delay of the ack packet.
// we don't know the delays inside the reply surbs so we use best-effort estimation from our poisson distribution
total_delay: expected_forward_delay + ack_delay,
mix_packet: MixPacket::new(first_hop_address, sphinx_packet, Default::default()),
fragment_identifier,
})
}
/// Tries to convert this [`Fragment`] into a [`SphinxPacket`] that can be sent through the Nym mix-network,
/// such that it contains required SURB-ACK and public component of the ephemeral key used to
/// derive the shared key.
/// Also all the data, apart from the said public component, is encrypted with an ephemeral shared key.
/// This method can fail if the provided network topology is invalid.
/// It returns total expected delay as well as the [`SphinxPacket`] (including first hop address)
/// to be sent through the network.
///
/// The procedure is as follows:
/// For each fragment:
/// - compute SURB_ACK
/// - generate (x, g^x)
/// - compute k = KDF(remote encryption key ^ x) this is equivalent to KDF( dh(remote, x) )
/// - compute v_b = AES-128-CTR(k, serialized_fragment)
/// - compute vk_b = g^x || v_b
/// - compute sphinx_plaintext = SURB_ACK || g^x || v_b
/// - compute sphinx_packet = Sphinx(recipient, sphinx_plaintext)
fn prepare_chunk_for_sending(
&mut self,
fragment: Fragment,
topology: &NymTopology,
ack_key: &AckKey,
packet_sender: &Recipient,
packet_recipient: &Recipient,
) -> Result<PreparedFragment, NymTopologyError> {
// each plain or repliable packet (i.e. not a reply) attaches an ephemeral public key so that the recipient
// could perform diffie-hellman with its own keys followed by a kdf to re-derive
// the packet encryption key
let non_reply_overhead = encryption::PUBLIC_KEY_SIZE;
let expected_plaintext = fragment.serialized_size() + ACK_OVERHEAD + non_reply_overhead;
// the reason we're unwrapping (or rather 'expecting') here rather than handling the error
// more gracefully is that this error should never be reached as it implies incorrect chunking
let packet_size = PacketSize::get_type_from_plaintext(expected_plaintext)
.expect("the message has been incorrectly fragmented");
let fragment_identifier = fragment.fragment_identifier();
// create an ack
let surb_ack =
self.generate_surb_ack(packet_sender, fragment_identifier, topology, ack_key)?;
let ack_delay = surb_ack.expected_total_delay();
let packet_payload = NymsphinxPayloadBuilder::new(fragment, surb_ack)
.build_regular(self.rng(), packet_recipient.encryption_key());
// generate pseudorandom route for the packet
let hops = self.num_mix_hops();
let route =
topology.random_route_to_gateway(self.rng(), hops, packet_recipient.gateway())?;
let destination = packet_recipient.as_sphinx_destination();
// including set of delays
let delays =
delays::generate_from_average_duration(route.len(), self.average_packet_delay());
// create the actual sphinx packet here. With valid route and correct payload size,
// there's absolutely no reason for this call to fail.
let sphinx_packet = SphinxPacketBuilder::new()
.with_payload_size(packet_size.payload_size())
.build_packet(packet_payload, &route, &destination, &delays)
.unwrap();
// from the previously constructed route extract the first hop
let first_hop_address =
NymNodeRoutingAddress::try_from(route.first().unwrap().address).unwrap();
Ok(PreparedFragment {
// the round-trip delay is the sum of delays of all hops on the forward route as
// well as the total delay of the ack packet.
// note that the last hop of the packet is a gateway that does not do any delays
total_delay: delays.iter().take(delays.len() - 1).sum::<Delay>() + ack_delay,
mix_packet: MixPacket::new(first_hop_address, sphinx_packet, Default::default()),
fragment_identifier,
})
}
fn pad_and_split_message(
&mut self,
message: NymMessage,
packet_size: PacketSize,
) -> Vec<Fragment> {
let plaintext_per_packet = message.available_sphinx_plaintext_per_packet(packet_size);
message
.pad_to_full_packet_lengths(plaintext_per_packet)
.split_into_fragments(self.rng(), plaintext_per_packet)
}
}
/// Prepares the message that is to be sent through the mix network by attaching /// Prepares the message that is to be sent through the mix network by attaching
/// an optional reply-SURB, padding it to appropriate length, encrypting its content, /// an optional reply-SURB, padding it to appropriate length, encrypting its content,
/// and chunking into appropriate size [`Fragment`]s. /// and chunking into appropriate size [`Fragment`]s.
@@ -111,16 +311,6 @@ where
Ok(reply_surbs) Ok(reply_surbs)
} }
/// The procedure is as follows:
/// For each fragment:
/// - compute SURB_ACK
/// - generate (x, g^x)
/// - obtain key k from the reply-surb which was computed as follows:
/// k = KDF(remote encryption key ^ x) this is equivalent to KDF( dh(remote, x) )
/// - compute v_b = AES-128-CTR(k, serialized_fragment)
/// - compute vk_b = H(k) || v_b
/// - compute sphinx_plaintext = SURB_ACK || H(k) || v_b
/// - compute sphinx_packet by applying the reply surb on the sphinx_plaintext
pub fn prepare_reply_chunk_for_sending( pub fn prepare_reply_chunk_for_sending(
&mut self, &mut self,
fragment: Fragment, fragment: Fragment,
@@ -128,62 +318,13 @@ where
ack_key: &AckKey, ack_key: &AckKey,
reply_surb: ReplySurb, reply_surb: ReplySurb,
) -> Result<PreparedFragment, NymTopologyError> { ) -> Result<PreparedFragment, NymTopologyError> {
// each reply attaches the digest of the encryption key so that the recipient could let sender = self.sender_address;
// lookup correct key for decryption,
let reply_overhead = ReplySurbKeyDigestAlgorithm::output_size();
let expected_plaintext = fragment.serialized_size() + ACK_OVERHEAD + reply_overhead;
// the reason we're unwrapping (or rather 'expecting') here rather than handling the error <Self as FragmentPreparer>::prepare_reply_chunk_for_sending(
// more gracefully is that this error should never be reached as it implies incorrect chunking self, fragment, topology, ack_key, reply_surb, &sender,
let packet_size = PacketSize::get_type_from_plaintext(expected_plaintext) )
.expect("the message has been incorrectly fragmented");
// this is not going to be accurate by any means. but that's the best estimation we can do
let expected_forward_delay = Delay::new_from_millis(
(self.average_packet_delay.as_millis() * self.num_mix_hops as u128) as u64,
);
let fragment_identifier = fragment.fragment_identifier();
// create an ack
let surb_ack = self.generate_surb_ack(fragment_identifier, topology, ack_key)?;
let ack_delay = surb_ack.expected_total_delay();
let packet_payload = NymsphinxPayloadBuilder::new(fragment, surb_ack)
.build_reply(reply_surb.encryption_key());
// the unwrap here is fine as the failures can only originate from attempting to use invalid payload lengths
// and we just very carefully constructed a (presumably) valid one
let (sphinx_packet, first_hop_address) =
reply_surb.apply_surb(packet_payload, packet_size).unwrap();
Ok(PreparedFragment {
// the round-trip delay is the sum of delays of all hops on the forward route as
// well as the total delay of the ack packet.
// we don't know the delays inside the reply surbs so we use best-effort estimation from our poisson distribution
total_delay: expected_forward_delay + ack_delay,
mix_packet: MixPacket::new(first_hop_address, sphinx_packet, Default::default()),
fragment_identifier,
})
} }
/// Tries to convert this [`Fragment`] into a [`SphinxPacket`] that can be sent through the Nym mix-network,
/// such that it contains required SURB-ACK and public component of the ephemeral key used to
/// derive the shared key.
/// Also all the data, apart from the said public component, is encrypted with an ephemeral shared key.
/// This method can fail if the provided network topology is invalid.
/// It returns total expected delay as well as the [`SphinxPacket`] (including first hop address)
/// to be sent through the network.
///
/// The procedure is as follows:
/// For each fragment:
/// - compute SURB_ACK
/// - generate (x, g^x)
/// - compute k = KDF(remote encryption key ^ x) this is equivalent to KDF( dh(remote, x) )
/// - compute v_b = AES-128-CTR(k, serialized_fragment)
/// - compute vk_b = g^x || v_b
/// - compute sphinx_plaintext = SURB_ACK || g^x || v_b
/// - compute sphinx_packet = Sphinx(recipient, sphinx_plaintext)
pub fn prepare_chunk_for_sending( pub fn prepare_chunk_for_sending(
&mut self, &mut self,
fragment: Fragment, fragment: Fragment,
@@ -191,73 +332,27 @@ where
ack_key: &AckKey, ack_key: &AckKey,
packet_recipient: &Recipient, packet_recipient: &Recipient,
) -> Result<PreparedFragment, NymTopologyError> { ) -> Result<PreparedFragment, NymTopologyError> {
// each plain or repliable packet (i.e. not a reply) attaches an ephemeral public key so that the recipient let sender = self.sender_address;
// could perform diffie-hellman with its own keys followed by a kdf to re-derive
// the packet encryption key
let non_reply_overhead = encryption::PUBLIC_KEY_SIZE;
let expected_plaintext = fragment.serialized_size() + ACK_OVERHEAD + non_reply_overhead;
// the reason we're unwrapping (or rather 'expecting') here rather than handling the error <Self as FragmentPreparer>::prepare_chunk_for_sending(
// more gracefully is that this error should never be reached as it implies incorrect chunking self,
let packet_size = PacketSize::get_type_from_plaintext(expected_plaintext) fragment,
.expect("the message has been incorrectly fragmented"); topology,
ack_key,
let fragment_identifier = fragment.fragment_identifier(); &sender,
packet_recipient,
// create an ack )
let surb_ack = self.generate_surb_ack(fragment_identifier, topology, ack_key)?;
let ack_delay = surb_ack.expected_total_delay();
let packet_payload = NymsphinxPayloadBuilder::new(fragment, surb_ack)
.build_regular(&mut self.rng, packet_recipient.encryption_key());
// generate pseudorandom route for the packet
let route = topology.random_route_to_gateway(
&mut self.rng,
self.num_mix_hops,
packet_recipient.gateway(),
)?;
let destination = packet_recipient.as_sphinx_destination();
// including set of delays
let delays = delays::generate_from_average_duration(route.len(), self.average_packet_delay);
// create the actual sphinx packet here. With valid route and correct payload size,
// there's absolutely no reason for this call to fail.
let sphinx_packet = SphinxPacketBuilder::new()
.with_payload_size(packet_size.payload_size())
.build_packet(packet_payload, &route, &destination, &delays)
.unwrap();
// from the previously constructed route extract the first hop
let first_hop_address =
NymNodeRoutingAddress::try_from(route.first().unwrap().address).unwrap();
Ok(PreparedFragment {
// the round-trip delay is the sum of delays of all hops on the forward route as
// well as the total delay of the ack packet.
// note that the last hop of the packet is a gateway that does not do any delays
total_delay: delays.iter().take(delays.len() - 1).sum::<Delay>() + ack_delay,
mix_packet: MixPacket::new(first_hop_address, sphinx_packet, Default::default()),
fragment_identifier,
})
} }
/// Construct an acknowledgement SURB for the given [`FragmentIdentifier`] /// Construct an acknowledgement SURB for the given [`FragmentIdentifier`]
fn generate_surb_ack( pub fn generate_surb_ack(
&mut self, &mut self,
fragment_id: FragmentIdentifier, fragment_id: FragmentIdentifier,
topology: &NymTopology, topology: &NymTopology,
ack_key: &AckKey, ack_key: &AckKey,
) -> Result<SurbAck, NymTopologyError> { ) -> Result<SurbAck, NymTopologyError> {
SurbAck::construct( let sender = self.sender_address;
&mut self.rng, <Self as FragmentPreparer>::generate_surb_ack(self, &sender, fragment_id, topology, ack_key)
&self.sender_address,
ack_key,
fragment_id.to_bytes(),
self.average_ack_delay,
topology,
)
} }
pub fn pad_and_split_message( pub fn pad_and_split_message(
@@ -265,11 +360,27 @@ where
message: NymMessage, message: NymMessage,
packet_size: PacketSize, packet_size: PacketSize,
) -> Vec<Fragment> { ) -> Vec<Fragment> {
let plaintext_per_packet = message.available_sphinx_plaintext_per_packet(packet_size); <Self as FragmentPreparer>::pad_and_split_message(self, message, packet_size)
}
}
message impl<R: CryptoRng + Rng> FragmentPreparer for MessagePreparer<R> {
.pad_to_full_packet_lengths(plaintext_per_packet) type Rng = R;
.split_into_fragments(&mut self.rng, plaintext_per_packet)
fn rng(&mut self) -> &mut Self::Rng {
&mut self.rng
}
fn num_mix_hops(&self) -> u8 {
self.num_mix_hops
}
fn average_packet_delay(&self) -> Duration {
self.average_packet_delay
}
fn average_ack_delay(&self) -> Duration {
self.average_ack_delay
} }
} }
+2 -2
View File
@@ -263,7 +263,7 @@ mod message_receiver {
use nym_crypto::asymmetric::identity; use nym_crypto::asymmetric::identity;
use nym_mixnet_contract_common::Layer; use nym_mixnet_contract_common::Layer;
use nym_topology::{gateway, mix, NymTopology}; use nym_topology::{gateway, mix, NymTopology};
use std::collections::HashMap; use std::collections::BTreeMap;
// TODO: is it somehow maybe possible to move it to `topology` and have if conditionally // TODO: is it somehow maybe possible to move it to `topology` and have if conditionally
// available to other modules? // available to other modules?
@@ -271,7 +271,7 @@ mod message_receiver {
/// tests requiring instance of topology. /// tests requiring instance of topology.
#[allow(dead_code)] #[allow(dead_code)]
fn topology_fixture() -> NymTopology { fn topology_fixture() -> NymTopology {
let mut mixes = HashMap::new(); let mut mixes = BTreeMap::new();
mixes.insert( mixes.insert(
1, 1,
vec![mix::Node { vec![mix::Node {
+14 -5
View File
@@ -7,15 +7,10 @@ use crate::socks::{
authentication::{AuthenticationMethods, Authenticator, User}, authentication::{AuthenticationMethods, Authenticator, User},
server::SphinxSocksServer, server::SphinxSocksServer,
}; };
use futures::channel::mpsc; use futures::channel::mpsc;
use futures::StreamExt; use futures::StreamExt;
use log::*; use log::*;
use nym_bandwidth_controller::BandwidthController; use nym_bandwidth_controller::BandwidthController;
#[cfg(target_os = "android")]
use nym_client_core::client::base_client::helpers::setup_empty_reply_surb_backend;
#[cfg(not(target_os = "android"))]
use nym_client_core::client::base_client::non_wasm_helpers;
use nym_client_core::client::base_client::{ use nym_client_core::client::base_client::{
BaseClientBuilder, ClientInput, ClientOutput, ClientState, BaseClientBuilder, ClientInput, ClientOutput, ClientState,
}; };
@@ -28,6 +23,12 @@ use nym_validator_client::nyxd::QueryNyxdClient;
use nym_validator_client::Client; use nym_validator_client::Client;
use std::error::Error; use std::error::Error;
#[cfg(target_os = "android")]
use nym_client_core::client::base_client::helpers::setup_empty_reply_surb_backend;
#[cfg(not(target_os = "android"))]
use nym_client_core::client::base_client::non_wasm_helpers;
use nym_client_core::config::DebugConfig;
pub mod config; pub mod config;
pub mod error; pub mod error;
pub mod socks; pub mod socks;
@@ -102,6 +103,7 @@ impl NymClient {
pub fn start_socks5_listener( pub fn start_socks5_listener(
socks5_config: &Socks5, socks5_config: &Socks5,
debug_config: DebugConfig,
client_input: ClientInput, client_input: ClientInput,
client_output: ClientOutput, client_output: ClientOutput,
client_status: ClientState, client_status: ClientState,
@@ -126,6 +128,11 @@ impl NymClient {
.. ..
} = client_status; } = client_status;
let packet_size = debug_config
.traffic
.secondary_packet_size
.unwrap_or(debug_config.traffic.primary_packet_size);
let authenticator = Authenticator::new(auth_methods, allowed_users); let authenticator = Authenticator::new(auth_methods, allowed_users);
let mut sphinx_socks = SphinxSocksServer::new( let mut sphinx_socks = SphinxSocksServer::new(
socks5_config.get_listening_port(), socks5_config.get_listening_port(),
@@ -134,6 +141,7 @@ impl NymClient {
self_address, self_address,
shared_lane_queue_lengths, shared_lane_queue_lengths,
socks::client::Config::new( socks::client::Config::new(
packet_size,
socks5_config.get_provider_interface_version(), socks5_config.get_provider_interface_version(),
socks5_config.get_socks5_protocol_version(), socks5_config.get_socks5_protocol_version(),
socks5_config.get_send_anonymously(), socks5_config.get_send_anonymously(),
@@ -255,6 +263,7 @@ impl NymClient {
Self::start_socks5_listener( Self::start_socks5_listener(
self.config.get_socks5(), self.config.get_socks5(),
*self.config.get_debug_settings(),
client_input, client_input,
client_output, client_output,
client_state, client_state,
@@ -17,6 +17,7 @@ use nym_socks5_requests::{
ConnectionId, RemoteAddress, Socks5ProtocolVersion, Socks5ProviderRequest, Socks5Request, ConnectionId, RemoteAddress, Socks5ProtocolVersion, Socks5ProviderRequest, Socks5Request,
}; };
use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::params::PacketSize;
use nym_task::connections::{LaneQueueLengths, TransmissionLane}; use nym_task::connections::{LaneQueueLengths, TransmissionLane};
use nym_task::TaskClient; use nym_task::TaskClient;
use pin_project::pin_project; use pin_project::pin_project;
@@ -131,6 +132,7 @@ impl AsyncWrite for StreamState {
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub(crate) struct Config { pub(crate) struct Config {
biggest_packet_size: PacketSize,
provider_interface_version: ProviderInterfaceVersion, provider_interface_version: ProviderInterfaceVersion,
socks5_protocol_version: Socks5ProtocolVersion, socks5_protocol_version: Socks5ProtocolVersion,
use_surbs_for_responses: bool, use_surbs_for_responses: bool,
@@ -140,6 +142,7 @@ pub(crate) struct Config {
impl Config { impl Config {
pub(crate) fn new( pub(crate) fn new(
biggest_packet_size: PacketSize,
provider_interface_version: ProviderInterfaceVersion, provider_interface_version: ProviderInterfaceVersion,
socks5_protocol_version: Socks5ProtocolVersion, socks5_protocol_version: Socks5ProtocolVersion,
use_surbs_for_responses: bool, use_surbs_for_responses: bool,
@@ -147,6 +150,7 @@ impl Config {
per_request_surbs: u32, per_request_surbs: u32,
) -> Self { ) -> Self {
Self { Self {
biggest_packet_size,
provider_interface_version, provider_interface_version,
socks5_protocol_version, socks5_protocol_version,
use_surbs_for_responses, use_surbs_for_responses,
@@ -410,6 +414,9 @@ impl SocksClient {
remote_proxy_target, remote_proxy_target,
conn_receiver, conn_receiver,
input_sender, input_sender,
// FIXME: this does NOT include overhead due to acks or chunking
// (so actual true plaintext is smaller)
self.config.biggest_packet_size.plaintext_size(),
connection_id, connection_id,
Some(self.lane_queue_lengths.clone()), Some(self.lane_queue_lengths.clone()),
self.shutdown_listener.clone(), self.shutdown_listener.clone(),
@@ -1,208 +1,36 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net> // Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use bytes::{BufMut, Bytes, BytesMut}; use bytes::Bytes;
use futures::Stream; use futures::Stream;
use std::cell::RefCell;
use std::future::Future;
use std::io; use std::io;
use std::ops::DerefMut;
use std::pin::Pin; use std::pin::Pin;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use tokio::io::AsyncRead; use tokio::io::AsyncRead;
use tokio::time::{sleep, Duration, Instant, Sleep};
use tokio_util::io::poll_read_buf;
const MAX_READ_AMOUNT: usize = 500 * 1000; // 0.5MB // note, min_capacity doesn't mean we're going to always read at least this amount of data,
const GRACE_DURATION: Duration = Duration::from_millis(1); // it defines the smallest allowed (by yours truly) upper bound
const MIN_CAPACITY: usize = 16 * 1024;
const DEFAULT_CAPACITY: usize = 64 * 1024;
pub struct AvailableReader<'a, R: AsyncRead + Unpin> { pub struct AvailableReader<R> {
// TODO: come up with a way to avoid using RefCell (not sure if possible though due to having to inner: tokio_util::io::ReaderStream<R>,
// mutably borrow both inner reader and buffer at the same time)
buf: RefCell<BytesMut>,
inner: RefCell<&'a mut R>,
grace_period: Option<Pin<Box<Sleep>>>,
} }
impl<'a, R> AvailableReader<'a, R> impl<R: AsyncRead> AvailableReader<R> {
where pub fn new(reader: R, capacity: Option<usize>) -> Self {
R: AsyncRead + Unpin, let capacity = capacity.unwrap_or(DEFAULT_CAPACITY).max(MIN_CAPACITY);
{
const BUF_INCREMENT: usize = 4096;
pub fn new(reader: &'a mut R) -> Self {
AvailableReader { AvailableReader {
buf: RefCell::new(BytesMut::with_capacity(Self::BUF_INCREMENT)), inner: tokio_util::io::ReaderStream::with_capacity(reader, capacity),
inner: RefCell::new(reader),
grace_period: Some(Box::pin(sleep(GRACE_DURATION))),
} }
} }
} }
impl<'a, R: AsyncRead + Unpin> Stream for AvailableReader<'a, R> { impl<R: AsyncRead + Unpin> Stream for AvailableReader<R> {
type Item = io::Result<Bytes>; type Item = io::Result<Bytes>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
// if we have no space in buffer left - expand it Pin::new(&mut self.inner).poll_next(cx)
if !self.buf.borrow().has_remaining_mut() {
self.buf.borrow_mut().reserve(Self::BUF_INCREMENT);
}
// note: poll_read_buf calls `buf.advance_mut(n)`
let poll_res = poll_read_buf(
Pin::new(self.inner.borrow_mut().deref_mut()),
cx,
self.buf.borrow_mut().deref_mut(),
);
match poll_res {
Poll::Pending => {
// there's nothing for us here, just return whatever we have (assuming we read anything!)
if self.buf.borrow().is_empty() {
Poll::Pending
} else {
// if exists - check grace period
if let Some(grace_period) = self.grace_period.as_mut() {
if Pin::new(grace_period).poll(cx).is_pending() {
return Poll::Pending;
}
}
let buf = self.buf.replace(BytesMut::new());
Poll::Ready(Some(Ok(buf.freeze())))
}
}
Poll::Ready(Err(err)) => Poll::Ready(Some(Err(err))),
Poll::Ready(Ok(n)) => {
// if exists - reset grace period
if let Some(grace_period) = self.grace_period.as_mut() {
let now = Instant::now();
grace_period.as_mut().reset(now + GRACE_DURATION);
}
// if we read a non-0 amount, we're not done yet!
if n == 0 {
let buf = self.buf.replace(BytesMut::new());
if !buf.is_empty() {
Poll::Ready(Some(Ok(buf.freeze())))
} else {
Poll::Ready(None)
}
} else {
// tell the waker we should be polled again!
cx.waker().wake_by_ref();
// if we reached our maximum amount - return it
let read_bytes_len = self.buf.borrow().len();
if read_bytes_len >= MAX_READ_AMOUNT {
let buf = self.buf.replace(BytesMut::new());
return Poll::Ready(Some(Ok(buf.freeze())));
}
Poll::Pending
}
}
}
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use futures::{poll, StreamExt};
use std::io::Cursor;
use std::time::Duration;
use tokio::io::AsyncReadExt;
use tokio_test::assert_pending;
#[tokio::test]
async fn available_reader_reads_all_available_data_smaller_than_its_buf() {
let data = vec![42u8; 100];
let mut reader = Cursor::new(data.clone());
let mut available_reader = AvailableReader::new(&mut reader);
let read_data = available_reader.next().await.unwrap().unwrap();
assert_eq!(read_data, data);
assert!(available_reader.next().await.is_none());
}
#[tokio::test]
async fn available_reader_reads_all_available_data_bigger_than_its_buf() {
let data = vec![42u8; AvailableReader::<Cursor<Vec<u8>>>::BUF_INCREMENT + 100];
let mut reader = Cursor::new(data.clone());
let mut available_reader = AvailableReader::new(&mut reader);
let read_data = available_reader.next().await.unwrap().unwrap();
assert_eq!(read_data, data);
assert!(available_reader.next().await.is_none());
}
#[tokio::test]
async fn available_reader_will_not_wait_for_more_data_if_it_already_has_some() {
let first_data_chunk = vec![42u8; 100];
let second_data_chunk = vec![123u8; 100];
let mut reader_mock = tokio_test::io::Builder::new()
.read(&first_data_chunk)
.wait(Duration::from_millis(100)) // delay is irrelevant, what matters is that we don't get everything immediately
.read(&second_data_chunk)
.build();
let mut available_reader = AvailableReader::new(&mut reader_mock);
let read_data = available_reader.next().await.unwrap().unwrap();
assert_eq!(read_data, first_data_chunk);
assert_pending!(poll!(available_reader.next()));
// before dropping the mock, we need to empty it
let mut buf = vec![0u8; second_data_chunk.len()];
assert_eq!(reader_mock.read(&mut buf).await.unwrap(), 100);
}
#[tokio::test]
async fn available_reader_will_wait_for_more_data_if_it_doesnt_have_anything() {
let data = vec![42u8; 100];
let mut reader_mock = tokio_test::io::Builder::new()
.wait(Duration::from_millis(100))
.read(&data)
.build();
let mut available_reader = AvailableReader::new(&mut reader_mock);
let read_data = available_reader.next().await.unwrap().unwrap();
assert_eq!(read_data, data);
assert!(available_reader.next().await.is_none());
}
// perhaps the issue of tokio io builder will be resolved in tokio 0.3?
// #[tokio::test]
// async fn available_reader_will_wait_for_more_data_if_its_within_grace_period() {
// let first_data_chunk = vec![42u8; 100];
// let second_data_chunk = vec![123u8; 100];
//
// let combined_chunks: Vec<_> = first_data_chunk
// .iter()
// .cloned()
// .chain(second_data_chunk.iter().cloned())
// .collect();
//
// let mut reader_mock = tokio_test::io::Builder::new()
// .read(&first_data_chunk)
// .wait(Duration::from_millis(2))
// .read(&second_data_chunk)
// .build();
//
// let mut available_reader = AvailableReader {
// buf: RefCell::new(BytesMut::with_capacity(4096)),
// inner: RefCell::new(&mut reader_mock),
// grace_period: Some(delay_for(Duration::from_millis(5))),
// };
//
// let read_data = available_reader.next().await.unwrap().unwrap();
//
// assert_eq!(read_data, combined_chunks);
// assert!(available_reader.next().await.is_none())
// }
}
@@ -4,6 +4,7 @@
use super::MixProxySender; use super::MixProxySender;
use super::SHUTDOWN_TIMEOUT; use super::SHUTDOWN_TIMEOUT;
use crate::available_reader::AvailableReader; use crate::available_reader::AvailableReader;
use crate::proxy_runner::KEEPALIVE_INTERVAL;
use bytes::Bytes; use bytes::Bytes;
use futures::FutureExt; use futures::FutureExt;
use futures::StreamExt; use futures::StreamExt;
@@ -35,6 +36,23 @@ async fn send_empty_close<F, S>(
.expect("BatchRealMessageReceiver has stopped receiving!"); .expect("BatchRealMessageReceiver has stopped receiving!");
} }
async fn send_empty_keepalive<F, S>(
connection_id: ConnectionId,
message_sender: &mut OrderedMessageSender,
mix_sender: &MixProxySender<S>,
adapter_fn: F,
) where
F: Fn(ConnectionId, Vec<u8>, bool) -> S,
S: Debug,
{
log::trace!("Sending keepalive for connection: {connection_id}");
let ordered_msg = message_sender.wrap_message(Vec::new()).into_bytes();
mix_sender
.send(adapter_fn(connection_id, ordered_msg, false))
.await
.expect("BatchRealMessageReceiver has stopped receiving!");
}
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
async fn deal_with_data<F, S>( async fn deal_with_data<F, S>(
read_data: Option<io::Result<Bytes>>, read_data: Option<io::Result<Bytes>>,
@@ -167,6 +185,7 @@ pub(super) async fn run_inbound<F, S>(
remote_source_address: String, remote_source_address: String,
connection_id: ConnectionId, connection_id: ConnectionId,
mix_sender: MixProxySender<S>, mix_sender: MixProxySender<S>,
available_plaintext_per_mix_packet: usize,
adapter_fn: F, adapter_fn: F,
shutdown_notify: Arc<Notify>, shutdown_notify: Arc<Notify>,
lane_queue_lengths: Option<LaneQueueLengths>, lane_queue_lengths: Option<LaneQueueLengths>,
@@ -176,12 +195,16 @@ where
F: Fn(ConnectionId, Vec<u8>, bool) -> S + Send + 'static, F: Fn(ConnectionId, Vec<u8>, bool) -> S + Send + 'static,
S: Debug, S: Debug,
{ {
let mut available_reader = AvailableReader::new(&mut reader); // TODO: this multiplication by 4 is completely arbitrary here
let mut available_reader =
AvailableReader::new(&mut reader, Some(available_plaintext_per_mix_packet * 4));
let mut message_sender = OrderedMessageSender::new(); let mut message_sender = OrderedMessageSender::new();
let shutdown_future = shutdown_notify.notified().then(|_| sleep(SHUTDOWN_TIMEOUT)); let shutdown_future = shutdown_notify.notified().then(|_| sleep(SHUTDOWN_TIMEOUT));
tokio::pin!(shutdown_future); tokio::pin!(shutdown_future);
let mut keepalive_timer = tokio::time::interval(KEEPALIVE_INTERVAL);
loop { loop {
select! { select! {
read_data = &mut available_reader.next() => { read_data = &mut available_reader.next() => {
@@ -197,6 +220,10 @@ where
).await { ).await {
break break
} }
keepalive_timer.reset();
}
_ = keepalive_timer.tick() => {
send_empty_keepalive(connection_id, &mut message_sender, &mix_sender, &adapter_fn).await;
} }
_ = &mut shutdown_future => { _ = &mut shutdown_future => {
debug!( debug!(
@@ -1,4 +1,4 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net> // Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use crate::connection_controller::ConnectionReceiver; use crate::connection_controller::ConnectionReceiver;
@@ -15,6 +15,10 @@ mod outbound;
// TODO: make this configurable // TODO: make this configurable
const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(30); const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(30);
// Send empty keepalive messages regurarly to keep the connection alive. This should be smaller
// than [`MIX_TTL`].
const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(60);
#[derive(Debug)] #[derive(Debug)]
pub struct ProxyMessage { pub struct ProxyMessage {
pub data: Vec<u8>, pub data: Vec<u8>,
@@ -49,6 +53,8 @@ pub struct ProxyRunner<S> {
connection_id: ConnectionId, connection_id: ConnectionId,
lane_queue_lengths: Option<LaneQueueLengths>, lane_queue_lengths: Option<LaneQueueLengths>,
available_plaintext_per_mix_packet: usize,
// Listens to shutdown commands from higher up // Listens to shutdown commands from higher up
shutdown_listener: TaskClient, shutdown_listener: TaskClient,
} }
@@ -64,6 +70,7 @@ where
remote_source_address: String, remote_source_address: String,
mix_receiver: ConnectionReceiver, mix_receiver: ConnectionReceiver,
mix_sender: MixProxySender<S>, mix_sender: MixProxySender<S>,
available_plaintext_per_mix_packet: usize,
connection_id: ConnectionId, connection_id: ConnectionId,
lane_queue_lengths: Option<LaneQueueLengths>, lane_queue_lengths: Option<LaneQueueLengths>,
shutdown_listener: TaskClient, shutdown_listener: TaskClient,
@@ -76,6 +83,7 @@ where
remote_source_address, remote_source_address,
connection_id, connection_id,
lane_queue_lengths, lane_queue_lengths,
available_plaintext_per_mix_packet,
shutdown_listener, shutdown_listener,
} }
} }
@@ -96,6 +104,7 @@ where
self.remote_source_address.clone(), self.remote_source_address.clone(),
self.connection_id, self.connection_id,
self.mix_sender.clone(), self.mix_sender.clone(),
self.available_plaintext_per_mix_packet,
adapter_fn, adapter_fn,
Arc::clone(&shutdown_notify), Arc::clone(&shutdown_notify),
self.lane_queue_lengths.clone(), self.lane_queue_lengths.clone(),

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