Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| deae210b82 | |||
| 5b2b45a6eb | |||
| 896a3e1be6 | |||
| 1eaa13155c | |||
| 534187cc8f | |||
| 25b4934f69 | |||
| 5ef7e24893 | |||
| 87ef46bc05 | |||
| 68ca41a6be | |||
| a1a5c7772d | |||
| f8d68d8ef0 | |||
| a9d86508b5 | |||
| bb7fa587de | |||
| 6585732dfc | |||
| 2065e0fc17 | |||
| 3f7bdad59c | |||
| 6209e78c1e | |||
| 8e5062af96 | |||
| 496e642d7f | |||
| 07e18ec198 | |||
| d5953c28c1 | |||
| 3aa4b66588 | |||
| 005f0ce340 | |||
| 8d1d025fa2 | |||
| 1d53a2f954 | |||
| 966d123608 | |||
| 963d55273f | |||
| 6872d7bf77 | |||
| 6fe93bcda0 | |||
| 78d568e04e | |||
| 8880bdd857 | |||
| cc83ecf7e4 | |||
| 796f5a678a | |||
| 00b60f5493 | |||
| 0dfd1cca44 | |||
| eacefd3697 | |||
| 424c25768c | |||
| e460c1700e | |||
| 9935c99d41 | |||
| 2c4aee63bf | |||
| 0ebc395df9 | |||
| 1de3317e75 | |||
| 91c20af893 | |||
| 1365e2f246 | |||
| cfa1ce46f2 | |||
| 3f4f76859b | |||
| 3f7f4b82de | |||
| 934ba2b027 | |||
| 221e1870e5 | |||
| 9b36bccf0c | |||
| 80d7285497 | |||
| b94f2a483d | |||
| f64cfb4cd1 | |||
| eda223ed3d | |||
| c98d4305fa | |||
| 2eecbca6eb | |||
| a58c80ef08 | |||
| ac9d0db8be | |||
| 7521d98963 | |||
| 1e98131090 | |||
| 46bf65462c | |||
| e3df4c2d68 | |||
| 45c013350f | |||
| 30e2f27c64 | |||
| 6fecc53975 | |||
| e4dbfb1904 | |||
| 3113c1e9a7 | |||
| f822d3db7b | |||
| 9d23766288 | |||
| fd4930b198 | |||
| 5d7be89edb | |||
| 47f5b4ceac | |||
| 790220039b | |||
| 16fdfa4583 | |||
| 1d8a931e0c | |||
| 48d0883b31 | |||
| e271370326 | |||
| cbbeb66b5b | |||
| de020f46a6 | |||
| f24bb5c038 | |||
| 79dfe7eeda | |||
| 0108c6ed19 | |||
| 0e8f60d501 | |||
| 6e30e6178b |
+17
-22
@@ -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
|
|
||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
+50
-7
@@ -4,14 +4,57 @@ 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.18] (2023-05-09)
|
||||||
- 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
|
- Implement heartbeat messages between socks5 proxy and network requester ([#3215])
|
||||||
[#3226]: https://github.com/nymtech/nym/pull/3226
|
|
||||||
[#3255]: https://github.com/nymtech/nym/pull/3255
|
[#3215]: https://github.com/nymtech/nym/issues/3215
|
||||||
[#3273]: https://github.com/nymtech/nym/pull/3273
|
|
||||||
|
## [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
+98
-18
@@ -103,6 +103,17 @@ version = "1.0.70"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
|
checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argon2"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "95c2fcf79ad1932ac6269a738109997a83c227c09b75842ae564dc8ede6a861c"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"blake2 0.10.6",
|
||||||
|
"password-hash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayref"
|
name = "arrayref"
|
||||||
version = "0.3.7"
|
version = "0.3.7"
|
||||||
@@ -366,6 +377,15 @@ dependencies = [
|
|||||||
"opaque-debug 0.2.3",
|
"opaque-debug 0.2.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blake2"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||||
|
dependencies = [
|
||||||
|
"digest 0.10.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "blake3"
|
name = "blake3"
|
||||||
version = "1.3.3"
|
version = "1.3.3"
|
||||||
@@ -1576,7 +1596,7 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "explorer-api"
|
name = "explorer-api"
|
||||||
version = "1.1.15"
|
version = "1.1.17"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap 4.1.11",
|
"clap 4.1.11",
|
||||||
@@ -2757,7 +2777,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "4ae926706ba42c425c9457121178330d75e273df2e82e28b758faf3de3a9acb9"
|
checksum = "4ae926706ba42c425c9457121178330d75e273df2e82e28b758faf3de3a9acb9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayref",
|
"arrayref",
|
||||||
"blake2",
|
"blake2 0.8.1",
|
||||||
"chacha",
|
"chacha",
|
||||||
"keystream",
|
"keystream",
|
||||||
]
|
]
|
||||||
@@ -3011,7 +3031,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-api"
|
name = "nym-api"
|
||||||
version = "1.1.16"
|
version = "1.1.18"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -3047,7 +3067,9 @@ dependencies = [
|
|||||||
"nym-inclusion-probability",
|
"nym-inclusion-probability",
|
||||||
"nym-mixnet-contract-common",
|
"nym-mixnet-contract-common",
|
||||||
"nym-multisig-contract-common",
|
"nym-multisig-contract-common",
|
||||||
|
"nym-node-tester-utils",
|
||||||
"nym-pemstore",
|
"nym-pemstore",
|
||||||
|
"nym-service-provider-directory-common",
|
||||||
"nym-sphinx",
|
"nym-sphinx",
|
||||||
"nym-task",
|
"nym-task",
|
||||||
"nym-topology",
|
"nym-topology",
|
||||||
@@ -3142,7 +3164,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-cli"
|
name = "nym-cli"
|
||||||
version = "1.1.15"
|
version = "1.1.17"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.13.1",
|
"base64 0.13.1",
|
||||||
@@ -3189,6 +3211,7 @@ dependencies = [
|
|||||||
"nym-mixnet-contract-common",
|
"nym-mixnet-contract-common",
|
||||||
"nym-multisig-contract-common",
|
"nym-multisig-contract-common",
|
||||||
"nym-network-defaults",
|
"nym-network-defaults",
|
||||||
|
"nym-service-provider-directory-common",
|
||||||
"nym-validator-client",
|
"nym-validator-client",
|
||||||
"nym-vesting-contract-common",
|
"nym-vesting-contract-common",
|
||||||
"rand 0.6.5",
|
"rand 0.6.5",
|
||||||
@@ -3203,7 +3226,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-client"
|
name = "nym-client"
|
||||||
version = "1.1.15"
|
version = "1.1.17"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap 4.1.11",
|
"clap 4.1.11",
|
||||||
"dirs",
|
"dirs",
|
||||||
@@ -3415,7 +3438,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-crypto"
|
name = "nym-crypto"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes 0.8.2",
|
"aes 0.8.2",
|
||||||
"blake3",
|
"blake3",
|
||||||
@@ -3436,6 +3459,7 @@ dependencies = [
|
|||||||
"subtle-encoding",
|
"subtle-encoding",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"x25519-dalek",
|
"x25519-dalek",
|
||||||
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3471,7 +3495,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-gateway"
|
name = "nym-gateway"
|
||||||
version = "1.1.15"
|
version = "1.1.17"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -3600,7 +3624,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-mixnet-contract-common"
|
name = "nym-mixnet-contract-common"
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bs58",
|
"bs58",
|
||||||
"cosmwasm-std",
|
"cosmwasm-std",
|
||||||
@@ -3619,7 +3643,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-mixnode"
|
name = "nym-mixnode"
|
||||||
version = "1.1.16"
|
version = "1.1.18"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bs58",
|
"bs58",
|
||||||
@@ -3721,7 +3745,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-network-requester"
|
name = "nym-network-requester"
|
||||||
version = "1.1.15"
|
version = "1.1.17"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-file-watcher",
|
"async-file-watcher",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -3763,7 +3787,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-network-statistics"
|
name = "nym-network-statistics"
|
||||||
version = "1.1.15"
|
version = "1.1.17"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs",
|
"dirs",
|
||||||
"log",
|
"log",
|
||||||
@@ -3778,6 +3802,24 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nym-node-tester-utils"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"futures",
|
||||||
|
"log",
|
||||||
|
"nym-crypto",
|
||||||
|
"nym-sphinx",
|
||||||
|
"nym-task",
|
||||||
|
"nym-topology",
|
||||||
|
"rand 0.7.3",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"wasm-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-nonexhaustive-delayqueue"
|
name = "nym-nonexhaustive-delayqueue"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -3858,6 +3900,7 @@ name = "nym-service-provider-directory-common"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cosmwasm-std",
|
"cosmwasm-std",
|
||||||
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3879,7 +3922,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-socks5-client"
|
name = "nym-socks5-client"
|
||||||
version = "1.1.15"
|
version = "1.1.17"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap 4.1.11",
|
"clap 4.1.11",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
@@ -3975,6 +4018,7 @@ dependencies = [
|
|||||||
"nym-sphinx-forwarding",
|
"nym-sphinx-forwarding",
|
||||||
"nym-sphinx-framing",
|
"nym-sphinx-framing",
|
||||||
"nym-sphinx-params",
|
"nym-sphinx-params",
|
||||||
|
"nym-sphinx-routing",
|
||||||
"nym-sphinx-types",
|
"nym-sphinx-types",
|
||||||
"nym-topology",
|
"nym-topology",
|
||||||
"rand 0.7.3",
|
"rand 0.7.3",
|
||||||
@@ -4084,6 +4128,15 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nym-sphinx-routing"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"nym-sphinx-addressing",
|
||||||
|
"nym-sphinx-types",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-sphinx-types"
|
name = "nym-sphinx-types"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -4105,6 +4158,20 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nym-store-cipher"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"aes-gcm",
|
||||||
|
"argon2",
|
||||||
|
"getrandom 0.2.8",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-task"
|
name = "nym-task"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -4128,6 +4195,7 @@ dependencies = [
|
|||||||
"nym-crypto",
|
"nym-crypto",
|
||||||
"nym-mixnet-contract-common",
|
"nym-mixnet-contract-common",
|
||||||
"nym-sphinx-addressing",
|
"nym-sphinx-addressing",
|
||||||
|
"nym-sphinx-routing",
|
||||||
"nym-sphinx-types",
|
"nym-sphinx-types",
|
||||||
"rand 0.7.3",
|
"rand 0.7.3",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
@@ -4186,6 +4254,7 @@ dependencies = [
|
|||||||
"nym-mixnet-contract-common",
|
"nym-mixnet-contract-common",
|
||||||
"nym-multisig-contract-common",
|
"nym-multisig-contract-common",
|
||||||
"nym-network-defaults",
|
"nym-network-defaults",
|
||||||
|
"nym-service-provider-directory-common",
|
||||||
"nym-vesting-contract",
|
"nym-vesting-contract",
|
||||||
"nym-vesting-contract-common",
|
"nym-vesting-contract-common",
|
||||||
"prost 0.10.4",
|
"prost 0.10.4",
|
||||||
@@ -4202,7 +4271,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-vesting-contract"
|
name = "nym-vesting-contract"
|
||||||
version = "1.3.1"
|
version = "1.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cosmwasm-derive",
|
"cosmwasm-derive",
|
||||||
"cosmwasm-std",
|
"cosmwasm-std",
|
||||||
@@ -4220,7 +4289,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-vesting-contract-common"
|
name = "nym-vesting-contract-common"
|
||||||
version = "0.5.0"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cosmwasm-std",
|
"cosmwasm-std",
|
||||||
"nym-contracts-common",
|
"nym-contracts-common",
|
||||||
@@ -4452,6 +4521,17 @@ dependencies = [
|
|||||||
"windows-sys 0.45.0",
|
"windows-sys 0.45.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "password-hash"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"subtle 2.4.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
@@ -5873,7 +5953,7 @@ checksum = "cc43eda802856ee82a7555c7b75ceb9e07451741c7a2f5f23d036020e01189d4"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"aes 0.7.5",
|
"aes 0.7.5",
|
||||||
"arrayref",
|
"arrayref",
|
||||||
"blake2",
|
"blake2 0.8.1",
|
||||||
"bs58",
|
"bs58",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"chacha",
|
"chacha",
|
||||||
@@ -5896,7 +5976,7 @@ source = "git+https://github.com/nymtech/sphinx.git#ca107d94360cdf8bbfbdb12fe532
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"aes 0.7.5",
|
"aes 0.7.5",
|
||||||
"arrayref",
|
"arrayref",
|
||||||
"blake2",
|
"blake2 0.8.1",
|
||||||
"bs58",
|
"bs58",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"chacha",
|
"chacha",
|
||||||
@@ -7450,9 +7530,9 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zeroize"
|
name = "zeroize"
|
||||||
version = "1.5.7"
|
version = "1.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f"
|
checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zeroize_derive",
|
"zeroize_derive",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -48,6 +48,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 +60,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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nym-client"
|
name = "nym-client"
|
||||||
version = "1.1.15"
|
version = "1.1.18"
|
||||||
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,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nym-socks5-client"
|
name = "nym-socks5-client"
|
||||||
version = "1.1.15"
|
version = "1.1.18"
|
||||||
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"
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[build]
|
||||||
|
target = "wasm32-unknown-unknown"
|
||||||
@@ -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" }
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
@@ -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>
|
||||||
@@ -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 },
|
||||||
|
};
|
||||||
@@ -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
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -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)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ nym-vesting-contract-common = { path = "../../cosmwasm-smart-contracts/vesting-c
|
|||||||
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-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 +64,10 @@ 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"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
nyxd-client = [
|
nyxd-client = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
|||||||
@@ -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:#?}");
|
||||||
|
}
|
||||||
@@ -117,7 +117,7 @@ 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::debug!("Checking: nyxd url: {url}: {}: {error}", "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),
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ 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>,
|
||||||
// TODO: add this in later commits
|
// TODO: add this in later commits
|
||||||
// pub(crate) gas_price: GasPrice,
|
// pub(crate) gas_price: GasPrice,
|
||||||
}
|
}
|
||||||
@@ -131,6 +132,13 @@ 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,
|
||||||
|
)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,6 +254,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 +316,11 @@ 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()
|
||||||
|
}
|
||||||
|
|
||||||
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,20 @@ 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;
|
||||||
|
|
||||||
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 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 sp_directory_signing_client::SpDirectorySigningClient;
|
||||||
pub use vesting_signing_client::VestingSigningClient;
|
pub use vesting_signing_client::VestingSigningClient;
|
||||||
|
|||||||
@@ -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 exectute_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.exectute_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.exectute_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.exectute_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.exectute_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 exectute_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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cosmwasm-std = { workspace = true }
|
cosmwasm-std = { workspace = true }
|
||||||
|
schemars = "0.8"
|
||||||
serde = { workspace = true, default-features = false, features = ["derive"] }
|
serde = { workspace = true, default-features = false, 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"
|
||||||
|
|||||||
@@ -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" }
|
||||||
|
|||||||
@@ -3,7 +3,12 @@
|
|||||||
|
|
||||||
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;
|
||||||
@@ -28,6 +33,7 @@ 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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 +74,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 +131,9 @@ 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,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_mainnet() -> Self {
|
pub fn new_mainnet() -> Self {
|
||||||
@@ -146,6 +163,9 @@ 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: parse_optional_str(
|
||||||
|
mainnet::SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,6 +247,15 @@ 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
#[derive(Debug, Copy, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ 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 SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS: &str =
|
||||||
|
"n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0";
|
||||||
pub(crate) const _ETH_CONTRACT_ADDRESS: [u8; 20] =
|
pub(crate) const _ETH_CONTRACT_ADDRESS: [u8; 20] =
|
||||||
hex_literal::hex!("0000000000000000000000000000000000000000");
|
hex_literal::hex!("0000000000000000000000000000000000000000");
|
||||||
pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] =
|
pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] =
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ 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 NYXD: &str = "NYXD";
|
pub const NYXD: &str = "NYXD";
|
||||||
pub const NYM_API: &str = "NYM_API";
|
pub const NYM_API: &str = "NYM_API";
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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)*)}
|
||||||
|
}};
|
||||||
|
}
|
||||||
@@ -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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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" }
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "nym-store-cipher"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
aes-gcm = { version = "0.10.1" }
|
||||||
|
argon2 = { version = "0.5.0" }
|
||||||
|
rand = "0.8.5"
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
serde_json = { workspace = true, optional = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
zeroize = { version = "1.6.0", features = ["zeroize_derive"] }
|
||||||
|
|
||||||
|
[target.'cfg(target_env = "wasm32-unknown-unknown")'.dependencies]
|
||||||
|
getrandom = { version = "0.2", features = ["js"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
json = ["serde_json"]
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use aes_gcm::aead::{Aead, Nonce};
|
||||||
|
use aes_gcm::{AeadCore, AeadInPlace, Key, KeyInit, KeySizeUser};
|
||||||
|
use rand::{thread_rng, CryptoRng, Fill, RngCore};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_helpers::{argon2_algorithm_helper, argon2_params_helper, argon2_version_helper};
|
||||||
|
use thiserror::Error;
|
||||||
|
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||||
|
|
||||||
|
pub use aes_gcm::Aes256Gcm;
|
||||||
|
pub use argon2::{Algorithm, Argon2, Params, Version};
|
||||||
|
|
||||||
|
mod serde_helpers;
|
||||||
|
|
||||||
|
pub const CURRENT_VERSION: u8 = 1;
|
||||||
|
pub const ARGON2_SALT_SIZE: usize = 16;
|
||||||
|
pub const AES256GCM_NONCE_SIZE: usize = 12;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("failed to encrypt/decrypt provided data: {cause}")]
|
||||||
|
AesFailure { cause: aes_gcm::Error },
|
||||||
|
|
||||||
|
#[error("failed to expand the passphrase: {cause}")]
|
||||||
|
Argon2Failure { cause: argon2::Error },
|
||||||
|
|
||||||
|
#[cfg(feature = "json")]
|
||||||
|
#[error("failed to serialize/deserialize JSON: {source}")]
|
||||||
|
SerdeJsonFailure {
|
||||||
|
#[from]
|
||||||
|
source: serde_json::Error,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("failed to generate random bytes: {source}")]
|
||||||
|
RandomError {
|
||||||
|
#[from]
|
||||||
|
source: rand::Error,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("the received ciphertext was encrypted with different store version ({received}). The current version is {CURRENT_VERSION}")]
|
||||||
|
VersionMismatch { received: u8 },
|
||||||
|
}
|
||||||
|
|
||||||
|
// it's weird that this couldn't be auto-derived with a `#[from]`...
|
||||||
|
impl From<aes_gcm::Error> for Error {
|
||||||
|
fn from(cause: aes_gcm::Error) -> Self {
|
||||||
|
Error::AesFailure { cause }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<argon2::Error> for Error {
|
||||||
|
fn from(cause: argon2::Error) -> Self {
|
||||||
|
Error::Argon2Failure { cause }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum KdfInfo {
|
||||||
|
Argon2 {
|
||||||
|
/// The Argon2 parameters that were used when deriving the store key.
|
||||||
|
#[serde(with = "argon2_params_helper")]
|
||||||
|
params: Params,
|
||||||
|
|
||||||
|
/// The specific Argon2 algorithm variant used when deriving the store key.
|
||||||
|
#[serde(with = "argon2_algorithm_helper")]
|
||||||
|
algorithm: Algorithm,
|
||||||
|
|
||||||
|
/// The specific version of the Argon2 algorithm used when deriving the store key.
|
||||||
|
#[serde(with = "argon2_version_helper")]
|
||||||
|
version: Version,
|
||||||
|
|
||||||
|
/// The salt that was used when the passphrase was expanded into a store key.
|
||||||
|
kdf_salt: [u8; ARGON2_SALT_SIZE],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KdfInfo {
|
||||||
|
pub fn expand_key<C>(&self, passphrase: &[u8]) -> Result<Key<C>, Error>
|
||||||
|
where
|
||||||
|
C: KeySizeUser,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
KdfInfo::Argon2 {
|
||||||
|
params,
|
||||||
|
algorithm,
|
||||||
|
version,
|
||||||
|
kdf_salt,
|
||||||
|
} => argon2_derive_cipher_key::<C>(
|
||||||
|
passphrase,
|
||||||
|
kdf_salt,
|
||||||
|
&[],
|
||||||
|
params.clone(),
|
||||||
|
*algorithm,
|
||||||
|
*version,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_default_settings() -> Result<Self, Error> {
|
||||||
|
let kdf_salt = Self::random_salt()?;
|
||||||
|
Ok(KdfInfo::Argon2 {
|
||||||
|
params: Default::default(),
|
||||||
|
algorithm: Default::default(),
|
||||||
|
version: Default::default(),
|
||||||
|
kdf_salt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn random_salt() -> Result<[u8; ARGON2_SALT_SIZE], Error> {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
Self::random_salt_with_rng(&mut rng)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn random_salt_with_rng<R: RngCore + CryptoRng>(
|
||||||
|
rng: &mut R,
|
||||||
|
) -> Result<[u8; ARGON2_SALT_SIZE], Error> {
|
||||||
|
let mut salt = [0u8; ARGON2_SALT_SIZE];
|
||||||
|
salt.try_fill(rng)?;
|
||||||
|
Ok(salt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Zeroize, ZeroizeOnDrop)]
|
||||||
|
pub struct StoreCipher<C = Aes256Gcm>
|
||||||
|
where
|
||||||
|
C: KeySizeUser,
|
||||||
|
{
|
||||||
|
key: Key<C>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: KeySizeUser + KeyInit> StoreCipher<C>
|
||||||
|
where
|
||||||
|
C: KeySizeUser + KeyInit,
|
||||||
|
{
|
||||||
|
pub fn new(passphrase: &[u8], kdf_info: KdfInfo) -> Result<Self, Error> {
|
||||||
|
let key = kdf_info.expand_key::<C>(passphrase)?;
|
||||||
|
Ok(StoreCipher { key })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_default_kdf(passphrase: &[u8]) -> Result<Self, Error> {
|
||||||
|
let kdf_info = KdfInfo::new_with_default_settings()?;
|
||||||
|
Self::new(passphrase, kdf_info)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "json")]
|
||||||
|
pub fn encrypt_json_value<T: Serialize>(&self, data: &T) -> Result<EncryptedData, Error>
|
||||||
|
where
|
||||||
|
C: AeadInPlace,
|
||||||
|
{
|
||||||
|
let raw = serde_json::to_vec(data)?;
|
||||||
|
self.encrypt_data(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unless you know what you're doing, use `Self::encrypt_data` instead.
|
||||||
|
// As the caller of this method needs to make sure to correctly dispose of the original plaintext.
|
||||||
|
pub fn encrypt_data_ref(&self, data: &[u8]) -> Result<EncryptedData, Error>
|
||||||
|
where
|
||||||
|
C: Aead,
|
||||||
|
{
|
||||||
|
let nonce = Self::random_nonce()?;
|
||||||
|
|
||||||
|
let cipher = C::new(&self.key);
|
||||||
|
let ciphertext = cipher.encrypt(&nonce, data)?;
|
||||||
|
|
||||||
|
Ok(EncryptedData {
|
||||||
|
version: CURRENT_VERSION,
|
||||||
|
ciphertext,
|
||||||
|
nonce: nonce.to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encrypt_data(&self, mut data: Vec<u8>) -> Result<EncryptedData, Error>
|
||||||
|
where
|
||||||
|
C: AeadInPlace,
|
||||||
|
{
|
||||||
|
let nonce = Self::random_nonce()?;
|
||||||
|
|
||||||
|
let cipher = C::new(&self.key);
|
||||||
|
cipher.encrypt_in_place(&nonce, &[], &mut data)?;
|
||||||
|
|
||||||
|
Ok(EncryptedData {
|
||||||
|
version: CURRENT_VERSION,
|
||||||
|
ciphertext: data,
|
||||||
|
nonce: nonce.to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "json")]
|
||||||
|
pub fn decrypt_json_value<T: serde::de::DeserializeOwned>(
|
||||||
|
&self,
|
||||||
|
data: EncryptedData,
|
||||||
|
) -> Result<T, Error>
|
||||||
|
where
|
||||||
|
C: AeadInPlace,
|
||||||
|
{
|
||||||
|
let mut plaintext = self.decrypt_data(data)?;
|
||||||
|
|
||||||
|
// DO NOT USE `?` here!!
|
||||||
|
// We need to make sure to zeroize the plaintext even if deserialization fails!!
|
||||||
|
let value = serde_json::from_slice(&plaintext);
|
||||||
|
|
||||||
|
plaintext.zeroize();
|
||||||
|
Ok(value?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt_data_unchecked(&self, data: EncryptedData) -> Result<Vec<u8>, Error>
|
||||||
|
where
|
||||||
|
C: Aead,
|
||||||
|
{
|
||||||
|
let cipher = C::new(&self.key);
|
||||||
|
let plaintext = cipher.decrypt(
|
||||||
|
Nonce::<C>::from_slice(&data.nonce),
|
||||||
|
data.ciphertext.as_ref(),
|
||||||
|
)?;
|
||||||
|
Ok(plaintext)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt_data(&self, data: EncryptedData) -> Result<Vec<u8>, Error>
|
||||||
|
where
|
||||||
|
C: Aead,
|
||||||
|
{
|
||||||
|
if data.version != CURRENT_VERSION {
|
||||||
|
return Err(Error::VersionMismatch {
|
||||||
|
received: data.version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.decrypt_data_unchecked(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn random_nonce() -> Result<Nonce<C>, Error>
|
||||||
|
where
|
||||||
|
C: AeadCore,
|
||||||
|
{
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
Self::random_nonce_with_rng(&mut rng)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn random_nonce_with_rng<R: RngCore + CryptoRng>(rng: &mut R) -> Result<Nonce<C>, Error>
|
||||||
|
where
|
||||||
|
C: AeadCore,
|
||||||
|
{
|
||||||
|
let mut nonce = Nonce::<C>::default();
|
||||||
|
nonce.try_fill(rng)?;
|
||||||
|
Ok(nonce)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
|
||||||
|
pub struct EncryptedData {
|
||||||
|
pub version: u8,
|
||||||
|
pub ciphertext: Vec<u8>,
|
||||||
|
pub nonce: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn argon2_derive_cipher_key<C>(
|
||||||
|
passphrase: &[u8],
|
||||||
|
salt: &[u8],
|
||||||
|
pepper: &[u8],
|
||||||
|
params: Params,
|
||||||
|
algorithm: Algorithm,
|
||||||
|
version: Version,
|
||||||
|
) -> Result<Key<C>, Error>
|
||||||
|
where
|
||||||
|
C: KeySizeUser,
|
||||||
|
{
|
||||||
|
let argon2 = if pepper.is_empty() {
|
||||||
|
Argon2::new(algorithm, version, params)
|
||||||
|
} else {
|
||||||
|
Argon2::new_with_secret(pepper, algorithm, version, params)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut key = Key::<C>::default();
|
||||||
|
argon2.hash_password_into(passphrase, salt, &mut key)?;
|
||||||
|
|
||||||
|
Ok(key)
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
pub(crate) mod argon2_params_helper {
|
||||||
|
use argon2::Params;
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
|
||||||
|
/// Refer to [argon2::Params] for details.
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct Argon2Params {
|
||||||
|
m_cost: u32,
|
||||||
|
t_cost: u32,
|
||||||
|
p_cost: u32,
|
||||||
|
output_len: Option<usize>,
|
||||||
|
// Note: `keyid` and `data` are not longer part of the argon2 standard
|
||||||
|
// (see: <https://github.com/P-H-C/phc-winner-argon2/pull/173>), and should
|
||||||
|
// not be used for any non-legacy work.
|
||||||
|
// So we're explicitly skipping them for serialization
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Params> for Argon2Params {
|
||||||
|
fn from(value: &Params) -> Self {
|
||||||
|
Argon2Params {
|
||||||
|
m_cost: value.m_cost(),
|
||||||
|
t_cost: value.t_cost(),
|
||||||
|
p_cost: value.p_cost(),
|
||||||
|
output_len: value.output_len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Argon2Params> for Params {
|
||||||
|
type Error = argon2::Error;
|
||||||
|
|
||||||
|
fn try_from(value: Argon2Params) -> Result<Self, Self::Error> {
|
||||||
|
Params::new(value.m_cost, value.t_cost, value.p_cost, value.output_len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize<S: Serializer>(params: &Params, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
Argon2Params::from(params).serialize(serializer)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Params, D::Error> {
|
||||||
|
<Argon2Params>::deserialize(deserializer)?
|
||||||
|
.try_into()
|
||||||
|
.map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) mod argon2_algorithm_helper {
|
||||||
|
use argon2::Algorithm;
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
|
||||||
|
/// Refer to [argon2::Algorithm] for details.
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
enum Argon2Algorithm {
|
||||||
|
Argon2d = 0,
|
||||||
|
Argon2i = 1,
|
||||||
|
Argon2id = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Algorithm> for Argon2Algorithm {
|
||||||
|
fn from(value: Algorithm) -> Self {
|
||||||
|
match value {
|
||||||
|
Algorithm::Argon2d => Argon2Algorithm::Argon2d,
|
||||||
|
Algorithm::Argon2i => Argon2Algorithm::Argon2i,
|
||||||
|
Algorithm::Argon2id => Argon2Algorithm::Argon2id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Argon2Algorithm> for Algorithm {
|
||||||
|
fn from(value: Argon2Algorithm) -> Self {
|
||||||
|
match value {
|
||||||
|
Argon2Algorithm::Argon2d => Algorithm::Argon2d,
|
||||||
|
Argon2Algorithm::Argon2i => Algorithm::Argon2i,
|
||||||
|
Argon2Algorithm::Argon2id => Algorithm::Argon2id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize<S: Serializer>(
|
||||||
|
algorithm: &Algorithm,
|
||||||
|
serializer: S,
|
||||||
|
) -> Result<S::Ok, S::Error> {
|
||||||
|
Argon2Algorithm::from(*algorithm).serialize(serializer)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Algorithm, D::Error> {
|
||||||
|
<Argon2Algorithm>::deserialize(deserializer).map(From::from)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) mod argon2_version_helper {
|
||||||
|
use argon2::Version;
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
|
||||||
|
/// Refer to [argon2::Version] for details.
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[repr(u32)]
|
||||||
|
enum Argon2Version {
|
||||||
|
V0x10 = 0x10,
|
||||||
|
V0x13 = 0x13,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Version> for Argon2Version {
|
||||||
|
fn from(value: Version) -> Self {
|
||||||
|
match value {
|
||||||
|
Version::V0x10 => Argon2Version::V0x10,
|
||||||
|
Version::V0x13 => Argon2Version::V0x13,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Argon2Version> for Version {
|
||||||
|
fn from(value: Argon2Version) -> Self {
|
||||||
|
match value {
|
||||||
|
Argon2Version::V0x10 => Version::V0x10,
|
||||||
|
Argon2Version::V0x13 => Version::V0x13,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize<S: Serializer>(algorithm: &Version, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
Argon2Version::from(*algorithm).serialize(serializer)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Version, D::Error> {
|
||||||
|
<Argon2Version>::deserialize(deserializer).map(From::from)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,4 +11,4 @@ pub use manager::{StatusReceiver, StatusSender, TaskClient, TaskManager};
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub use signal::wait_for_signal_and_error;
|
pub use signal::wait_for_signal_and_error;
|
||||||
|
|
||||||
pub use spawn::spawn_with_report_error;
|
pub use spawn::{spawn, spawn_with_report_error};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::TaskClient;
|
|||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub(crate) fn spawn<F>(future: F)
|
pub fn spawn<F>(future: F)
|
||||||
where
|
where
|
||||||
F: Future<Output = ()> + 'static,
|
F: Future<Output = ()> + 'static,
|
||||||
{
|
{
|
||||||
@@ -10,7 +10,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub(crate) fn spawn<F>(future: F)
|
pub fn spawn<F>(future: F)
|
||||||
where
|
where
|
||||||
F: Future + Send + 'static,
|
F: Future + Send + 'static,
|
||||||
F::Output: Send + 'static,
|
F::Output: Send + 'static,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ nym-crypto = { path = "../crypto" }
|
|||||||
nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" }
|
nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" }
|
||||||
nym-sphinx-addressing = { path = "../nymsphinx/addressing" }
|
nym-sphinx-addressing = { path = "../nymsphinx/addressing" }
|
||||||
nym-sphinx-types = { path = "../nymsphinx/types" }
|
nym-sphinx-types = { path = "../nymsphinx/types" }
|
||||||
|
nym-sphinx-routing = { path = "../nymsphinx/routing" }
|
||||||
nym-bin-common = { path = "../bin-common" }
|
nym-bin-common = { path = "../bin-common" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::MixLayer;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Error)]
|
||||||
|
pub enum NymTopologyError {
|
||||||
|
#[error("The provided network topology is empty - there are no mixnodes and no gateways on it - the network request(s) probably failed")]
|
||||||
|
EmptyNetworkTopology,
|
||||||
|
|
||||||
|
#[error("The provided network topology has no gateways available")]
|
||||||
|
NoGatewaysAvailable,
|
||||||
|
|
||||||
|
#[error("The provided network topology has no mixnodes available")]
|
||||||
|
NoMixnodesAvailable,
|
||||||
|
|
||||||
|
#[error("Gateway with identity key {identity_key} doesn't exist")]
|
||||||
|
NonExistentGatewayError { identity_key: String },
|
||||||
|
|
||||||
|
#[error("Wanted to create a mix route with {requested} hops, while only {available} layers are available")]
|
||||||
|
InvalidNumberOfHopsError { available: usize, requested: usize },
|
||||||
|
|
||||||
|
#[error("No mixnodes available on layer {layer}")]
|
||||||
|
EmptyMixLayer { layer: MixLayer },
|
||||||
|
|
||||||
|
#[error("Uneven layer distribution. Layer {layer} has {nodes} on it, while we expected a value between {lower_bound} and {upper_bound} as we have {total_nodes} nodes in total. Full breakdown: {layer_distribution:?}")]
|
||||||
|
UnevenLayerDistribution {
|
||||||
|
layer: MixLayer,
|
||||||
|
nodes: usize,
|
||||||
|
lower_bound: usize,
|
||||||
|
upper_bound: usize,
|
||||||
|
total_nodes: usize,
|
||||||
|
layer_distribution: Vec<(MixLayer, usize)>,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
use nym_bin_common::version_checker;
|
use nym_bin_common::version_checker;
|
||||||
use std::collections::HashMap;
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
|
|
||||||
pub trait Versioned: Clone {
|
pub trait Versioned: Clone {
|
||||||
@@ -40,3 +40,16 @@ where
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T, K, V> VersionFilterable<T> for BTreeMap<K, V>
|
||||||
|
where
|
||||||
|
K: Eq + Ord + Clone,
|
||||||
|
V: VersionFilterable<T>,
|
||||||
|
T: Versioned,
|
||||||
|
{
|
||||||
|
fn filter_by_version(&self, expected_version: &str) -> Self {
|
||||||
|
self.iter()
|
||||||
|
.map(|(k, v)| (k.clone(), v.filter_by_version(expected_version)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,6 +42,26 @@ pub struct Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Node {
|
impl Node {
|
||||||
|
pub fn parse_host(raw: &str) -> Result<NetworkAddress, GatewayConversionError> {
|
||||||
|
raw.parse()
|
||||||
|
.map_err(|err| GatewayConversionError::InvalidAddress {
|
||||||
|
value: raw.to_owned(),
|
||||||
|
source: err,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_mix_host(
|
||||||
|
host: &NetworkAddress,
|
||||||
|
mix_port: u16,
|
||||||
|
) -> Result<SocketAddr, GatewayConversionError> {
|
||||||
|
Ok(host.to_socket_addrs(mix_port).map_err(|err| {
|
||||||
|
GatewayConversionError::InvalidAddress {
|
||||||
|
value: host.to_string(),
|
||||||
|
source: err,
|
||||||
|
}
|
||||||
|
})?[0])
|
||||||
|
}
|
||||||
|
|
||||||
pub fn identity(&self) -> &NodeIdentity {
|
pub fn identity(&self) -> &NodeIdentity {
|
||||||
&self.identity_key
|
&self.identity_key
|
||||||
}
|
}
|
||||||
@@ -81,23 +101,11 @@ impl<'a> TryFrom<&'a GatewayBond> for Node {
|
|||||||
type Error = GatewayConversionError;
|
type Error = GatewayConversionError;
|
||||||
|
|
||||||
fn try_from(bond: &'a GatewayBond) -> Result<Self, Self::Error> {
|
fn try_from(bond: &'a GatewayBond) -> Result<Self, Self::Error> {
|
||||||
let host: NetworkAddress =
|
let host = Self::parse_host(&bond.gateway.host)?;
|
||||||
bond.gateway
|
|
||||||
.host
|
|
||||||
.parse()
|
|
||||||
.map_err(|err| GatewayConversionError::InvalidAddress {
|
|
||||||
value: bond.gateway.host.clone(),
|
|
||||||
source: err,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// try to completely resolve the host in the mix situation to avoid doing it every
|
// try to completely resolve the host in the mix situation to avoid doing it every
|
||||||
// single time we want to construct a path
|
// single time we want to construct a path
|
||||||
let mix_host = host.to_socket_addrs(bond.gateway.mix_port).map_err(|err| {
|
let mix_host = Self::extract_mix_host(&host, bond.gateway.mix_port)?;
|
||||||
GatewayConversionError::InvalidAddress {
|
|
||||||
value: bond.gateway.host.clone(),
|
|
||||||
source: err,
|
|
||||||
}
|
|
||||||
})?[0];
|
|
||||||
|
|
||||||
Ok(Node {
|
Ok(Node {
|
||||||
owner: bond.owner.as_str().to_owned(),
|
owner: bond.owner.as_str().to_owned(),
|
||||||
|
|||||||
+48
-42
@@ -1,58 +1,30 @@
|
|||||||
// 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 crate::filter::VersionFilterable;
|
use crate::filter::VersionFilterable;
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use nym_mixnet_contract_common::mixnode::MixNodeDetails;
|
use nym_mixnet_contract_common::mixnode::MixNodeDetails;
|
||||||
use nym_mixnet_contract_common::GatewayBond;
|
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId};
|
||||||
use nym_sphinx_addressing::nodes::NodeIdentity;
|
use nym_sphinx_addressing::nodes::NodeIdentity;
|
||||||
use nym_sphinx_types::Node as SphinxNode;
|
use nym_sphinx_types::Node as SphinxNode;
|
||||||
use rand::{CryptoRng, Rng};
|
use rand::{CryptoRng, Rng};
|
||||||
use std::collections::HashMap;
|
use std::collections::BTreeMap;
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::fmt::{self, Display, Formatter};
|
use std::fmt::{self, Display, Formatter};
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
|
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
pub mod filter;
|
pub mod filter;
|
||||||
pub mod gateway;
|
pub mod gateway;
|
||||||
pub mod mix;
|
pub mod mix;
|
||||||
|
pub mod random_route_provider;
|
||||||
|
|
||||||
#[cfg(feature = "provider-trait")]
|
#[cfg(feature = "provider-trait")]
|
||||||
pub mod provider_trait;
|
pub mod provider_trait;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Error)]
|
pub use error::NymTopologyError;
|
||||||
pub enum NymTopologyError {
|
|
||||||
#[error("The provided network topology is empty - there are no mixnodes and no gateways on it - the network request(s) probably failed")]
|
|
||||||
EmptyNetworkTopology,
|
|
||||||
|
|
||||||
#[error("The provided network topology has no gateways available")]
|
|
||||||
NoGatewaysAvailable,
|
|
||||||
|
|
||||||
#[error("The provided network topology has no mixnodes available")]
|
|
||||||
NoMixnodesAvailable,
|
|
||||||
|
|
||||||
#[error("Gateway with identity key {identity_key} doesn't exist")]
|
|
||||||
NonExistentGatewayError { identity_key: String },
|
|
||||||
|
|
||||||
#[error("Wanted to create a mix route with {requested} hops, while only {available} layers are available")]
|
|
||||||
InvalidNumberOfHopsError { available: usize, requested: usize },
|
|
||||||
|
|
||||||
#[error("No mixnodes available on layer {layer}")]
|
|
||||||
EmptyMixLayer { layer: MixLayer },
|
|
||||||
|
|
||||||
#[error("Uneven layer distribution. Layer {layer} has {nodes} on it, while we expected a value between {lower_bound} and {upper_bound} as we have {total_nodes} nodes in total. Full breakdown: {layer_distribution:?}")]
|
|
||||||
UnevenLayerDistribution {
|
|
||||||
layer: MixLayer,
|
|
||||||
nodes: usize,
|
|
||||||
lower_bound: usize,
|
|
||||||
upper_bound: usize,
|
|
||||||
total_nodes: usize,
|
|
||||||
layer_distribution: Vec<(MixLayer, usize)>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum NetworkAddress {
|
pub enum NetworkAddress {
|
||||||
@@ -96,16 +68,51 @@ pub type MixLayer = u8;
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct NymTopology {
|
pub struct NymTopology {
|
||||||
mixes: HashMap<MixLayer, Vec<mix::Node>>,
|
mixes: BTreeMap<MixLayer, Vec<mix::Node>>,
|
||||||
gateways: Vec<gateway::Node>,
|
gateways: Vec<gateway::Node>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NymTopology {
|
impl NymTopology {
|
||||||
pub fn new(mixes: HashMap<MixLayer, Vec<mix::Node>>, gateways: Vec<gateway::Node>) -> Self {
|
pub fn new(mixes: BTreeMap<MixLayer, Vec<mix::Node>>, gateways: Vec<gateway::Node>) -> Self {
|
||||||
NymTopology { mixes, gateways }
|
NymTopology { mixes, gateways }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mixes(&self) -> &HashMap<MixLayer, Vec<mix::Node>> {
|
pub fn from_detailed(
|
||||||
|
mix_details: Vec<MixNodeDetails>,
|
||||||
|
gateway_bonds: Vec<GatewayBond>,
|
||||||
|
) -> Self {
|
||||||
|
nym_topology_from_detailed(mix_details, gateway_bonds)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_mix(&self, mix_id: MixId) -> Option<&mix::Node> {
|
||||||
|
for nodes in self.mixes.values() {
|
||||||
|
for node in nodes {
|
||||||
|
if node.mix_id == mix_id {
|
||||||
|
return Some(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_mix_by_identity(&self, mixnode_identity: IdentityKeyRef) -> Option<&mix::Node> {
|
||||||
|
for nodes in self.mixes.values() {
|
||||||
|
for node in nodes {
|
||||||
|
if node.identity_key.to_base58_string() == mixnode_identity {
|
||||||
|
return Some(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_gateway(&self, gateway_identity: IdentityKeyRef) -> Option<&gateway::Node> {
|
||||||
|
self.gateways
|
||||||
|
.iter()
|
||||||
|
.find(|&gateway| gateway.identity_key.to_base58_string() == gateway_identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mixes(&self) -> &BTreeMap<MixLayer, Vec<mix::Node>> {
|
||||||
&self.mixes
|
&self.mixes
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,8 +160,7 @@ impl NymTopology {
|
|||||||
num_mix_hops: u8,
|
num_mix_hops: u8,
|
||||||
) -> Result<Vec<SphinxNode>, NymTopologyError>
|
) -> Result<Vec<SphinxNode>, NymTopologyError>
|
||||||
where
|
where
|
||||||
// I don't think there's a need for this RNG to be crypto-secure
|
R: Rng + CryptoRng + ?Sized,
|
||||||
R: Rng + ?Sized,
|
|
||||||
{
|
{
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
|
|
||||||
@@ -308,7 +314,7 @@ pub fn nym_topology_from_detailed(
|
|||||||
mix_details: Vec<MixNodeDetails>,
|
mix_details: Vec<MixNodeDetails>,
|
||||||
gateway_bonds: Vec<GatewayBond>,
|
gateway_bonds: Vec<GatewayBond>,
|
||||||
) -> NymTopology {
|
) -> NymTopology {
|
||||||
let mut mixes = HashMap::new();
|
let mut mixes = BTreeMap::new();
|
||||||
for bond in mix_details
|
for bond in mix_details
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|details| details.bond_information)
|
.map(|details| details.bond_information)
|
||||||
@@ -389,7 +395,7 @@ mod converting_mixes_to_vec {
|
|||||||
..node1.clone()
|
..node1.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut mixes: HashMap<MixLayer, Vec<mix::Node>> = HashMap::new();
|
let mut mixes: BTreeMap<MixLayer, Vec<mix::Node>> = BTreeMap::new();
|
||||||
mixes.insert(1, vec![node1, node2]);
|
mixes.insert(1, vec![node1, node2]);
|
||||||
mixes.insert(2, vec![node3]);
|
mixes.insert(2, vec![node3]);
|
||||||
|
|
||||||
@@ -405,7 +411,7 @@ mod converting_mixes_to_vec {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn returns_an_empty_vec() {
|
fn returns_an_empty_vec() {
|
||||||
let topology = NymTopology::new(HashMap::new(), vec![]);
|
let topology = NymTopology::new(BTreeMap::new(), vec![]);
|
||||||
let mixvec = topology.mixes_as_vec();
|
let mixvec = topology.mixes_as_vec();
|
||||||
assert!(mixvec.is_empty());
|
assert!(mixvec.is_empty());
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-14
@@ -42,6 +42,28 @@ pub struct Node {
|
|||||||
pub version: String,
|
pub version: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Node {
|
||||||
|
pub fn parse_host(raw: &str) -> Result<NetworkAddress, MixnodeConversionError> {
|
||||||
|
raw.parse()
|
||||||
|
.map_err(|err| MixnodeConversionError::InvalidAddress {
|
||||||
|
value: raw.to_owned(),
|
||||||
|
source: err,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_mix_host(
|
||||||
|
host: &NetworkAddress,
|
||||||
|
mix_port: u16,
|
||||||
|
) -> Result<SocketAddr, MixnodeConversionError> {
|
||||||
|
Ok(host.to_socket_addrs(mix_port).map_err(|err| {
|
||||||
|
MixnodeConversionError::InvalidAddress {
|
||||||
|
value: host.to_string(),
|
||||||
|
source: err,
|
||||||
|
}
|
||||||
|
})?[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl filter::Versioned for Node {
|
impl filter::Versioned for Node {
|
||||||
fn version(&self) -> String {
|
fn version(&self) -> String {
|
||||||
self.version.clone()
|
self.version.clone()
|
||||||
@@ -62,23 +84,11 @@ impl<'a> TryFrom<&'a MixNodeBond> for Node {
|
|||||||
type Error = MixnodeConversionError;
|
type Error = MixnodeConversionError;
|
||||||
|
|
||||||
fn try_from(bond: &'a MixNodeBond) -> Result<Self, Self::Error> {
|
fn try_from(bond: &'a MixNodeBond) -> Result<Self, Self::Error> {
|
||||||
let host: NetworkAddress =
|
let host = Self::parse_host(&bond.mix_node.host)?;
|
||||||
bond.mix_node
|
|
||||||
.host
|
|
||||||
.parse()
|
|
||||||
.map_err(|err| MixnodeConversionError::InvalidAddress {
|
|
||||||
value: bond.mix_node.host.clone(),
|
|
||||||
source: err,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// try to completely resolve the host in the mix situation to avoid doing it every
|
// try to completely resolve the host in the mix situation to avoid doing it every
|
||||||
// single time we want to construct a path
|
// single time we want to construct a path
|
||||||
let mix_host = host
|
let mix_host = Self::extract_mix_host(&host, bond.mix_node.mix_port)?;
|
||||||
.to_socket_addrs(bond.mix_node.mix_port)
|
|
||||||
.map_err(|err| MixnodeConversionError::InvalidAddress {
|
|
||||||
value: bond.mix_node.host.clone(),
|
|
||||||
source: err,
|
|
||||||
})?[0];
|
|
||||||
|
|
||||||
Ok(Node {
|
Ok(Node {
|
||||||
mix_id: bond.mix_id,
|
mix_id: bond.mix_id,
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::{NymTopology, NymTopologyError};
|
||||||
|
use nym_sphinx_addressing::clients::Recipient;
|
||||||
|
use nym_sphinx_routing::SphinxRouteMaker;
|
||||||
|
use nym_sphinx_types::Node;
|
||||||
|
use rand::{CryptoRng, Rng};
|
||||||
|
|
||||||
|
pub struct NymTopologyRouteProvider<R> {
|
||||||
|
rng: R,
|
||||||
|
inner: NymTopology,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> SphinxRouteMaker for NymTopologyRouteProvider<R>
|
||||||
|
where
|
||||||
|
R: Rng + CryptoRng,
|
||||||
|
{
|
||||||
|
type Error = NymTopologyError;
|
||||||
|
|
||||||
|
fn sphinx_route(
|
||||||
|
&mut self,
|
||||||
|
hops: u8,
|
||||||
|
destination: &Recipient,
|
||||||
|
) -> Result<Vec<Node>, NymTopologyError> {
|
||||||
|
self.inner
|
||||||
|
.random_route_to_gateway(&mut self.rng, hops, destination.gateway())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ macro_rules! console_log {
|
|||||||
($($t:tt)*) => ($crate::log(&format_args!($($t)*).to_string()))
|
($($t:tt)*) => ($crate::log(&format_args!($($t)*).to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// will cause messages to be written as if console.warm("...") was called
|
// will cause messages to be written as if console.warn("...") was called
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! console_warn {
|
macro_rules! console_warn {
|
||||||
($($t:tt)*) => ($crate::warn(&format_args!($($t)*).to_string()))
|
($($t:tt)*) => ($crate::warn(&format_args!($($t)*).to_string()))
|
||||||
@@ -50,3 +50,76 @@ pub async fn sleep(ms: i32) -> Result<(), JsValue> {
|
|||||||
js_fut.await?;
|
js_fut.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A helper that construct a `JsValue` containing an error with the provided message.
|
||||||
|
pub fn simple_js_error<S: AsRef<str>>(message: S) -> JsValue {
|
||||||
|
let js_error = js_sys::Error::new(message.as_ref());
|
||||||
|
JsValue::from(js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! js_error {
|
||||||
|
($($t:tt)*) => {{
|
||||||
|
let js_error = js_sys::Error::new(&format!($($t)*));
|
||||||
|
wasm_bindgen::JsValue::from(js_error)
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maps provided `Result`'s inner values into a pair of `JsValue` that can be returned
|
||||||
|
/// inside a promise (and in particular from inside `future_to_promise`)
|
||||||
|
pub fn into_promise_result<T, E>(res: Result<T, E>) -> Result<JsValue, JsValue>
|
||||||
|
where
|
||||||
|
T: Into<JsValue>,
|
||||||
|
E: Into<JsValue>,
|
||||||
|
{
|
||||||
|
res.map(Into::into).map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn map_promise_err<T, E>(res: Result<T, E>) -> Result<T, JsValue>
|
||||||
|
where
|
||||||
|
E: Into<JsValue>,
|
||||||
|
{
|
||||||
|
res.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait PromisableResult {
|
||||||
|
fn into_promise_result(self) -> Result<JsValue, JsValue>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this should probably get renamed : )
|
||||||
|
pub trait PromisableResultError {
|
||||||
|
type Ok;
|
||||||
|
|
||||||
|
fn map_promise_err(self) -> Result<Self::Ok, JsValue>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, E> PromisableResult for Result<T, E>
|
||||||
|
where
|
||||||
|
T: Into<JsValue>,
|
||||||
|
E: Into<JsValue>,
|
||||||
|
{
|
||||||
|
fn into_promise_result(self) -> Result<JsValue, JsValue> {
|
||||||
|
into_promise_result(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, E> PromisableResultError for Result<T, E>
|
||||||
|
where
|
||||||
|
E: Into<JsValue>,
|
||||||
|
{
|
||||||
|
type Ok = T;
|
||||||
|
|
||||||
|
fn map_promise_err(self) -> Result<T, JsValue> {
|
||||||
|
map_promise_err(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! check_promise_result {
|
||||||
|
( $x:expr ) => {
|
||||||
|
match $crate::PromisableResultError::map_promise_err($x) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(err) => return js_sys::Promise::reject(&err),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,17 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## [v1.4.0] (2023-04-25)
|
||||||
|
- Allow mixnode operators to decrease their bond amount without having to rebond (will require a lot of testing EXACT reward values to make sure the "unit delegation" isn't broken afterwards) ([#3233])
|
||||||
|
- Fix a few clippy warnings in contract test code ([#3340])
|
||||||
|
- Add --all-targets to clippy for contracts ([#3337])
|
||||||
|
- A branch with all clippy warnings dealt with in contracts ([#3294])
|
||||||
|
|
||||||
|
[#3233]: https://github.com/nymtech/nym/issues/3233
|
||||||
|
[#3340]: https://github.com/nymtech/nym/pull/3340
|
||||||
|
[#3337]: https://github.com/nymtech/nym/pull/3337
|
||||||
|
[#3294]: https://github.com/nymtech/nym/pull/3294
|
||||||
|
|
||||||
## [v1.3.1] (2023-04-18)
|
## [v1.3.1] (2023-04-18)
|
||||||
- Add a query to the vesting contract for the amount of delegated tokens towards a particular `mix_id` (might be needed by NG) ([#3228])
|
- Add a query to the vesting contract for the amount of delegated tokens towards a particular `mix_id` (might be needed by NG) ([#3228])
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user