Compare commits

...

36 Commits

Author SHA1 Message Date
Tommy Verrall 3a80b9bf59 update branch for latest changes 2023-04-25 12:53:45 +02:00
Raphaël Walther 3f7f4b82de Move workflows to custom runner 2023-04-25 11:10:12 +02:00
farbanas 934ba2b027 Merge branch 'master' into develop 2023-04-25 10:53:43 +02:00
Jędrzej Stuczyński eda223ed3d Resolved beta clippy complaints (#3351) 2023-04-25 09:53:11 +01:00
farbanas c98d4305fa update cargo locks 2023-04-25 10:09:49 +02:00
farbanas 2eecbca6eb bump versions and update changelogs for release v1.1.16 2023-04-25 10:06:03 +02:00
farbanas a58c80ef08 update versions of mixnet and vesting contract crates 2023-04-25 10:06:03 +02:00
farbanas ac9d0db8be update versions of mixnet and vesting contract crates 2023-04-25 10:06:03 +02:00
farbanas 7521d98963 update versions of mixnet and vesting contract common crates 2023-04-25 10:06:03 +02:00
mx 1e98131090 Merge pull request #3349 from nymtech/feature/general-docs-updates
version bump for next release
2023-04-25 07:39:10 +00:00
mx 46bf65462c Merge pull request #3325 from esomore/mixnode/description
update mix-node setup docs with node description
2023-04-25 07:38:22 +00:00
mx e3df4c2d68 reintroduce minimum rust version variable 2023-04-24 17:13:03 +02:00
mx 45c013350f version bump for next release 2023-04-24 17:10:41 +02:00
mx 6fecc53975 Merge pull request #3339 from nymtech/feature/coconut-rust-sdk-docs
added coconut credential generation example
2023-04-24 14:49:24 +00:00
Tommy Verrall e4dbfb1904 Merge pull request #3222 from nymtech/feature/available_reader_changes
Feature/available reader changes
2023-04-24 15:48:08 +01:00
Tommy Verrall f822d3db7b cargo fmt 2023-04-24 15:29:51 +01:00
Jędrzej Stuczyński 9d23766288 updated used packet size 2023-04-24 15:29:51 +01:00
Jędrzej Stuczyński fd4930b198 removed old leftover log statement 2023-04-24 15:29:51 +01:00
Jędrzej Stuczyński 5d7be89edb replaced inner implementation with tokio's 'ReaderStream' 2023-04-24 15:29:51 +01:00
Jędrzej Stuczyński 47f5b4ceac limit the maximum buffer size of AvailableReader by PacketSize of our mix packets 2023-04-24 15:29:51 +01:00
Jędrzej Stuczyński 790220039b added read deadline to AvailableReader 2023-04-24 15:29:51 +01:00
Tommy Verrall 16fdfa4583 Update mainnet.env 2023-04-24 13:54:54 +02:00
Jędrzej Stuczyński cbbeb66b5b Feature/wasm client topology injection (#3311)
* added cargo config file to explicitly specify build target

* wip

* Config option to disable topology refreshing

* extracted common parsing code

* helper trait for working on wasm topology

* wasm topology parsing

* restored (slightly modified) old js-example

* wip

* Moved message preparation into a trait

* wip

* long-winded way of sending test packet

* standalone NymNodeTester

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

* initial round of cleanup

* sending multiple test packets in normal NymClient

* javascript-side cleanup

* starting mixnode test on btn click

* Improved NymNodeTester constructors

* improved error handling and constructors

* tester utils error handling

* further cleanup + using BTreeMap for NymTopology mixnodes

* handling missed errors

* splitting up 'test_node'

* split up and cleaned up generation of test result

* clippy + fixed example

* post rebase fixes

* another broken test

* prevent running multiple parallel tests

* cargo fmt

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

* Add @octol to codeowners

* added @mfahampshire as owner of /documentation

---------

Co-authored-by: Jon Häggblad <jon.haggblad@gmail.com>
2023-04-20 15:25:23 +01:00
Jon Häggblad 79dfe7eeda Add clippy target in top-level Makefile 2023-04-20 14:13:20 +02:00
Jon Häggblad 0108c6ed19 Merge remote-tracking branch 'origin/release/v1.1.16' into develop 2023-04-20 09:55:19 +02:00
Itamar Perez 0e8f60d501 update mix-node setup docs with node description 2023-04-19 12:01:36 -07:00
Jon Häggblad 6e30e6178b Update Cargo.lock files after bumping internal versions during release 2023-04-19 09:37:38 +02:00
durch 237597da90 Remove dependency 2023-04-12 14:33:01 +02:00
durch 8000100735 Log to $HOME/.nym/logs 2023-04-12 13:53:43 +02:00
durch 68b8993e84 Merge branch 'feature/cpu-mixnode-build' of https://github.com/nymtech/nym into feature/cpu-mixnode-build 2023-04-12 09:21:58 +02:00
durch 3a897ed08e Log if measurement is ON or OFF 2023-04-12 09:21:51 +02:00
Tommy Verrall 18a24fc10d update cpu-cycles 2023-04-12 09:20:30 +02:00
Tommy Verrall dfcf812243 change the binary build 2023-04-12 09:19:11 +02:00
Tommy Verrall bdc91bb324 build only the mixnode binary with cpu-cycles 2023-04-06 10:08:38 +02:00
86 changed files with 5414 additions and 634 deletions
+17 -22
View File
@@ -11,30 +11,25 @@
# In each subsection folders are ordered first by depth, then alphabetically.
# This should make it easy to add new rules without breaking existing ones.
# Something weird not covered by anything else
* @futurechimp @mmsinclair
# contracts
/contracts/mixnet @durch @jstuczyn
/contracts/vesting @durch @jstuczyn
/contracts/service-provider-directory @octol
# Rust rules:
*.rs @durch @futurechimp @jstuczyn @neacsu @octol
Cargo.* @durch @futurechimp @jstuczyn @neacsu @octol
# crypto code
/common/crypto/ @jstuczyn
/common/nymcoconut/ @jstuczyn
/common/dkg/ @jstuczyn
/common/nymsphinx/ @jstuczyn
# JS rules:
*.js @mmsinclair @fmtabbara
*.ts @mmsinclair @fmtabbara
*.tsx @mmsinclair @fmtabbara
*.jsx @mmsinclair @fmtabbara
# rust sdk
/sdk/rust/ @octol
# Something looking like possible documentation rules:
*.md @mfahampshire
# nym-connect (rust)
/nym-connect/desktop/src-tauri/ @octol
# our docker scripts
/docker/ @neacsu
# nym-wallet (rust)
/nym-wallet/src-tauri/ @octol
# if there are any changes in the core crypto, I feel like Ania should take a look:
/common/crypto/ @aniampio
/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
# documentation
/documentation @mfahampshire
@@ -4,34 +4,11 @@ on:
workflow_dispatch:
push:
paths:
- 'clients/**'
- 'common/**'
- 'contracts/**'
- 'explorer-api/**'
- 'gateway/**'
- 'integrations/**'
- 'mixnode/**'
- 'sdk/rust/nym-sdk/**'
- 'service-providers/**'
- 'nym-api/**'
- 'nym-outfox/**'
- 'tools/nym-cli/**'
- 'tools/ts-rs-cli/**'
pull_request:
paths:
- 'clients/**'
- 'common/**'
- 'contracts/**'
- 'explorer-api/**'
- 'gateway/**'
- 'integrations/**'
- 'mixnode/**'
- 'sdk/rust/nym-sdk/**'
- 'service-providers/**'
- 'nym-api/**'
- 'nym-outfox/**'
- 'tools/nym-cli/**'
- 'tools/ts-rs-cli/**'
env:
NETWORK: mainnet
@@ -65,11 +42,11 @@ jobs:
with:
toolchain: stable
- name: Build all binaries
- name: Build all mixnode binary
uses: actions-rs/cargo@v1
with:
command: build
args: --workspace --release --all
args: --manifest-path mixnode/Cargo.toml --release --features cpucycles
- name: Install Rust stable
uses: actions-rs/toolchain@v1
@@ -79,34 +56,12 @@ jobs:
override: true
components: rustfmt, clippy
- name: Install wasm-opt
run: cargo install --version 0.112.0 wasm-opt
- name: Build release contracts
run: make wasm
- name: Prepare build output
shell: bash
env:
OUTPUT_DIR: ci-builds/${{ github.ref_name }}
run: |
cp target/release/nym-client $OUTPUT_DIR
cp target/release/nym-gateway $OUTPUT_DIR
cp target/release/nym-mixnode $OUTPUT_DIR
cp target/release/nym-socks5-client $OUTPUT_DIR
cp target/release/nym-api $OUTPUT_DIR
cp target/release/nym-network-requester $OUTPUT_DIR
cp target/release/nym-network-statistics $OUTPUT_DIR
cp target/release/nym-cli $OUTPUT_DIR
cp target/release/nym-credential-client $OUTPUT_DIR
cp target/release/explorer-api $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/mixnet_contract.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/nym_coconut_bandwidth.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/nym_coconut_dkg.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/cw3_flex_multisig.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/cw4_group.wasm $OUTPUT_DIR
- name: Deploy branch to CI www
continue-on-error: true
@@ -6,7 +6,7 @@
},
{
"os":"windows-latest",
"os":"windows10",
"rust":"stable",
"runOnEvent":"schedule"
},
@@ -22,7 +22,7 @@
"runOnEvent":"schedule"
},
{
"os":"windows-latest",
"os":"windows10",
"rust":"beta",
"runOnEvent":"schedule"
},
@@ -38,7 +38,7 @@
"runOnEvent":"schedule"
},
{
"os":"windows-latest",
"os":"windows10",
"rust":"nightly",
"runOnEvent":"schedule"
},
+22 -7
View File
@@ -4,14 +4,29 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
- nym-network-statistics properly handles signals ([#3209])
- add socks5 support for Rust SDK ([#3226], [#3255])
- add coconut bandwidth credential support for Rust SDK ([#3273])
## [v1.1.16] (2023-04-25)
[#3209]: https://github.com/nymtech/nym/issues/3209
[#3226]: https://github.com/nymtech/nym/pull/3226
[#3255]: https://github.com/nymtech/nym/pull/3255
[#3273]: https://github.com/nymtech/nym/pull/3273
- 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)
Generated
+30 -13
View File
@@ -1576,7 +1576,7 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "explorer-api"
version = "1.1.15"
version = "1.1.16"
dependencies = [
"chrono",
"clap 4.1.11",
@@ -3011,7 +3011,7 @@ dependencies = [
[[package]]
name = "nym-api"
version = "1.1.16"
version = "1.1.17"
dependencies = [
"anyhow",
"async-trait",
@@ -3142,7 +3142,7 @@ dependencies = [
[[package]]
name = "nym-cli"
version = "1.1.15"
version = "1.1.16"
dependencies = [
"anyhow",
"base64 0.13.1",
@@ -3203,7 +3203,7 @@ dependencies = [
[[package]]
name = "nym-client"
version = "1.1.15"
version = "1.1.16"
dependencies = [
"clap 4.1.11",
"dirs",
@@ -3471,7 +3471,7 @@ dependencies = [
[[package]]
name = "nym-gateway"
version = "1.1.15"
version = "1.1.16"
dependencies = [
"anyhow",
"async-trait",
@@ -3600,7 +3600,7 @@ dependencies = [
[[package]]
name = "nym-mixnet-contract-common"
version = "0.4.0"
version = "0.5.0"
dependencies = [
"bs58",
"cosmwasm-std",
@@ -3619,14 +3619,13 @@ dependencies = [
[[package]]
name = "nym-mixnode"
version = "1.1.16"
version = "1.1.17"
dependencies = [
"anyhow",
"bs58",
"cfg-if",
"clap 4.1.11",
"colored",
"cpu-cycles",
"cupid",
"dirs",
"futures",
@@ -3721,7 +3720,7 @@ dependencies = [
[[package]]
name = "nym-network-requester"
version = "1.1.15"
version = "1.1.16"
dependencies = [
"async-file-watcher",
"async-trait",
@@ -3763,7 +3762,7 @@ dependencies = [
[[package]]
name = "nym-network-statistics"
version = "1.1.15"
version = "1.1.16"
dependencies = [
"dirs",
"log",
@@ -3778,6 +3777,24 @@ dependencies = [
"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]]
name = "nym-nonexhaustive-delayqueue"
version = "0.1.0"
@@ -3879,7 +3896,7 @@ dependencies = [
[[package]]
name = "nym-socks5-client"
version = "1.1.15"
version = "1.1.16"
dependencies = [
"clap 4.1.11",
"lazy_static",
@@ -4202,7 +4219,7 @@ dependencies = [
[[package]]
name = "nym-vesting-contract"
version = "1.3.1"
version = "1.4.0"
dependencies = [
"cosmwasm-derive",
"cosmwasm-std",
@@ -4220,7 +4237,7 @@ dependencies = [
[[package]]
name = "nym-vesting-contract-common"
version = "0.5.0"
version = "0.6.0"
dependencies = [
"cosmwasm-std",
"nym-contracts-common",
+1
View File
@@ -48,6 +48,7 @@ members = [
"common/ledger",
"common/mixnode-common",
"common/network-defaults",
"common/node-tester-utils",
"common/nonexhaustive-delayqueue",
"common/nymcoconut",
"common/nymsphinx",
+5 -1
View File
@@ -13,6 +13,10 @@ happy: fmt clippy-happy test
# on all workspaces.
build-release: build-release-main wasm
# Deprecated
# For backwards compatibility
clippy-all: clippy
# -----------------------------------------------------------------------------
# Define targets for a given workspace
# $(1): name
@@ -52,7 +56,7 @@ fmt-$(1):
cargo fmt --manifest-path $(2)/Cargo.toml --all
clippy-happy: clippy-happy-$(1)
clippy-all: clippy-$(1) clippy-examples-$(1)
clippy: clippy-$(1) clippy-examples-$(1)
check: check-$(1)
cargo-test: test-$(1)
cargo-test-expensive: test-expensive-$(1)
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.15"
version = "1.1.16"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.15"
version = "1.1.16"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
+2
View File
@@ -0,0 +1,2 @@
[build]
target = "wasm32-unknown-unknown"
+7
View File
@@ -17,6 +17,7 @@ default = ["console_error_panic_hook"]
offline-test = []
[dependencies]
bs58 = "0.4.0"
futures = "0.3"
js-sys = "0.3"
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
@@ -28,8 +29,12 @@ tokio = { version = "1.24.1", features = ["sync"] }
url = "2.2"
wasm-bindgen = { version = "=0.2.83", features = ["serde-serialize"] }
wasm-bindgen-futures = "0.4"
thiserror = "1.0.40"
wasm-timer = { git = "https://github.com/mmsinclair/wasm-timer", rev = "b9d1a54ad514c2f230a026afe0dde341e98cd7b6"}
# internal
nym-node-tester-utils = { path = "../../common/node-tester-utils" }
nym-client-core = { path = "../../common/client-core", default-features = false, features = ["wasm"] }
nym-bandwidth-controller = { path = "../../common/bandwidth-controller" }
nym-coconut-interface = { path = "../../common/coconut-interface" }
@@ -37,6 +42,8 @@ nym-credentials = { path = "../../common/credentials" }
nym-credential-storage = { path = "../../common/credential-storage" }
nym-crypto = { path = "../../common/crypto" }
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 }
wasm-utils = { path = "../../common/wasm-utils" }
nym-task = { path = "../../common/task" }
@@ -0,0 +1,2 @@
node_modules
dist
+5
View File
@@ -0,0 +1,5 @@
// A dependency graph that contains any wasm must all be imported
// asynchronously. This `bootstrap.js` file does the single async import, so
// that no one else needs to worry about it again.
import('./index.js')
.catch(e => console.error('Error importing `index.js`:', e));
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nym WebAssembly Demo</title>
</head>
<body>
<p>
<label>Sender: </label><input disabled="true" size="85" type="text" id="sender" value="">
</p>
<p>
<label>Recipient: </label><input size="85" type="text" id="recipient" value="">
</p>
<p>
<label>Message: </label><input type="text" id="message" value="Hello mixnet!">
</p>
<p>
<button id="send-button">Send</button>
</p>
<div>
<label>Mixnode Identity: </label>
<input type="text" size = "60" id="mixnode_identity" value="...">
<button id="magic-button">✨ Magic Test Button ✨</button>
</div>
<p>Send messages from your browser, through the mixnet, and to the recipient using the "send" button.</p>
<p><span style='color: blue;'>Sent</span> messages show in blue, <span style='color: green;'>received</span>
messages show in green.</p>
<hr>
<p>
<span id="output"></span>
</p>
<script src="./bootstrap.js"></script>
</body>
</html>
+170
View File
@@ -0,0 +1,170 @@
// Copyright 2020-2023 Nym Technologies SA
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
class WebWorkerClient {
worker = null;
constructor() {
this.worker = new Worker('./worker.js');
this.worker.onmessage = (ev) => {
if (ev.data && ev.data.kind) {
switch (ev.data.kind) {
case 'Ready':
const {selfAddress} = ev.data.args;
displaySenderAddress(selfAddress);
break;
case 'ReceiveMessage':
const {message, senderTag, isTestPacket } = ev.data.args;
displayReceived(message, senderTag, isTestPacket);
break;
case 'DisableMagicTestButton':
const magicButton = document.querySelector('#magic-button');
magicButton.setAttribute('disabled', "true")
break;
case 'DisplayTesterResults':
const {score, sentPackets, receivedPackets, receivedAcks, duplicatePackets, duplicateAcks} = ev.data.args;
const resultText = `Test score: ${score}. Sent ${sentPackets} packets. Received ${receivedPackets} packets and ${receivedAcks} acks back. We also got ${duplicatePackets} duplicate packets and ${duplicateAcks} duplicate acks.`
displayReceivedRawString(resultText)
break;
}
}
};
}
sendMessage = (message, recipient) => {
if (!this.worker) {
console.error('Could not send message because worker does not exist');
return;
}
this.worker.postMessage({
kind: 'SendMessage',
args: {
message, recipient,
},
});
};
sendTestPacket = (mixnodeIdentity) => {
if (!this.worker) {
console.error('Could not send message because worker does not exist');
return;
}
this.worker.postMessage({
kind: 'TestPacket',
args: {
mixnodeIdentity,
},
});
}
}
let client = null;
async function main() {
client = new WebWorkerClient();
const sendButton = document.querySelector('#send-button');
sendButton.onclick = function () {
sendMessageTo();
};
const magicButton = document.querySelector('#magic-button');
magicButton.onclick = function () {
sendTestPacket();
}
}
/**
* Create a Sphinx packet and send it to the mixnet through the gateway node.
*
* Message and recipient are taken from the values in the user interface.
*
*/
async function sendMessageTo() {
const message = document.getElementById('message').value;
const recipient = document.getElementById('recipient').value;
await client.sendMessage(message, recipient);
displaySend(message);
}
async function sendTestPacket() {
const mixnodeIdentity = document.getElementById('mixnode_identity').value;
await client.sendTestPacket(mixnodeIdentity)
displaySend(`sending test packets to: ${mixnodeIdentity}...`);
}
/**
* Display messages that have been sent up the websocket. Colours them blue.
*
* @param {string} message
*/
function displaySend(message) {
let timestamp = new Date().toISOString().substr(11, 12);
let sendDiv = document.createElement('div');
let paragraph = document.createElement('p');
paragraph.setAttribute('style', 'color: blue');
let paragraphContent = document.createTextNode(timestamp + ' sent >>> ' + message);
paragraph.appendChild(paragraphContent);
sendDiv.appendChild(paragraph);
document.getElementById('output').appendChild(sendDiv);
}
/**
* Display received text messages in the browser. Colour them green.
*
* @param {Uint8Array} raw
*/
function displayReceived(raw, sender_tag, isTestPacket) {
let content = new TextDecoder().decode(raw);
if (sender_tag !== undefined) {
console.log("this message also contained some surbs from", sender_tag)
}
if (isTestPacket) {
const decoded = JSON.parse(content)
content = `Received packet ${decoded.msg_id} / ${decoded.total_msgs} for node ${decoded.encoded_node_identity} (test: ${decoded.test_id})`
}
displayReceivedRawString(content)
}
function displayReceivedRawString(raw) {
let timestamp = new Date().toISOString().substr(11, 12);
let receivedDiv = document.createElement('div');
let paragraph = document.createElement('p');
paragraph.setAttribute('style', 'color: green');
let paragraphContent = document.createTextNode(timestamp + ' received >>> ' + raw);
paragraph.appendChild(paragraphContent);
receivedDiv.appendChild(paragraph);
document.getElementById('output').appendChild(receivedDiv);
}
/**
* Display the nymClient's sender address in the user interface
*
* @param {String} address
*/
function displaySenderAddress(address) {
document.getElementById('sender').value = address;
}
main();
@@ -0,0 +1,39 @@
{
"name": "create-wasm-app",
"version": "0.1.0",
"description": "create an app to consume rust-generated wasm packages",
"main": "index.js",
"bin": {
"create-wasm-app": ".bin/create-wasm-app.js"
},
"scripts": {
"build": "webpack --config webpack.config.js",
"start": "webpack-dev-server --port 8001"
},
"repository": {
"type": "git",
"url": "git+https://github.com/rustwasm/create-wasm-app.git"
},
"keywords": [
"webassembly",
"wasm",
"rust",
"webpack"
],
"author": "Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/nymtech/nym/issues"
},
"homepage": "https://nymtech.net/docs",
"devDependencies": {
"copy-webpack-plugin": "^10.2.4",
"hello-wasm-pack": "^0.1.0",
"webpack": "^5.70.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.7.4"
},
"dependencies": {
"@nymproject/nym-client-wasm": "file:../pkg"
}
}
@@ -0,0 +1,33 @@
const CopyWebpackPlugin = require('copy-webpack-plugin');
const path = require('path');
module.exports = {
performance: {
hints: false,
maxEntrypointSize: 512000,
maxAssetSize: 512000
},
entry: {
bootstrap: './bootstrap.js',
worker: './worker.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
// mode: 'development',
mode: 'production',
plugins: [
new CopyWebpackPlugin({
patterns: [
'index.html',
{
from: 'node_modules/@nymproject/nym-client-wasm/*.(js|wasm)',
to: '[name][ext]',
},
],
}),
],
experiments: { syncWebAssembly: true },
};
+294
View File
@@ -0,0 +1,294 @@
// Copyright 2020-2023 Nym Technologies SA
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
importScripts('nym_client_wasm.js');
console.log('Initializing worker');
// wasm_bindgen creates a global variable (with the exports attached) that is in scope after `importScripts`
const {
NymNodeTester,
WasmGateway,
WasmMixNode,
WasmNymTopology,
default_debug,
NymClientBuilder,
NymClient,
set_panic_hook,
Config,
GatewayEndpointConfig,
current_network_topology,
} = wasm_bindgen;
let client = null;
let tester = null;
function dummyTopology() {
const l1Mixnode = new WasmMixNode(
1,
'n1fzv4jc7fanl9s0qj02ge2ezk3kts545kjtek47',
'178.79.143.65',
1789,
'4Yr4qmEHd9sgsuQ83191FR2hD88RfsbMmB4tzhhZWriz',
'8ndjk5oZ6HxUZNScLJJ7hk39XtUqGexdKgW7hSX6kpWG',
1,
'1.10.0',
);
const l2Mixnode = new WasmMixNode(
2,
'n1z93z44vf8ssvdhujjvxcj4rd5e3lz0l60wdk70',
'109.74.197.180',
1789,
'7sVjiMrPYZrDWRujku9QLxgE8noT7NTgBAqizCsu7AoK',
'GepXwRnKZDd8x2nBWAajGGBVvF3mrpVMQBkgfrGuqRCN',
2,
'1.10.0',
);
const l3Mixnode = new WasmMixNode(
3,
'n1ptg680vnmef2cd8l0s9uyc4f0hgf3x8sed6w77',
'176.58.101.80',
1789,
'FoM5Mx9Pxk1g3zEqkS3APgtBeTtTo3M8k7Yu4bV6kK1R',
'DeYjrDC2AcQRVFshiKnbUo6bRvPyZ33QGYR2DLeFJ9qD',
3,
'1.10.0',
);
const gateway = new WasmGateway(
'n16evnn8glr0sham3matj8rg2s24m6x56ayk87ts',
'85.159.212.96',
1789,
9000,
'336yuXAeGEgedRfqTJZsG2YV7P13QH1bHv1SjCZYarc9',
'BtYjoWihiuFihGKQypmpSspbhmWDPxzqeTVSd8ciCpWL',
'1.10.1',
);
const mixnodes = new Map();
mixnodes.set(1, [l1Mixnode]);
mixnodes.set(2, [l2Mixnode]);
mixnodes.set(3, [l3Mixnode]);
const gateways = [gateway];
return new WasmNymTopology(mixnodes, gateways)
}
function printAndDisplayTestResult(result) {
result.log_details();
self.postMessage({
kind: 'DisplayTesterResults',
args: {
score: result.score(),
sentPackets: result.sent_packets,
receivedPackets: result.received_packets,
receivedAcks: result.received_acks,
duplicatePackets: result.duplicate_packets,
duplicateAcks: result.duplicate_acks,
},
});
}
function dummyGatewayConfig() {
return new GatewayEndpointConfig(
'336yuXAeGEgedRfqTJZsG2YV7P13QH1bHv1SjCZYarc9',
'n1rqqw8km7a0rvf8lr6k8dsdqvvkyn2mglj7xxfm',
'ws://85.159.212.96:9000',
)
}
async function testWithTester() {
const gatewayConfig = dummyGatewayConfig();
// A) construct with hardcoded topology
const topology = dummyTopology()
const nodeTester = await new NymNodeTester(gatewayConfig, topology);
// B) first get topology directly from nym-api
// const validator = 'https://qwerty-validator-api.qa.nymte.ch/api';
// const topology = await current_network_topology(validator)
// const nodeTester = await new NymNodeTester(gatewayConfig, topology);
//
// C) use nym-api in the constructor (note: it does no filtering for 'good' nodes on other layers)
// const validator = 'https://qwerty-validator-api.qa.nymte.ch/api';
// const nodeTester = await NymNodeTester.new_with_api(gatewayConfig, validator)
self.onmessage = async event => {
if (event.data && event.data.kind) {
switch (event.data.kind) {
case 'TestPacket': {
const {mixnodeIdentity} = event.data.args;
console.log("starting node test...");
let result = await nodeTester.test_node(mixnodeIdentity);
printAndDisplayTestResult(result)
}
}
}
};
}
async function testWithNymClient() {
const gatewayConfig = dummyGatewayConfig();
const topology = dummyTopology()
let received = 0
const onMessageHandler = (message) => {
received += 1;
self.postMessage({
kind: 'ReceiveMessage',
args: {
message,
senderTag: undefined,
isTestPacket: true,
},
});
// it's really up to the user to create proper callback here...
console.log(`received ${received} packets so far`)
};
console.log('Instantiating WASM client...');
let clientBuilder = NymClientBuilder.new_tester(gatewayConfig, topology, onMessageHandler)
console.log('Web worker creating WASM client...');
let local_client = await clientBuilder.start_client();
console.log('WASM client running!');
const selfAddress = local_client.self_address();
// set the global (I guess we don't have to anymore?)
client = local_client;
console.log(`Client address is ${selfAddress}`);
self.postMessage({
kind: 'Ready',
args: {
selfAddress,
},
});
// Set callback to handle messages passed to the worker.
self.onmessage = async event => {
console.log(event)
if (event.data && event.data.kind) {
switch (event.data.kind) {
case 'SendMessage': {
const {message, recipient} = event.data.args;
let uint8Array = new TextEncoder().encode(message);
await client.send_regular_message(uint8Array, recipient);
break;
}
case 'TestPacket': {
const {mixnodeIdentity} = event.data.args;
const req = await client.try_construct_test_packet_request(mixnodeIdentity);
await client.change_hardcoded_topology(req.injectable_topology());
await client.try_send_test_packets(req);
break;
}
}
}
};
}
async function normalNymClientUsage() {
self.postMessage({kind: 'DisableMagicTestButton'});
// only really useful if you want to adjust some settings like traffic rate
// (if not needed you can just pass a null)
const debug = default_debug();
debug.disable_main_poisson_packet_distribution = true;
debug.disable_loop_cover_traffic_stream = true;
debug.use_extended_packet_size = false;
// debug.average_packet_delay_ms = BigInt(10);
// debug.average_ack_delay_ms = BigInt(10);
// debug.ack_wait_addition_ms = BigInt(3000);
// debug.ack_wait_multiplier = 10;
debug.topology_refresh_rate_ms = BigInt(60000)
const gatewayConfig = dummyGatewayConfig();
const validator = 'https://qwerty-validator-api.qa.nymte.ch/api';
const config = new Config('my-awesome-wasm-client', validator, gatewayConfig, debug);
const onMessageHandler = (message) => {
console.log(message);
self.postMessage({
kind: 'ReceiveMessage',
args: {
message,
},
});
};
console.log('Instantiating WASM client...');
let localClient = await new NymClient(config, onMessageHandler)
console.log('WASM client running!');
const selfAddress = localClient.self_address();
// set the global (I guess we don't have to anymore?)
client = localClient;
console.log(`Client address is ${selfAddress}`);
self.postMessage({
kind: 'Ready',
args: {
selfAddress,
},
});
// Set callback to handle messages passed to the worker.
self.onmessage = async event => {
console.log(event)
if (event.data && event.data.kind) {
switch (event.data.kind) {
case 'SendMessage': {
const {message, recipient} = event.data.args;
let uint8Array = new TextEncoder().encode(message);
await client.send_regular_message(uint8Array, recipient);
break;
}
}
}
};
}
async function main() {
// load WASM package
await wasm_bindgen('nym_client_wasm_bg.wasm');
console.log('Loaded WASM');
// sets up better stack traces in case of in-rust panics
set_panic_hook();
// run test on simplified and dedicated tester:
await testWithTester()
// hook-up the whole client for testing
// await testWithNymClient()
// 'Normal' client setup (to send 'normal' messages)
// await normalNymClientUsage()
}
// Let's get started!
main();
File diff suppressed because it is too large Load Diff
+13 -4
View File
@@ -25,7 +25,7 @@ pub struct Config {
/// ID specifies the human readable ID of this particular client.
pub(crate) id: String,
pub(crate) nym_api_url: Url,
pub(crate) nym_api_url: Option<Url>,
pub(crate) disabled_credentials_mode: bool,
@@ -46,9 +46,11 @@ impl Config {
) -> Self {
Config {
id,
nym_api_url: validator_server
.parse()
.expect("provided url was malformed"),
nym_api_url: Some(
validator_server
.parse()
.expect("provided url was malformed"),
),
disabled_credentials_mode: true,
gateway_endpoint,
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
/// did not reach its destination.
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 {
@@ -238,6 +245,7 @@ impl From<Topology> for ConfigTopology {
topology_resolution_timeout: Duration::from_millis(
topology.topology_resolution_timeout_ms,
),
disable_refreshing: topology.disable_refreshing,
}
}
}
@@ -247,6 +255,7 @@ impl From<ConfigTopology> for Topology {
Topology {
topology_refresh_rate_ms: topology.topology_refresh_rate.as_millis() as u64,
topology_resolution_timeout_ms: topology.topology_resolution_timeout.as_millis() as u64,
disable_refreshing: topology.disable_refreshing,
}
}
}
+146 -7
View File
@@ -1,16 +1,42 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
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 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_topology::{MixLayer, NymTopology};
use std::sync::Arc;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::future_to_promise;
use wasm_utils::{console_log, js_error, 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
pub(crate) trait InputSender {
fn send_message(&self, message: InputMessage) -> Promise;
fn send_messages(&self, messages: Vec<InputMessage>) -> Promise;
}
impl InputSender for Arc<ClientInput> {
@@ -19,12 +45,125 @@ impl InputSender for Arc<ClientInput> {
future_to_promise(async move {
match this.input_sender.send(message).await {
Ok(_) => Ok(JsValue::null()),
Err(_) => {
let js_error =
js_sys::Error::new("InputMessageReceiver has stopped receiving!");
Err(JsValue::from(js_error))
}
Err(_) => Err(simple_js_error(
"InputMessageReceiver has stopped receiving!",
)),
}
})
}
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 mut test_msgs = Vec::with_capacity(num_test_packets as usize);
for i in 1..=num_test_packets {
let msg = NodeTestMessage::new_mix(
mix,
i,
num_test_packets,
WasmTestMessageExt::new(test_id),
);
let serialized = match msg.as_bytes() {
Ok(bytes) => bytes,
Err(err) => return Err(js_error!("failed to serialize test message: {err}")),
};
test_msgs.push(serialized);
}
let mut updated = current_topology.clone();
updated.set_mixes_in_layer(mix.layer.into(), vec![mix.to_owned()]);
Ok(JsValue::from(NymClientTestRequest {
test_msgs,
testable_topology: updated,
}))
})
}
}
+178 -97
View File
@@ -1,27 +1,36 @@
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use self::config::Config;
use crate::client::helpers::InputSender;
use crate::client::helpers::{InputSender, NymClientTestRequest, WasmTopologyExt};
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 nym_bandwidth_controller::wasm_mockups::{Client as FakeClient, DirectSigningNyxdClient};
use nym_bandwidth_controller::BandwidthController;
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::{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_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_task::connections::TransmissionLane;
use nym_task::TaskManager;
use nym_topology::provider_trait::{HardcodedTopologyProvider, TopologyProvider};
use nym_topology::NymTopology;
use rand::rngs::OsRng;
use rand::RngCore;
use std::sync::Arc;
use wasm_bindgen::prelude::*;
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;
mod helpers;
@@ -31,6 +40,11 @@ mod response_pusher;
pub struct NymClient {
self_address: String,
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
// and if it's dropped, everything will start going offline
@@ -40,6 +54,7 @@ pub struct NymClient {
#[wasm_bindgen]
pub struct NymClientBuilder {
config: Config,
custom_topology: Option<NymTopology>,
/// KeyManager object containing smart pointers to all relevant keys used by the client.
key_manager: KeyManager,
@@ -60,118 +75,190 @@ impl NymClientBuilder {
pub fn new(config: Config, on_message: js_sys::Function) -> Self {
//, key_manager: Option<KeyManager>) {
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,
key_manager: Self::setup_key_manager(),
custom_topology: None,
key_manager: setup_new_key_manager(),
on_message,
bandwidth_controller: None,
disabled_credentials: true,
}
}
// TODO: once we make keys persistent, we'll require some kind of `init` method to generate
// a prior shared keypair between the client and the gateway
// no cover traffic
// 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?
fn setup_key_manager() -> KeyManager {
let mut rng = OsRng;
// for time being generate new keys each time...
console_log!("generated new set of keys");
KeyManager::new(&mut rng)
}
let full_config = Config {
id: "ephemeral-id".to_string(),
nym_api_url: None,
disabled_credentials_mode: true,
gateway_endpoint: gateway_config,
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
// with no persistence
fn setup_reply_surb_storage_backend(config: &Config) -> browser_backend::Backend {
browser_backend::Backend::new(
config
.debug
.reply_surbs
.minimum_reply_surb_storage_threshold,
config
.debug
.reply_surbs
.maximum_reply_surb_storage_threshold,
)
NymClientBuilder {
reply_surb_storage_backend: setup_reply_surb_storage_backend(
full_config.debug.reply_surbs,
),
config: full_config,
custom_topology: Some(topology.into()),
// TODO: once we make keys persistent, we'll require some kind of `init` method to generate
// a prior shared keypair between the client and the gateway
key_manager: setup_new_key_manager(),
on_message,
bandwidth_controller: None,
disabled_credentials: true,
}
}
fn start_reconstructed_pusher(client_output: ClientOutput, on_message: js_sys::Function) {
ResponsePusher::new(client_output, on_message).start()
}
pub async fn start_client(self) -> Promise {
future_to_promise(async move {
console_log!("Starting the wasm client");
fn topology_provider(&mut self) -> Option<Box<dyn TopologyProvider>> {
if let Some(hardcoded_topology) = self.custom_topology.take() {
Some(Box::new(HardcodedTopologyProvider::new(hardcoded_topology)))
} else {
None
}
}
let disabled_credentials = if self.disabled_credentials {
CredentialsToggle::Disabled
} else {
CredentialsToggle::Enabled
};
async fn start_client_async(mut self) -> Result<NymClient, WasmClientError> {
console_log!("Starting the wasm client");
let base_builder = BaseClientBuilder::new(
&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 maybe_topology_provider = self.topology_provider();
let self_address = base_builder.as_mix_recipient().to_string();
let mut started_client = match base_builder.start_base().await {
Ok(base_client) => base_client,
Err(err) => {
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 disabled_credentials = if self.disabled_credentials {
CredentialsToggle::Disabled
} else {
CredentialsToggle::Enabled
};
let client_input = started_client.client_input.register_producer();
let client_output = started_client.client_output.register_consumer();
let nym_api_endpoints = match self.config.nym_api_url {
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 {
self_address,
client_input: Arc::new(client_input),
_task_manager: started_client.task_manager,
}))
let client_input = started_client.client_input.register_producer();
let client_output = started_client.client_output.register_consumer();
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]
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 {
self.self_address.clone()
}
fn parse_recipient(recipient: &str) -> Result<Recipient, JsValue> {
match Recipient::try_from_base58_string(recipient) {
Ok(recipient) => Ok(recipient),
Err(err) => {
let error_msg = format!("{recipient} is not a valid Nym network recipient - {err}");
console_error!("{}", error_msg);
let js_error = js_sys::Error::new(&error_msg);
Err(JsValue::from(js_error))
}
}
pub fn try_construct_test_packet_request(
&self,
mixnode_identity: String,
num_test_packets: Option<u32>,
) -> Promise {
// TODO: improve the source of rng (i.e. don't make it ephemeral...)
let mut ephemeral_rng = OsRng;
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> {
match AnonymousSenderTag::try_from_base58_string(tag) {
Ok(tag) => Ok(tag),
Err(err) => {
let error_msg = format!("{tag} is not a valid Nym AnonymousSenderTag - {err}");
console_error!("{}", error_msg);
let js_error = js_sys::Error::new(&error_msg);
Err(JsValue::from(js_error))
}
}
pub fn change_hardcoded_topology(&self, topology: WasmNymTopology) -> Promise {
self.client_state.change_hardcoded_topology(topology)
}
pub fn current_network_topology(&self) -> Promise {
self.client_state.current_topology()
}
/// 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.
@@ -184,10 +271,8 @@ impl NymClient {
message.len() as f64 / 1024.0
);
let recipient = match Self::parse_recipient(&recipient) {
Ok(recipient) => recipient,
Err(err) => return Promise::reject(&err),
};
let recipient = check_promise_result!(parse_recipient(&recipient));
let lane = TransmissionLane::General;
let input_msg = InputMessage::new_regular(recipient, message, lane);
@@ -213,10 +298,8 @@ impl NymClient {
message.len() as f64 / 1024.0
);
let recipient = match Self::parse_recipient(&recipient) {
Ok(recipient) => recipient,
Err(err) => return Promise::reject(&err),
};
let recipient = check_promise_result!(parse_recipient(&recipient));
let lane = TransmissionLane::General;
let input_msg = InputMessage::new_anonymous(recipient, message, reply_surbs, lane);
@@ -233,10 +316,8 @@ impl NymClient {
message.len() as f64 / 1024.0
);
let sender_tag = match Self::parse_sender_tag(&recipient_tag) {
Ok(recipient) => recipient,
Err(err) => return Promise::reject(&err),
};
let sender_tag = check_promise_result!(parse_sender_tag(&recipient_tag));
let lane = TransmissionLane::General;
let input_msg = InputMessage::new_reply(sender_tag, message, lane);
+99
View File
@@ -0,0 +1,99 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::topology::WasmTopologyError;
use js_sys::Promise;
use nym_client_core::error::ClientCoreError;
use nym_crypto::asymmetric::identity::Ed25519RecoveryError;
use nym_gateway_client::error::GatewayClientError;
use nym_node_tester_utils::error::NetworkTestingError;
use nym_sphinx::addressing::clients::RecipientFormattingError;
use nym_sphinx::anonymous_replies::requests::InvalidAnonymousSenderTagRepresentation;
use nym_validator_client::ValidatorClientError;
use thiserror::Error;
use wasm_bindgen::JsValue;
use wasm_utils::simple_js_error;
// might as well start using well-defined error enum...
#[derive(Debug, Error)]
pub enum WasmClientError {
#[error(
"A node test is already in progress. Wait for it to finish before starting another one."
)]
TestInProgress,
#[error("experienced an issue with internal client components: {source}")]
BaseClientError {
#[from]
source: ClientCoreError,
},
#[error("The provided gateway identity is invalid: {source}")]
InvalidGatewayIdentity { source: Ed25519RecoveryError },
#[error("Gateway communication failure: {source}")]
GatewayClientError {
#[from]
source: GatewayClientError,
},
#[error("failed to query nym api: {source}")]
NymApiError {
#[from]
source: ValidatorClientError,
},
#[error("The provided topology was invalid: {source}")]
WasmTopologyError {
#[from]
source: WasmTopologyError,
},
#[error("failed to test the node: {source}")]
NodeTestingFailure {
#[from]
source: NetworkTestingError,
},
#[error("{raw} is not a valid url: {source}")]
MalformedUrl {
raw: String,
source: url::ParseError,
},
#[error("Network topology is currently unavailable")]
UnavailableNetworkTopology,
#[error("Mixnode {mixnode_identity} is not present in the current network topology")]
NonExistentMixnode { mixnode_identity: String },
#[error("{raw} is not a valid Nym network recipient: {source}")]
MalformedRecipient {
raw: String,
source: RecipientFormattingError,
},
#[error("{raw} is not a valid Nym AnonymousSenderTag: {source}")]
MalformedSenderTag {
raw: String,
source: InvalidAnonymousSenderTagRepresentation,
},
}
impl WasmClientError {
pub fn into_rejected_promise(self) -> Promise {
self.into()
}
}
impl From<WasmClientError> for JsValue {
fn from(value: WasmClientError) -> Self {
simple_js_error(value.to_string())
}
}
impl From<WasmClientError> for Promise {
fn from(value: WasmClientError) -> Self {
Promise::reject(&value.into())
}
}
+82
View File
@@ -0,0 +1,82 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::WasmClientError;
use crate::topology::WasmNymTopology;
use js_sys::Promise;
use nym_client_core::client::key_manager::KeyManager;
use nym_client_core::client::replies::reply_storage::browser_backend;
use nym_client_core::config;
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_topology::NymTopology;
use nym_validator_client::NymApiClient;
use rand::rngs::OsRng;
use url::Url;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen_futures::future_to_promise;
use wasm_utils::{console_log, PromisableResult};
pub(crate) fn setup_new_key_manager() -> KeyManager {
let mut rng = OsRng;
console_log!("generated new set of keys");
KeyManager::new(&mut rng)
}
// don't get too excited about the name, under the hood it's just a big fat placeholder
// with no persistence
pub(crate) fn setup_reply_surb_storage_backend(
config: config::ReplySurbs,
) -> browser_backend::Backend {
browser_backend::Backend::new(
config.minimum_reply_surb_storage_threshold,
config.maximum_reply_surb_storage_threshold,
)
}
pub(crate) fn parse_recipient(recipient: &str) -> Result<Recipient, WasmClientError> {
Recipient::try_from_base58_string(recipient).map_err(|source| {
WasmClientError::MalformedRecipient {
raw: recipient.to_string(),
source,
}
})
}
pub(crate) fn parse_sender_tag(tag: &str) -> Result<AnonymousSenderTag, WasmClientError> {
AnonymousSenderTag::try_from_base58_string(tag).map_err(|source| {
WasmClientError::MalformedSenderTag {
raw: tag.to_string(),
source,
}
})
}
pub(crate) async fn current_network_topology_async(
nym_api_url: String,
) -> Result<WasmNymTopology, WasmClientError> {
let url: Url = match nym_api_url.parse() {
Ok(url) => url,
Err(source) => {
return Err(WasmClientError::MalformedUrl {
raw: nym_api_url,
source,
})
}
};
let api_client = NymApiClient::new(url);
let mixnodes = api_client.get_cached_active_mixnodes().await?;
let gateways = api_client.get_cached_gateways().await?;
Ok(NymTopology::from_detailed(mixnodes, gateways).into())
}
#[wasm_bindgen]
pub fn current_network_topology(nym_api_url: String) -> Promise {
future_to_promise(async move {
current_network_topology_async(nym_api_url)
.await
.into_promise_result()
})
}
+8
View File
@@ -7,11 +7,19 @@ use wasm_bindgen::prelude::*;
mod client;
#[cfg(target_arch = "wasm32")]
pub mod encoded_payload_helper;
pub mod error;
#[cfg(target_arch = "wasm32")]
pub mod gateway_selector;
#[cfg(target_arch = "wasm32")]
pub mod tester;
#[cfg(target_arch = "wasm32")]
pub mod topology;
#[cfg(target_arch = "wasm32")]
pub mod validation;
#[cfg(target_arch = "wasm32")]
mod helpers;
#[wasm_bindgen]
pub fn set_panic_hook() {
// When the `console_error_panic_hook` feature is enabled, we can call the
@@ -0,0 +1,117 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::tester::helpers::NodeTestResult;
use crate::tester::NodeTestMessage;
use futures::StreamExt;
use nym_node_tester_utils::receiver::{Received, 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>,
}
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>,
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>) -> 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) => match NodeTestMessage::try_recover(msg) {
Ok(test_msg) => {
if !self.received_valid_messages.insert(test_msg.msg_id) {
self.duplicate_packets += 1;
}
}
Err(err) => {
console_warn!("failed to recover test message from received packet: {err}")
}
},
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()
}
}
+108
View File
@@ -0,0 +1,108 @@
// 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::receiver::{Received, 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>>);
impl ReceivedReceiverWrapper {
pub(super) fn new(inner: ReceivedReceiver) -> 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> {
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)
}
}
+316
View File
@@ -0,0 +1,316 @@
// 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,
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)
.map_err(Into::into)
}
pub fn test_node(
&self,
mixnode_identity: String,
timeout_millis: Option<u64>,
num_test_packets: Option<u32>,
) -> Promise {
// establish test parameters
let timeout = timeout_millis
.map(Duration::from_millis)
.unwrap_or(DEFAULT_TEST_TIMEOUT);
let num_test_packets = num_test_packets.unwrap_or(DEFAULT_TEST_PACKETS);
// mark start of the test
if self.test_in_progress.swap(true, Ordering::SeqCst) {
return WasmClientError::TestInProgress.into_rejected_promise();
}
// prepare test packets
// (I simultaneously feel both disgusted and amazed by this workaround)
let test_nonce = self.current_test_nonce.fetch_add(1, Ordering::Relaxed);
let test_packets = check_promise_result!(self.prepare_test_packets(
mixnode_identity,
test_nonce,
num_test_packets
));
let processed_receiver_clone = self.processed_receiver.clone();
let gateway_client_clone = Arc::clone(&self.gateway_client);
let tester_marker = TestMarker::new(Arc::clone(&self.test_in_progress));
// start doing async things (send packets and watch for anything coming back)
future_to_promise(async move {
test_mixnode(
test_packets,
gateway_client_clone,
processed_receiver_clone,
tester_marker,
timeout,
)
.await
.into_promise_result()
})
}
}
+262
View File
@@ -0,0 +1,262 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_client_core::config::GatewayEndpointConfig;
use nym_crypto::asymmetric::{encryption, identity};
use nym_topology::gateway::GatewayConversionError;
use nym_topology::mix::{Layer, MixnodeConversionError};
use nym_topology::{gateway, mix, MixLayer, NymTopology};
use nym_validator_client::client::MixId;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use thiserror::Error;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;
use wasm_utils::{console_log, simple_js_error};
#[derive(Debug, Error)]
pub enum WasmTopologyError {
#[error("got invalid mix layer {value}. Expected 1, 2 or 3.")]
InvalidMixLayer { value: u8 },
#[error(transparent)]
GatewayConversion(#[from] GatewayConversionError),
#[error(transparent)]
MixnodeConversion(#[from] MixnodeConversionError),
#[error("The provided mixnode map was malformed: {source}")]
MalformedMixnodeMap { source: serde_wasm_bindgen::Error },
#[error("The provided gateway list was malformed: {source}")]
MalformedGatewayList { source: serde_wasm_bindgen::Error },
}
impl From<WasmTopologyError> for JsValue {
fn from(value: WasmTopologyError) -> Self {
simple_js_error(value.to_string())
}
}
#[wasm_bindgen]
#[derive(Debug)]
pub struct WasmNymTopology {
inner: NymTopology,
}
#[wasm_bindgen]
impl WasmNymTopology {
#[wasm_bindgen(constructor)]
pub fn new(
// expected: BTreeMap<MixLayer, Vec<WasmMixNode>>,
// HashMap<MixLayer, Vec<WasmMixNode>> will also work because it has the same json representation
mixnodes: JsValue,
// expected: Vec<WasmGateway>
gateways: JsValue,
) -> Result<WasmNymTopology, WasmTopologyError> {
let mixnodes: BTreeMap<MixLayer, Vec<WasmMixNode>> =
serde_wasm_bindgen::from_value(mixnodes)
.map_err(|source| WasmTopologyError::MalformedMixnodeMap { source })?;
let gateways: Vec<WasmGateway> = serde_wasm_bindgen::from_value(gateways)
.map_err(|source| WasmTopologyError::MalformedGatewayList { source })?;
let mut converted_mixes = BTreeMap::new();
for (layer, nodes) in mixnodes {
let layer_nodes = nodes
.into_iter()
.map(TryInto::try_into)
.collect::<Result<_, _>>()?;
converted_mixes.insert(layer, layer_nodes);
}
let gateways = gateways
.into_iter()
.map(TryInto::try_into)
.collect::<Result<_, _>>()?;
Ok(WasmNymTopology {
inner: NymTopology::new(converted_mixes, gateways),
})
}
pub(crate) fn ensure_contains(&self, gateway_config: &GatewayEndpointConfig) -> bool {
self.inner
.gateways()
.iter()
.any(|g| g.identity_key.to_base58_string() == gateway_config.gateway_id)
}
pub fn print(&self) {
if !self.inner.mixes().is_empty() {
console_log!("mixnodes:");
for (layer, nodes) in self.inner.mixes() {
console_log!("\tlayer {layer}:");
for node in nodes {
console_log!("\t\t{} - {}", node.mix_id, node.identity_key)
}
}
} else {
console_log!("NO MIXNODES")
}
if !self.inner.gateways().is_empty() {
console_log!("gateways:");
for gateway in self.inner.gateways() {
console_log!("\t{}", gateway.identity_key)
}
} else {
console_log!("NO GATEWAYS")
}
}
}
impl From<WasmNymTopology> for NymTopology {
fn from(value: WasmNymTopology) -> Self {
value.inner
}
}
impl From<NymTopology> for WasmNymTopology {
fn from(value: NymTopology) -> Self {
WasmNymTopology { inner: value }
}
}
#[wasm_bindgen]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct WasmMixNode {
pub mix_id: MixId,
#[wasm_bindgen(getter_with_clone)]
pub owner: String,
#[wasm_bindgen(getter_with_clone)]
pub host: String,
pub mix_port: u16,
#[wasm_bindgen(getter_with_clone)]
pub identity_key: String,
#[wasm_bindgen(getter_with_clone)]
pub sphinx_key: String,
pub layer: MixLayer,
#[wasm_bindgen(getter_with_clone)]
pub version: String,
}
#[wasm_bindgen]
impl WasmMixNode {
#[wasm_bindgen(constructor)]
#[allow(clippy::too_many_arguments)]
pub fn new(
mix_id: MixId,
owner: String,
host: String,
mix_port: u16,
identity_key: String,
sphinx_key: String,
layer: MixLayer,
version: String,
) -> Self {
Self {
mix_id,
owner,
host,
mix_port,
identity_key,
sphinx_key,
layer,
version,
}
}
}
impl TryFrom<WasmMixNode> for mix::Node {
type Error = WasmTopologyError;
fn try_from(value: WasmMixNode) -> Result<Self, Self::Error> {
let host = mix::Node::parse_host(&value.host)?;
// try to completely resolve the host in the mix situation to avoid doing it every
// single time we want to construct a path
let mix_host = mix::Node::extract_mix_host(&host, value.mix_port)?;
Ok(mix::Node {
mix_id: value.mix_id,
owner: value.owner,
host,
mix_host,
identity_key: identity::PublicKey::from_base58_string(&value.identity_key)
.map_err(MixnodeConversionError::from)?,
sphinx_key: encryption::PublicKey::from_base58_string(&value.sphinx_key)
.map_err(MixnodeConversionError::from)?,
layer: Layer::try_from(value.layer)
.map_err(|_| WasmTopologyError::InvalidMixLayer { value: value.layer })?,
version: value.version,
})
}
}
#[wasm_bindgen]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct WasmGateway {
#[wasm_bindgen(getter_with_clone)]
pub owner: String,
#[wasm_bindgen(getter_with_clone)]
pub host: String,
pub mix_port: u16,
pub clients_port: u16,
#[wasm_bindgen(getter_with_clone)]
pub identity_key: String,
#[wasm_bindgen(getter_with_clone)]
pub sphinx_key: String,
#[wasm_bindgen(getter_with_clone)]
pub version: String,
}
#[wasm_bindgen]
impl WasmGateway {
#[wasm_bindgen(constructor)]
pub fn new(
owner: String,
host: String,
mix_port: u16,
clients_port: u16,
identity_key: String,
sphinx_key: String,
version: String,
) -> Self {
Self {
owner,
host,
mix_port,
clients_port,
identity_key,
sphinx_key,
version,
}
}
}
impl TryFrom<WasmGateway> for gateway::Node {
type Error = WasmTopologyError;
fn try_from(value: WasmGateway) -> Result<Self, Self::Error> {
let host = gateway::Node::parse_host(&value.host)?;
// try to completely resolve the host in the mix situation to avoid doing it every
// single time we want to construct a path
let mix_host = gateway::Node::extract_mix_host(&host, value.mix_port)?;
Ok(gateway::Node {
owner: value.owner,
host,
mix_host,
clients_port: value.clients_port,
identity_key: identity::PublicKey::from_base58_string(&value.identity_key)
.map_err(GatewayConversionError::from)?,
sphinx_key: encryption::PublicKey::from_base58_string(&value.sphinx_key)
.map_err(GatewayConversionError::from)?,
version: value.version,
})
}
}
@@ -22,7 +22,7 @@ use crate::client::topology_control::{
};
use crate::config::{Config, DebugConfig, GatewayEndpointConfig};
use crate::error::ClientCoreError;
use crate::spawn_future;
use crate::{config, spawn_future};
use futures::channel::mpsc;
use log::{debug, info};
use nym_bandwidth_controller::BandwidthController;
@@ -39,7 +39,6 @@ use nym_task::connections::{ConnectionCommandReceiver, ConnectionCommandSender,
use nym_task::{TaskClient, TaskManager};
use nym_topology::provider_trait::TopologyProvider;
use std::sync::Arc;
use std::time::Duration;
use tap::TapFallible;
use url::Url;
@@ -371,11 +370,12 @@ where
// the current global view of topology
async fn start_topology_refresher(
topology_provider: Box<dyn TopologyProvider>,
refresh_rate: Duration,
topology_config: config::Topology,
topology_accessor: TopologyAccessor,
shutdown: TaskClient,
mut shutdown: TaskClient,
) -> 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(
topology_refresher_config,
@@ -395,8 +395,17 @@ where
return Err(ClientCoreError::InsufficientNetworkTopology(err));
}
info!("Starting topology refresher...");
topology_refresher.start_with_shutdown(shutdown);
if topology_config.disable_refreshing {
// 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(())
}
@@ -500,7 +509,7 @@ where
);
Self::start_topology_refresher(
topology_provider,
self.debug_config.topology.topology_refresh_rate,
self.debug_config.topology,
shared_topology_accessor.clone(),
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::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::forwarding::packet::MixPacket;
use nym_task::connections::TransmissionLane;
pub type InputMessageSender = tokio::sync::mpsc::Sender<InputMessage>;
@@ -7,6 +11,14 @@ pub type InputMessageReceiver = tokio::sync::mpsc::Receiver<InputMessage>;
#[derive(Debug)]
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.
/// You're simply sending your `data` to specified `recipient` without any tagging.
///
@@ -44,6 +56,10 @@ pub enum 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 {
InputMessage::Regular {
recipient,
@@ -82,7 +98,8 @@ impl InputMessage {
match self {
InputMessage::Regular { lane, .. }
| InputMessage::Anonymous { lane, .. }
| InputMessage::Reply { lane, .. } => lane,
| InputMessage::Reply { lane, .. }
| InputMessage::Premade { lane, .. } => lane,
}
}
}
@@ -3,10 +3,12 @@
use crate::client::inbound_messages::{InputMessage, InputMessageReceiver};
use crate::client::real_messages_control::message_handler::MessageHandler;
use crate::client::real_messages_control::real_traffic_stream::RealMessage;
use crate::client::replies::reply_controller::ReplyControllerSender;
use log::*;
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::forwarding::packet::MixPacket;
use nym_task::connections::TransmissionLane;
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(
&mut self,
recipient_tag: AnonymousSenderTag,
@@ -106,6 +120,7 @@ where
} => {
self.handle_reply(recipient_tag, data, lane).await;
}
InputMessage::Premade { msgs, lane } => self.handle_premade_packets(msgs, lane).await,
};
}
@@ -131,7 +131,10 @@ where
// send to `OutQueueControl` to eventually send to the mix network
self.message_handler
.forward_messages(
vec![RealMessage::new(prepared_fragment.mix_packet, frag_id)],
vec![RealMessage::new(
prepared_fragment.mix_packet,
Some(frag_id),
)],
TransmissionLane::Retransmission,
)
.await
@@ -291,8 +291,10 @@ where
.try_prepare_single_reply_chunk_for_sending(reply_surb, chunk_clone)
.await?;
let real_messages =
RealMessage::new(prepared_fragment.mix_packet, chunk.fragment_identifier());
let real_messages = RealMessage::new(
prepared_fragment.mix_packet,
Some(chunk.fragment_identifier()),
);
let delay = prepared_fragment.total_delay;
let pending_ack =
PendingAcknowledgement::new_anonymous(chunk, delay, target, is_extra_surb_request);
@@ -384,7 +386,8 @@ where
let lane = raw.0;
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 pending_ack = PendingAcknowledgement::new_anonymous(fragment, delay, target, false);
@@ -401,6 +404,14 @@ where
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(
&mut self,
recipient: Recipient,
@@ -444,8 +455,10 @@ where
&recipient,
)?;
let real_message =
RealMessage::new(prepared_fragment.mix_packet, fragment.fragment_identifier());
let real_message = RealMessage::new(
prepared_fragment.mix_packet,
Some(fragment.fragment_identifier()),
);
let delay = prepared_fragment.total_delay;
let pending_ack = PendingAcknowledgement::new_known(fragment, delay, recipient);
@@ -121,7 +121,7 @@ where
#[derive(Debug)]
pub(crate) struct RealMessage {
mix_packet: MixPacket,
fragment_id: FragmentIdentifier,
fragment_id: Option<FragmentIdentifier>,
// TODO: add info about it being constructed with reply-surb
}
@@ -129,7 +129,7 @@ impl From<PreparedFragment> for RealMessage {
fn from(fragment: PreparedFragment) -> Self {
RealMessage {
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()
}
pub(crate) fn new(mix_packet: MixPacket, fragment_id: FragmentIdentifier) -> Self {
pub(crate) fn new(mix_packet: MixPacket, fragment_id: Option<FragmentIdentifier>) -> Self {
RealMessage {
mix_packet,
fragment_id,
@@ -255,7 +255,7 @@ where
)
}
StreamMessage::Real(real_message) => {
(real_message.mix_packet, Some(real_message.fragment_id))
(real_message.mix_packet, real_message.fragment_id)
}
};
+6
View File
@@ -743,6 +743,11 @@ pub struct Topology {
/// did not reach its destination.
#[serde(with = "humantime_serde")]
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 {
@@ -750,6 +755,7 @@ impl Default for Topology {
Topology {
topology_refresh_rate: DEFAULT_TOPOLOGY_REFRESH_RATE,
topology_resolution_timeout: DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT,
disable_refreshing: false,
}
}
}
@@ -142,6 +142,7 @@ impl From<OldDebugConfigV1_1_13> for DebugConfig {
topology: Topology {
topology_refresh_rate: value.topology_refresh_rate,
topology_resolution_timeout: value.topology_resolution_timeout,
disable_refreshing: false,
},
reply_surbs: ReplySurbs {
minimum_reply_surb_storage_threshold: value.minimum_reply_surb_storage_threshold,
@@ -13,7 +13,8 @@ pub mod nyxd;
pub mod signing;
pub use crate::error::ValidatorClientError;
pub use client::NymApiClient;
pub use nym_api_requests::*;
#[cfg(feature = "nyxd-client")]
pub use client::{Client, CoconutApiClient, Config, NymApiClient};
pub use client::{Client, CoconutApiClient, Config};
@@ -1,6 +1,6 @@
[package]
name = "nym-mixnet-contract-common"
version = "0.4.0"
version = "0.5.0"
description = "Common library for the Nym mixnet contract"
rust-version = "1.62"
edition = { workspace = true }
@@ -1,6 +1,6 @@
[package]
name = "nym-vesting-contract-common"
version = "0.5.0"
version = "0.6.0"
description = "Common library for the Nym vesting contract"
edition = { workspace = true }
authors = { workspace = true }
@@ -9,7 +9,7 @@ repository = { workspace = true }
[dependencies]
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" }
serde = { version = "1.0", features = ["derive"] }
schemars = "0.8"
+29
View File
@@ -0,0 +1,29 @@
[package]
name = "nym-node-tester-utils"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
futures = "0.3.28"
rand = "0.7.3"
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["macros"]}
nym-crypto = { path = "../crypto", features = ["asymmetric"] }
nym-task = { path = "../task" }
nym-topology = { path = "../topology" }
# TODO: do we need the whole nymsphinx?
nym-sphinx = { path = "../nymsphinx" }
## non-wasm-only dependencies
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.log]
workspace = true
## wasm-only dependencies
[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-utils]
path = "../wasm-utils"
+49
View File
@@ -0,0 +1,49 @@
// 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,
}
+46
View File
@@ -0,0 +1,46 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod error;
pub mod message;
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)*)}
}};
}
+99
View File
@@ -0,0 +1,99 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::NetworkTestingError;
use crate::MixId;
use nym_sphinx::message::NymMessage;
use nym_topology::{gateway, mix};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::hash::{Hash, Hasher};
#[derive(Serialize, Deserialize, Hash, Clone, Copy)]
pub enum NodeType {
Mixnode(MixId),
Gateway,
}
#[derive(Serialize, Deserialize, Hash, Clone, Copy)]
pub struct Empty;
#[derive(Serialize, Deserialize, Clone)]
pub struct TestMessage<T = Empty> {
pub encoded_node_identity: String,
pub node_owner: String,
pub node_type: NodeType,
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_mix(node: &mix::Node, msg_id: u32, total_msgs: u32, ext: T) -> Self {
TestMessage {
encoded_node_identity: node.identity_key.to_base58_string(),
node_owner: node.owner.clone(),
node_type: NodeType::Mixnode(node.mix_id),
msg_id,
total_msgs,
ext,
}
}
pub fn new_gateway(node: &gateway::Node, msg_id: u32, total_msgs: u32, ext: T) -> Self {
TestMessage {
encoded_node_identity: node.identity_key.to_base58_string(),
node_owner: node.owner.clone(),
node_type: NodeType::Gateway,
msg_id,
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 })
}
}
impl<T: Hash> Hash for TestMessage<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.encoded_node_identity.hash(state);
self.node_owner.hash(state);
self.node_type.hash(state);
self.ext.hash(state)
}
}
+148
View File
@@ -0,0 +1,148 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::NetworkTestingError;
use crate::{log_err, log_info, log_warn};
use futures::channel::mpsc;
use futures::StreamExt;
use nym_crypto::asymmetric::encryption;
use nym_sphinx::message::NymMessage;
use nym_sphinx::receiver::{MessageReceiver, SphinxMessageReceiver};
use nym_sphinx::{
acknowledgements::{identifier::recover_identifier, AckKey},
chunking::fragment::FragmentIdentifier,
};
use nym_task::TaskClient;
use std::sync::Arc;
pub type ReceivedSender = mpsc::UnboundedSender<Received>;
pub type ReceivedReceiver = mpsc::UnboundedReceiver<Received>;
// simple enum containing aggregated processed results
pub enum Received {
Message(NymMessage),
Ack(FragmentIdentifier),
}
impl From<NymMessage> for Received {
fn from(value: NymMessage) -> Self {
Received::Message(value)
}
}
impl From<FragmentIdentifier> for Received {
fn from(value: FragmentIdentifier) -> Self {
Received::Ack(value)
}
}
// the 'Simple' bit comes from the fact that it expects all received messages to consist of a single `Fragment`
pub struct SimpleMessageReceiver<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,
mixnet_message_receiver: mpsc::UnboundedReceiver<Vec<Vec<u8>>>,
acks_receiver: mpsc::UnboundedReceiver<Vec<Vec<u8>>>,
received_sender: ReceivedSender,
shutdown: TaskClient,
}
impl SimpleMessageReceiver<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,
shutdown: TaskClient,
) -> Self {
SimpleMessageReceiver {
local_encryption_keypair,
ack_key,
message_receiver: SphinxMessageReceiver::new(),
mixnet_message_receiver,
acks_receiver,
received_sender,
shutdown,
}
}
}
impl<R: MessageReceiver> SimpleMessageReceiver<R> {
fn forward_received<T: Into<Received>>(&self, received: T) {
// 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, mut raw_message: Vec<u8>) -> Result<(), NetworkTestingError> {
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)?;
let (recovered, _) = self
.message_receiver
.insert_new_fragment(fragment)?
.ok_or(NetworkTestingError::NonReconstructablePacket)?; // by definition of this receiver, the message must consist of a single fragment
self.forward_received(recovered);
Ok(())
}
fn on_ack(&mut self, raw_ack: Vec<u8>) -> Result<(), NetworkTestingError> {
let serialized_ack = recover_identifier(&self.ack_key, &raw_ack)
.ok_or(NetworkTestingError::UnrecoverableAck)?;
let frag_id = FragmentIdentifier::try_from_bytes(serialized_ack)
.map_err(|source| NetworkTestingError::MalformedAckIdentifier { source })?;
self.forward_received(frag_id);
Ok(())
}
pub async fn run(&mut self) {
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")
}
}
+195
View File
@@ -0,0 +1,195 @@
// 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,
recipient: 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,
recipient: Recipient,
packet_size: PacketSize,
average_packet_delay: Duration,
average_ack_delay: Duration,
ack_key: Arc<AckKey>,
) -> Self {
Self {
rng,
base_topology,
recipient,
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)
}
pub fn mixnode_test_packets<T>(
&mut self,
mix: &mix::Node,
msg_ext: T,
test_packets: u32,
) -> 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 i in 1..=test_packets {
let msg = TestMessage::new_mix(mix, i, test_packets, msg_ext.clone());
packets.push(self.create_test_packet(&msg, &ephemeral_topology)?);
}
Ok(packets)
}
pub fn existing_mixnode_test_packets<T>(
&mut self,
mix_id: MixId,
msg_ext: T,
test_packets: u32,
) -> 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)
}
pub fn existing_identity_mixnode_test_packets<T>(
&mut self,
encoded_mix_identity: String,
msg_ext: T,
test_packets: u32,
) -> 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)
}
pub fn create_test_packet<T>(
&mut self,
message: &TestMessage<T>,
topology: &NymTopology,
) -> Result<PreparedFragment, NetworkTestingError>
where
T: Serialize,
{
let serialized = message.as_bytes()?;
let message = NymMessage::new_plain(serialized);
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();
// the packet is designed to be sent from ourselves to ourselves
let address = self.recipient;
// 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)?)
}
}
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
}
}
+230 -125
View File
@@ -38,6 +38,200 @@ pub struct PreparedFragment {
pub fragment_identifier: FragmentIdentifier,
}
// 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
/// an optional reply-SURB, padding it to appropriate length, encrypting its content,
/// and chunking into appropriate size [`Fragment`]s.
@@ -111,16 +305,6 @@ where
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(
&mut self,
fragment: Fragment,
@@ -128,62 +312,13 @@ where
ack_key: &AckKey,
reply_surb: ReplySurb,
) -> 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;
let sender = self.sender_address;
// 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(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,
})
<Self as FragmentPreparer>::prepare_reply_chunk_for_sending(
self, fragment, topology, ack_key, reply_surb, &sender,
)
}
/// 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(
&mut self,
fragment: Fragment,
@@ -191,73 +326,27 @@ where
ack_key: &AckKey,
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;
let sender = self.sender_address;
// 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(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,
})
<Self as FragmentPreparer>::prepare_chunk_for_sending(
self,
fragment,
topology,
ack_key,
&sender,
packet_recipient,
)
}
/// Construct an acknowledgement SURB for the given [`FragmentIdentifier`]
fn generate_surb_ack(
pub fn generate_surb_ack(
&mut self,
fragment_id: FragmentIdentifier,
topology: &NymTopology,
ack_key: &AckKey,
) -> Result<SurbAck, NymTopologyError> {
SurbAck::construct(
&mut self.rng,
&self.sender_address,
ack_key,
fragment_id.to_bytes(),
self.average_ack_delay,
topology,
)
let sender = self.sender_address;
<Self as FragmentPreparer>::generate_surb_ack(self, &sender, fragment_id, topology, ack_key)
}
pub fn pad_and_split_message(
@@ -265,11 +354,27 @@ where
message: NymMessage,
packet_size: PacketSize,
) -> 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
.pad_to_full_packet_lengths(plaintext_per_packet)
.split_into_fragments(&mut self.rng, plaintext_per_packet)
impl<R: CryptoRng + Rng> FragmentPreparer for MessagePreparer<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
}
}
+2 -2
View File
@@ -263,7 +263,7 @@ mod message_receiver {
use nym_crypto::asymmetric::identity;
use nym_mixnet_contract_common::Layer;
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
// available to other modules?
@@ -271,7 +271,7 @@ mod message_receiver {
/// tests requiring instance of topology.
#[allow(dead_code)]
fn topology_fixture() -> NymTopology {
let mut mixes = HashMap::new();
let mut mixes = BTreeMap::new();
mixes.insert(
1,
vec![mix::Node {
+14 -5
View File
@@ -7,15 +7,10 @@ use crate::socks::{
authentication::{AuthenticationMethods, Authenticator, User},
server::SphinxSocksServer,
};
use futures::channel::mpsc;
use futures::StreamExt;
use log::*;
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::{
BaseClientBuilder, ClientInput, ClientOutput, ClientState,
};
@@ -28,6 +23,12 @@ use nym_validator_client::nyxd::QueryNyxdClient;
use nym_validator_client::Client;
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 error;
pub mod socks;
@@ -102,6 +103,7 @@ impl NymClient {
pub fn start_socks5_listener(
socks5_config: &Socks5,
debug_config: DebugConfig,
client_input: ClientInput,
client_output: ClientOutput,
client_status: ClientState,
@@ -126,6 +128,11 @@ impl NymClient {
..
} = 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 mut sphinx_socks = SphinxSocksServer::new(
socks5_config.get_listening_port(),
@@ -134,6 +141,7 @@ impl NymClient {
self_address,
shared_lane_queue_lengths,
socks::client::Config::new(
packet_size,
socks5_config.get_provider_interface_version(),
socks5_config.get_socks5_protocol_version(),
socks5_config.get_send_anonymously(),
@@ -255,6 +263,7 @@ impl NymClient {
Self::start_socks5_listener(
self.config.get_socks5(),
*self.config.get_debug_settings(),
client_input,
client_output,
client_state,
@@ -17,6 +17,7 @@ use nym_socks5_requests::{
ConnectionId, RemoteAddress, Socks5ProtocolVersion, Socks5ProviderRequest, Socks5Request,
};
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::params::PacketSize;
use nym_task::connections::{LaneQueueLengths, TransmissionLane};
use nym_task::TaskClient;
use pin_project::pin_project;
@@ -131,6 +132,7 @@ impl AsyncWrite for StreamState {
#[derive(Debug, Copy, Clone)]
pub(crate) struct Config {
biggest_packet_size: PacketSize,
provider_interface_version: ProviderInterfaceVersion,
socks5_protocol_version: Socks5ProtocolVersion,
use_surbs_for_responses: bool,
@@ -140,6 +142,7 @@ pub(crate) struct Config {
impl Config {
pub(crate) fn new(
biggest_packet_size: PacketSize,
provider_interface_version: ProviderInterfaceVersion,
socks5_protocol_version: Socks5ProtocolVersion,
use_surbs_for_responses: bool,
@@ -147,6 +150,7 @@ impl Config {
per_request_surbs: u32,
) -> Self {
Self {
biggest_packet_size,
provider_interface_version,
socks5_protocol_version,
use_surbs_for_responses,
@@ -410,6 +414,9 @@ impl SocksClient {
remote_proxy_target,
conn_receiver,
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,
Some(self.lane_queue_lengths.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
use bytes::{BufMut, Bytes, BytesMut};
use bytes::Bytes;
use futures::Stream;
use std::cell::RefCell;
use std::future::Future;
use std::io;
use std::ops::DerefMut;
use std::pin::Pin;
use std::task::{Context, Poll};
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
const GRACE_DURATION: Duration = Duration::from_millis(1);
// note, min_capacity doesn't mean we're going to always read at least this amount of data,
// 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> {
// TODO: come up with a way to avoid using RefCell (not sure if possible though due to having to
// 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>>>,
pub struct AvailableReader<R> {
inner: tokio_util::io::ReaderStream<R>,
}
impl<'a, R> AvailableReader<'a, R>
where
R: AsyncRead + Unpin,
{
const BUF_INCREMENT: usize = 4096;
impl<R: AsyncRead> AvailableReader<R> {
pub fn new(reader: R, capacity: Option<usize>) -> Self {
let capacity = capacity.unwrap_or(DEFAULT_CAPACITY).max(MIN_CAPACITY);
pub fn new(reader: &'a mut R) -> Self {
AvailableReader {
buf: RefCell::new(BytesMut::with_capacity(Self::BUF_INCREMENT)),
inner: RefCell::new(reader),
grace_period: Some(Box::pin(sleep(GRACE_DURATION))),
inner: tokio_util::io::ReaderStream::with_capacity(reader, capacity),
}
}
}
impl<'a, R: AsyncRead + Unpin> Stream for AvailableReader<'a, R> {
impl<R: AsyncRead + Unpin> Stream for AvailableReader<R> {
type Item = io::Result<Bytes>;
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
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
}
}
}
Pin::new(&mut self.inner).poll_next(cx)
}
}
#[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())
// }
}
@@ -167,6 +167,7 @@ pub(super) async fn run_inbound<F, S>(
remote_source_address: String,
connection_id: ConnectionId,
mix_sender: MixProxySender<S>,
available_plaintext_per_mix_packet: usize,
adapter_fn: F,
shutdown_notify: Arc<Notify>,
lane_queue_lengths: Option<LaneQueueLengths>,
@@ -176,7 +177,9 @@ where
F: Fn(ConnectionId, Vec<u8>, bool) -> S + Send + 'static,
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 shutdown_future = shutdown_notify.notified().then(|_| sleep(SHUTDOWN_TIMEOUT));
@@ -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
use crate::connection_controller::ConnectionReceiver;
@@ -49,6 +49,8 @@ pub struct ProxyRunner<S> {
connection_id: ConnectionId,
lane_queue_lengths: Option<LaneQueueLengths>,
available_plaintext_per_mix_packet: usize,
// Listens to shutdown commands from higher up
shutdown_listener: TaskClient,
}
@@ -64,6 +66,7 @@ where
remote_source_address: String,
mix_receiver: ConnectionReceiver,
mix_sender: MixProxySender<S>,
available_plaintext_per_mix_packet: usize,
connection_id: ConnectionId,
lane_queue_lengths: Option<LaneQueueLengths>,
shutdown_listener: TaskClient,
@@ -76,6 +79,7 @@ where
remote_source_address,
connection_id,
lane_queue_lengths,
available_plaintext_per_mix_packet,
shutdown_listener,
}
}
@@ -96,6 +100,7 @@ where
self.remote_source_address.clone(),
self.connection_id,
self.mix_sender.clone(),
self.available_plaintext_per_mix_packet,
adapter_fn,
Arc::clone(&shutdown_notify),
self.lane_queue_lengths.clone(),
+1 -1
View File
@@ -11,4 +11,4 @@ pub use manager::{StatusReceiver, StatusSender, TaskClient, TaskManager};
#[cfg(not(target_arch = "wasm32"))]
pub use signal::wait_for_signal_and_error;
pub use spawn::spawn_with_report_error;
pub use spawn::{spawn, spawn_with_report_error};
+2 -2
View File
@@ -2,7 +2,7 @@ use crate::TaskClient;
use std::future::Future;
#[cfg(target_arch = "wasm32")]
pub(crate) fn spawn<F>(future: F)
pub fn spawn<F>(future: F)
where
F: Future<Output = ()> + 'static,
{
@@ -10,7 +10,7 @@ where
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn spawn<F>(future: F)
pub fn spawn<F>(future: F)
where
F: Future + Send + 'static,
F::Output: Send + 'static,
+14 -1
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use nym_bin_common::version_checker;
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use std::hash::Hash;
pub trait Versioned: Clone {
@@ -40,3 +40,16 @@ where
.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()
}
}
+22 -14
View File
@@ -42,6 +42,26 @@ pub struct 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 {
&self.identity_key
}
@@ -81,23 +101,11 @@ impl<'a> TryFrom<&'a GatewayBond> for Node {
type Error = GatewayConversionError;
fn try_from(bond: &'a GatewayBond) -> Result<Self, Self::Error> {
let host: NetworkAddress =
bond.gateway
.host
.parse()
.map_err(|err| GatewayConversionError::InvalidAddress {
value: bond.gateway.host.clone(),
source: err,
})?;
let host = Self::parse_host(&bond.gateway.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 = host.to_socket_addrs(bond.gateway.mix_port).map_err(|err| {
GatewayConversionError::InvalidAddress {
value: bond.gateway.host.clone(),
source: err,
}
})?[0];
let mix_host = Self::extract_mix_host(&host, bond.gateway.mix_port)?;
Ok(Node {
owner: bond.owner.as_str().to_owned(),
+38 -9
View File
@@ -1,14 +1,14 @@
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::filter::VersionFilterable;
use log::warn;
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_types::Node as SphinxNode;
use rand::{CryptoRng, Rng};
use std::collections::HashMap;
use std::collections::BTreeMap;
use std::convert::TryInto;
use std::fmt::{self, Display, Formatter};
use std::io;
@@ -96,16 +96,45 @@ pub type MixLayer = u8;
#[derive(Debug, Clone)]
pub struct NymTopology {
mixes: HashMap<MixLayer, Vec<mix::Node>>,
mixes: BTreeMap<MixLayer, Vec<mix::Node>>,
gateways: Vec<gateway::Node>,
}
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 }
}
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 mixes(&self) -> &BTreeMap<MixLayer, Vec<mix::Node>> {
&self.mixes
}
@@ -308,7 +337,7 @@ pub fn nym_topology_from_detailed(
mix_details: Vec<MixNodeDetails>,
gateway_bonds: Vec<GatewayBond>,
) -> NymTopology {
let mut mixes = HashMap::new();
let mut mixes = BTreeMap::new();
for bond in mix_details
.into_iter()
.map(|details| details.bond_information)
@@ -389,7 +418,7 @@ mod converting_mixes_to_vec {
..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(2, vec![node3]);
@@ -405,7 +434,7 @@ mod converting_mixes_to_vec {
#[test]
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();
assert!(mixvec.is_empty());
}
+24 -14
View File
@@ -42,6 +42,28 @@ pub struct Node {
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 {
fn version(&self) -> String {
self.version.clone()
@@ -62,23 +84,11 @@ impl<'a> TryFrom<&'a MixNodeBond> for Node {
type Error = MixnodeConversionError;
fn try_from(bond: &'a MixNodeBond) -> Result<Self, Self::Error> {
let host: NetworkAddress =
bond.mix_node
.host
.parse()
.map_err(|err| MixnodeConversionError::InvalidAddress {
value: bond.mix_node.host.clone(),
source: err,
})?;
let host = Self::parse_host(&bond.mix_node.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 = host
.to_socket_addrs(bond.mix_node.mix_port)
.map_err(|err| MixnodeConversionError::InvalidAddress {
value: bond.mix_node.host.clone(),
source: err,
})?[0];
let mix_host = Self::extract_mix_host(&host, bond.mix_node.mix_port)?;
Ok(Node {
mix_id: bond.mix_id,
+74 -1
View File
@@ -15,7 +15,7 @@ macro_rules! console_log {
($($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_rules! console_warn {
($($t:tt)*) => ($crate::warn(&format_args!($($t)*).to_string()))
@@ -50,3 +50,76 @@ pub async fn sleep(ms: i32) -> Result<(), JsValue> {
js_fut.await?;
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),
}
};
}
+11
View File
@@ -2,6 +2,17 @@
## 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)
- Add a query to the vesting contract for the amount of delegated tokens towards a particular `mix_id` (might be needed by NG) ([#3228])
+4 -4
View File
@@ -1068,7 +1068,7 @@ dependencies = [
[[package]]
name = "nym-mixnet-contract"
version = "1.3.1"
version = "1.4.0"
dependencies = [
"bs58",
"cosmwasm-derive",
@@ -1092,7 +1092,7 @@ dependencies = [
[[package]]
name = "nym-mixnet-contract-common"
version = "0.4.0"
version = "0.5.0"
dependencies = [
"bs58",
"cosmwasm-std",
@@ -1164,7 +1164,7 @@ dependencies = [
[[package]]
name = "nym-vesting-contract"
version = "1.3.1"
version = "1.4.0"
dependencies = [
"base64 0.21.0",
"cosmwasm-crypto",
@@ -1187,7 +1187,7 @@ dependencies = [
[[package]]
name = "nym-vesting-contract-common"
version = "0.5.0"
version = "0.6.0"
dependencies = [
"cosmwasm-std",
"nym-contracts-common",
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-mixnet-contract"
version = "1.3.1"
version = "1.4.0"
description = "Nym mixnet contract"
edition = { workspace = true }
authors = { workspace = true }
@@ -22,8 +22,8 @@ name = "mixnet_contract"
crate-type = ["cdylib", "rlib"]
[dependencies]
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common", version = "0.4.0" }
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.5.0" }
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common", version = "0.5.0" }
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.6.0" }
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", version = "0.4.0" }
cosmwasm-std = { workspace = true }
@@ -226,6 +226,8 @@ pub(crate) fn _try_update_gateway_config(
let cfg_update_event = new_gateway_config_update_event(&owner, &proxy, &new_config);
// clippy beta 1.70.0-beta.1 false positive
#[allow(clippy::redundant_clone)]
let mut updated_bond = existing_bond.clone();
updated_bond.gateway.host = new_config.host;
updated_bond.gateway.mix_port = new_config.mix_port;
@@ -355,6 +355,8 @@ pub(crate) fn _try_remove_mixnode(
ensure_no_pending_pledge_changes(&pending_changes)?;
// set `is_unbonding` field
// clippy beta 1.70.0-beta.1 false positive
#[allow(clippy::redundant_clone)]
let mut updated_bond = existing_bond.clone();
updated_bond.is_unbonding = true;
storage::mixnode_bonds().replace(
@@ -416,6 +418,8 @@ pub(crate) fn _try_update_mixnode_config(
let cfg_update_event =
new_mixnode_config_update_event(existing_bond.mix_id, &owner, &proxy, &new_config);
// clippy beta 1.70.0-beta.1 false positive
#[allow(clippy::redundant_clone)]
let mut updated_bond = existing_bond.clone();
updated_bond.mix_node.host = new_config.host;
updated_bond.mix_node.mix_port = new_config.mix_port;
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-vesting-contract"
version = "1.3.1"
version = "1.4.0"
description = "Nym vesting contract"
edition = { workspace = true }
authors = { workspace = true }
@@ -20,9 +20,9 @@ name = "vesting_contract"
crate-type = ["cdylib", "rlib"]
[dependencies]
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common", version = "0.4.0" }
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common", version = "0.5.0" }
contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", package = "nym-contracts-common", version = "0.4.0" }
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.5.0" }
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.6.0" }
cosmwasm-std = { workspace = true }
cosmwasm-derive = { workspace = true }
+6 -6
View File
@@ -135,8 +135,8 @@ pub fn try_withdraw_vested_coins(
return Err(ContractError::WrongDenom(amount.denom, mix_denom));
}
let address = info.sender.clone();
let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
let address = info.sender;
let account = account_from_address(address.as_str(), deps.storage, deps.api)?;
if address != account.owner_address() {
return Err(ContractError::NotOwner(account.owner_address().to_string()));
}
@@ -170,9 +170,9 @@ pub fn try_transfer_ownership(
info: MessageInfo,
deps: DepsMut<'_>,
) -> Result<Response, ContractError> {
let address = info.sender.clone();
let address = info.sender;
let to_address = deps.api.addr_validate(&to_address)?;
let mut account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
let mut account = account_from_address(address.as_str(), deps.storage, deps.api)?;
if address == account.owner_address() {
account.transfer_ownership(&to_address, deps.storage)?;
Ok(Response::new().add_event(new_ownership_transfer_event(&address, &to_address)))
@@ -194,9 +194,9 @@ pub fn try_update_staking_address(
}
}
let address = info.sender.clone();
let address = info.sender;
let to_address = to_address.and_then(|x| deps.api.addr_validate(&x).ok());
let mut account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
let mut account = account_from_address(address.as_str(), deps.storage, deps.api)?;
if address == account.owner_address() {
let old = account.staking_address().cloned();
account.update_staking_address(to_address.clone(), deps.storage)?;
+4 -4
View File
@@ -49,10 +49,10 @@ assets_version = "2.0.0" # do not edit: managed by `mdbook-admonish install`
[preprocessor.variables.variables]
# code prerequisites versions
minimum_rust_version = "1.66"
# minimum_node_version = ""
# nym platform code most recent release
platform_release_version = "v1.1.15"
platform_release_version = "v1.1.16"
upcoming_platform_release_version = "v1.1.17" # to use when adding 'edit on github' plugin
mix_node_release_version = "v1.1.17"
#
[preprocessor.last-changed]
command = "mdbook-last-changed"
renderer = ["html"]
+4 -3
View File
@@ -1,5 +1,5 @@
[book]
title = "Nym Docs v1.1.15"
title = "Nym Docs v1.1.16"
authors = ["Max Hampshire"]
description = "Nym technical documentation"
language = "en"
@@ -48,8 +48,9 @@ assets_version = "2.0.0" # do not edit: managed by `mdbook-admonish install`
# https://gitlab.com/tglman/mdbook-variables/
[preprocessor.variables.variables]
minimum_rust_version = "1.66"
platform_release_version = "v1.1.15"
mix_node_release_version = "v1.1.16"
platform_release_version = "v1.1.16"
upcoming_platform_release_version = "v1.1.17" # to use in 'edit page on github' plugin (coming soon)
mix_node_release_version = "v1.1.17"
# used by the cmdrun preprocessor - relative path from inside src/<dir>
# binaries_path = '../../../../target/release/'
@@ -355,6 +355,20 @@ username soft nofile 4096
Then reboot your server and restart your mixnode.
## Node Description
Node description is a short text that describes your node. It is displayed in the `nym-mixnode list` command and in the `nym-mixnode node-details` command. it also shows up in the node explorer to let people know what your node is about and link to your website.
To set your node description, create a file called `description.toml` and put it in the same directory as your `config.toml` file. The file should look like this:
```toml
name = "Winston Smith"
description = "I am the Sphinx"
link = "https://nymtech.net"
location = "Giza, Egypt"
```
you will need to restart your node for the changes to take effect.
## Node Families
Node family involves setting up a group of mix nodes that work together to provide greater privacy and security for network communications. This is achieved by having the nodes in the family share information and routes, creating a decentralized network that makes it difficult for third parties to monitor or track communication traffic.
+16 -4
View File
@@ -18,12 +18,14 @@ In the future the SDK will be made up of several components, each of which will
| Component | Functionality | Released |
| --------- | ------------------------------------------------------------------------------------- | -------- |
| Mixnet | Create / load clients & keypairs, subscribe to Mixnet events, send & receive messages | ✔️ |
| Coconut | Create & verify Coconut credentials | |
| Mixnet | Create / load clients & keypairs, subscribe to Mixnet events, send & receive messages | ✔️ |
| Coconut | Create & verify Coconut credentials | 🛠️ |
| Validator | Sign & broadcast Nyx blockchain transactions, query the blockchain | ❌ |
The `mixnet` component currently exposes the logic of two clients: the websocket client, and the socks client.
The `coconut` component is currently being worked on. Right now it exposes logic allowing for the creation of coconut credentials on the Sandbox testnet.
## Websocket client examples
> All the codeblocks below can be found in the `nym-sdk` [examples directory](https://github.com/nymtech/nym/tree/release/{{platform_release_version}}/sdk/rust/nym-sdk/examples) in the monorepo.
@@ -83,11 +85,21 @@ If you aren't running a Validator and Nym API, and just want to import a specifi
```
## Socks client example
There is also the option to embed the [`socks5-client`](../clients/socks5-client.md) into your app code (`examples/socks5.rs`):
```rust,noplayground
{{#include ../../../../sdk/rust/nym-sdk/examples/socks5.rs}}
```
> If you are looking at implementing Nym as a transport layer for a crypto wallet or desktop app, this is probably the best place to start.
```admonish info
If you are looking at implementing Nym as a transport layer for a crypto wallet or desktop app, this is probably the best place to start.
```
## Coconut credential generation
The following code shows how you can use the SDK to create and use a [credential](../bandwidth-credentials.md) representing paid bandwidth on the Sandbox testnet.
```rust,noplayground
{{#include ../../../../sdk/rust/nym-sdk/examples/bandwidth.rs}}
```
You can read more about Coconut credentials (also referred to as `zk-Nym`) [here](../cococnut.md).
+1 -1
View File
@@ -18,7 +18,7 @@ MULTISIG_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0
COCONUT_DKG_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0
REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy
STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090"
NYXD="https://rpc.nymtech.net";
NYXD="https://rpc.nymtech.net"
NYM_API="https://validator.nymtech.net/api/"
DKG_TIME_CONFIGURATION="259200,300,300,60,60,1209600"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "explorer-api"
version = "1.1.15"
version = "1.1.16"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-gateway"
version = "1.1.15"
version = "1.1.16"
authors = [
"Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
"Jędrzej Stuczyński <andrew@nymtech.net>",
+1 -2
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-mixnode"
version = "1.1.16"
version = "1.1.17"
authors = [
"Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
"Jędrzej Stuczyński <andrew@nymtech.net>",
@@ -57,7 +57,6 @@ nym-types = { path = "../common/types" }
nym-topology = { path = "../common/topology" }
nym-validator-client = { path = "../common/client-libs/validator-client" }
nym-bin-common = { path = "../common/bin-common", features = ["output_format"] }
cpu-cycles = { path = "../cpu-cycles", optional = true }
[dev-dependencies]
tokio = { version = "1.21.2", features = [
+6 -1
View File
@@ -51,9 +51,14 @@ fn test_function() {
async fn main() {
cfg_if::cfg_if! {
if #[cfg(feature = "cpucycles")] {
setup_tracing!("/tmp/tracing.log");
let home_dir = dirs::home_dir().expect("Could not get $HOME");
let logs_dir = home_dir.join(".nym").join("logs");
let logs_dir_str = logs_dir.to_str().expect("Could not construct logs path");
setup_tracing!(logs_dir_str);
info!("CPU cycles measurement is ON")
} else {
setup_logging();
info!("CPU cycles measurement is OFF")
}
}
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-api"
version = "1.1.16"
version = "1.1.17"
authors = [
"Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
"Jędrzej Stuczyński <andrew@nymtech.net>",
+3 -3
View File
@@ -3494,7 +3494,7 @@ dependencies = [
[[package]]
name = "nym-mixnet-contract-common"
version = "0.4.0"
version = "0.5.0"
dependencies = [
"bs58",
"cosmwasm-std",
@@ -3841,7 +3841,7 @@ dependencies = [
[[package]]
name = "nym-vesting-contract"
version = "1.3.1"
version = "1.4.0"
dependencies = [
"cosmwasm-derive",
"cosmwasm-std",
@@ -3859,7 +3859,7 @@ dependencies = [
[[package]]
name = "nym-vesting-contract-common"
version = "0.5.0"
version = "0.6.0"
dependencies = [
"cosmwasm-std",
"nym-contracts-common",
+3 -3
View File
@@ -2916,7 +2916,7 @@ dependencies = [
[[package]]
name = "nym-mixnet-contract-common"
version = "0.4.0"
version = "0.5.0"
dependencies = [
"bs58",
"cosmwasm-std",
@@ -3040,7 +3040,7 @@ dependencies = [
[[package]]
name = "nym-vesting-contract"
version = "1.3.1"
version = "1.4.0"
dependencies = [
"cosmwasm-derive",
"cosmwasm-std",
@@ -3058,7 +3058,7 @@ dependencies = [
[[package]]
name = "nym-vesting-contract-common"
version = "0.5.0"
version = "0.6.0"
dependencies = [
"cosmwasm-std",
"nym-contracts-common",
@@ -4,7 +4,7 @@
use nym_sdk::mixnet;
use nym_topology::mix::Layer;
use nym_topology::{mix, NymTopology};
use std::collections::HashMap;
use std::collections::BTreeMap;
#[tokio::main]
async fn main() {
@@ -15,7 +15,7 @@ async fn main() {
let starting_topology = client.read_current_topology().await.unwrap();
// but we don't like our default topology, we want to use only those very specific, hardcoded, nodes:
let mut mixnodes = HashMap::new();
let mut mixnodes = BTreeMap::new();
mixnodes.insert(
1,
vec![mix::Node {
+2
View File
@@ -506,6 +506,7 @@ where
.socks5_config
.clone()
.ok_or(Error::Socks5Config { set: false })?;
let debug_config = self.config.debug_config;
let (mut started_client, nym_address) = self.connect_to_mixnet_common().await?;
let (socks5_status_tx, mut socks5_status_rx) = mpsc::channel(128);
@@ -515,6 +516,7 @@ where
nym_socks5_client_core::NymClient::start_socks5_listener(
&socks5_config,
debug_config,
client_input,
client_output,
client_state.clone(),
@@ -3,7 +3,7 @@
[package]
name = "nym-network-requester"
version = "1.1.15"
version = "1.1.16"
authors.workspace = true
edition.workspace = true
rust-version = "1.65"
@@ -29,6 +29,7 @@ use nym_socks5_requests::{
};
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::params::PacketSize;
use nym_statistics_common::collector::StatisticsSender;
use nym_task::connections::LaneQueueLengths;
use nym_task::{TaskClient, TaskManager};
@@ -57,6 +58,8 @@ pub struct NRServiceProviderBuilder {
}
struct NRServiceProvider {
config: Config,
outbound_request_filter: OutboundRequestFilter,
open_proxy: bool,
mixnet_client: nym_sdk::mixnet::MixnetClient,
@@ -242,6 +245,7 @@ impl NRServiceProviderBuilder {
start_allowed_list_reloader(self.allowed_hosts, shutdown.subscribe()).await;
let service_provider = NRServiceProvider {
config: self.config,
outbound_request_filter: self.outbound_request_filter,
open_proxy: self.open_proxy,
mixnet_client,
@@ -329,6 +333,7 @@ impl NRServiceProvider {
connection_id: ConnectionId,
remote_addr: String,
return_address: reply::MixnetAddress,
biggest_packet_size: PacketSize,
controller_sender: ControllerSender,
mix_input_sender: MixProxySender<MixnetMessage>,
lane_queue_lengths: LaneQueueLengths,
@@ -385,6 +390,7 @@ impl NRServiceProvider {
// run the proxy on the connection
conn.run_proxy(
remote_version,
biggest_packet_size,
mix_receiver,
mix_input_sender,
lane_queue_lengths,
@@ -437,6 +443,11 @@ impl NRServiceProvider {
return;
}
let traffic_config = self.config.get_base().get_debug_config().traffic;
let packet_size = traffic_config
.secondary_packet_size
.unwrap_or(traffic_config.primary_packet_size);
let controller_sender_clone = self.controller_sender.clone();
let mix_input_sender_clone = self.mix_input_sender.clone();
let lane_queue_lengths_clone = self.mixnet_client.shared_lane_queue_lengths();
@@ -449,6 +460,7 @@ impl NRServiceProvider {
conn_id,
remote_addr,
return_address,
packet_size,
controller_sender_clone,
mix_input_sender_clone,
lane_queue_lengths_clone,
@@ -7,6 +7,7 @@ use nym_service_providers_common::interface::RequestVersion;
use nym_socks5_proxy_helpers::connection_controller::ConnectionReceiver;
use nym_socks5_proxy_helpers::proxy_runner::{MixProxySender, ProxyRunner};
use nym_socks5_requests::{ConnectionId, RemoteAddress, Socks5Request};
use nym_sphinx::params::PacketSize;
use nym_task::connections::LaneQueueLengths;
use nym_task::TaskClient;
use std::io;
@@ -42,6 +43,7 @@ impl Connection {
pub(crate) async fn run_proxy(
&mut self,
remote_version: RequestVersion<Socks5Request>,
biggest_packet_size: PacketSize,
mix_receiver: ConnectionReceiver,
mix_sender: MixProxySender<MixnetMessage>,
lane_queue_lengths: LaneQueueLengths,
@@ -57,6 +59,9 @@ impl Connection {
remote_source_address,
mix_receiver,
mix_sender,
// FIXME: this does NOT include overhead due to acks or chunking
// (so actual true plaintext is smaller)
biggest_packet_size.plaintext_size(),
connection_id,
Some(lane_queue_lengths),
shutdown,
@@ -1,6 +1,6 @@
[package]
name = "nym-network-statistics"
version = "1.1.15"
version = "1.1.16"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-cli"
version = "1.1.15"
version = "1.1.16"
authors.workspace = true
edition = "2021"