Compare commits

...

35 Commits

Author SHA1 Message Date
mfahampshire 8a520df064 Update Readme with c->c note, no mixnet-as-proxy yet 2026-02-09 14:23:45 +00:00
mfahampshire 6bb5f1bcae Tidy up integration test loops 2026-02-09 14:20:56 +00:00
mfahampshire e6b10c708c Removed unused dependency 2026-02-09 13:21:51 +00:00
mfahampshire 56fc135c2f Remove unnused code 2026-02-09 13:21:23 +00:00
mfahampshire 24ff4272b6 Increase bindgen timout for headless testing for CI 2026-02-09 12:58:37 +00:00
mfahampshire e6d5f463e7 Fix poll_read bug 2026-02-09 12:58:10 +00:00
mfahampshire cad47732b7 Debug logging 2026-02-09 12:57:24 +00:00
mfahampshire f4ff6717e0 Streamline readme 2026-02-09 12:57:10 +00:00
mfahampshire d8b8b38101 Adding more integration tests 2026-02-09 12:22:38 +00:00
mfahampshire 8e6ceddc66 Port native reference implementation to WASM compatible state 2026-02-09 12:13:27 +00:00
mfahampshire d029a58e13 Add stream to wasm/client for libp2p 2026-02-09 12:12:47 +00:00
mfahampshire 21f8cb89d6 Streamline shutdown flush 2026-02-02 20:42:42 +00:00
mfahampshire 4ac9cdb1b1 Clippy 2026-02-02 15:24:12 +00:00
mfahampshire 296f243433 Remove serde and bincode from ReconstructedMessage - simplify
encoding/decoding
2026-02-02 15:13:06 +00:00
mfahampshire 10b6ad050b Remove unnecessary clippy flag 2026-02-02 14:28:53 +00:00
mfahampshire 5441960976 Change to use split_to on bytesmut 2026-02-02 13:14:46 +00:00
mfahampshire e6e25dacea Make send_self_pings() concurrent again 2026-02-02 12:26:41 +00:00
mfahampshire 77ab256588 Added stream mode guard + errors + example file 2026-02-02 11:42:44 +00:00
mfahampshire fa2cbb5d21 Move bincode out of nym-sphinx 2026-02-02 10:31:41 +00:00
mfahampshire c3e4f944d5 Fix mut clippy error for gateway probe 2026-02-02 09:55:38 +00:00
mfahampshire 933bbbb67d Wrapper for API stability with LP client 2026-02-02 09:55:10 +00:00
mfahampshire 3f300cc2c1 remove unused errs and imports related to IPR stream module 2026-02-02 09:32:02 +00:00
mfahampshire 30da87bf41 Fix network-requester Cargo.toml after rebase 2026-01-30 16:02:07 +00:00
mfahampshire e32783bced Remove serde requirement for MixPacket 2026-01-30 16:01:10 +00:00
mfahampshire 49687270b1 Fix future resolution problem 2026-01-30 16:00:16 +00:00
mfahampshire a49957cb5c Update Cargo.lock 2026-01-30 15:59:24 +00:00
mfahampshire 8b8c583ac5 Suppress clippy deprecated warnings for generic-array 2026-01-30 15:59:24 +00:00
mfahampshire 9d808d30c2 Update FFI shared lib for mutable sender 2026-01-30 15:59:24 +00:00
mfahampshire 880a33fb20 Update WASM client for mutable sender access 2026-01-30 15:59:24 +00:00
mfahampshire 932bd0660f Update SDK modules for &mut self and clippy fixes 2026-01-30 15:59:24 +00:00
mfahampshire 80f3ddca89 Update clients and tools for &mut self send signature 2026-01-30 15:59:24 +00:00
mfahampshire c7b0eed15f Update service providers for &mut self send signature 2026-01-30 15:55:53 +00:00
mfahampshire 9224f01d49 Implement AsyncRead/AsyncWrite/Sink for MixnetClient 2026-01-30 15:53:53 +00:00
mfahampshire 878fe85d66 Add serde, codecs, and PollSender to client-core 2026-01-30 15:53:37 +00:00
mfahampshire f060d63f2e Add serde derives and codecs for nymsphinx message types 2026-01-30 15:53:36 +00:00
78 changed files with 5292 additions and 104 deletions
Generated
+531 -3
View File
@@ -536,6 +536,19 @@ dependencies = [
"tungstenite 0.21.0",
]
[[package]]
name = "asynchronous-codec"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233"
dependencies = [
"bytes",
"futures-sink",
"futures-util",
"memchr",
"pin-project-lite",
]
[[package]]
name = "atoi"
version = "2.0.0"
@@ -703,12 +716,28 @@ dependencies = [
"url",
]
[[package]]
name = "base-x"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270"
[[package]]
name = "base16ct"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]]
name = "base256emoji"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c"
dependencies = [
"const-str 0.4.3",
"match-lookup",
]
[[package]]
name = "base64"
version = "0.13.1"
@@ -1391,6 +1420,12 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "const-str"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3"
[[package]]
name = "const-str"
version = "0.5.7"
@@ -1459,6 +1494,15 @@ dependencies = [
"rand 0.9.2",
]
[[package]]
name = "core2"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
dependencies = [
"memchr",
]
[[package]]
name = "cosmos-sdk-proto"
version = "0.27.0"
@@ -2134,6 +2178,26 @@ version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]]
name = "data-encoding-macro"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d"
dependencies = [
"data-encoding",
"data-encoding-macro-internal",
]
[[package]]
name = "data-encoding-macro-internal"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976"
dependencies = [
"data-encoding",
"syn 2.0.114",
]
[[package]]
name = "defguard_boringtun"
version = "0.6.3"
@@ -2905,6 +2969,16 @@ dependencies = [
"futures-util",
]
[[package]]
name = "futures-bounded"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91f328e7fb845fc832912fb6a34f40cf6d1888c92f974d1893a54e97b5ff542e"
dependencies = [
"futures-timer",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
@@ -2930,6 +3004,7 @@ dependencies = [
"futures-core",
"futures-task",
"futures-util",
"num_cpus",
]
[[package]]
@@ -2987,6 +3062,27 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-ticker"
version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9763058047f713632a52e916cc7f6a4b3fc6e9fc1ff8c5b1dc49e5a89041682e"
dependencies = [
"futures",
"futures-timer",
"instant",
]
[[package]]
name = "futures-timer"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
dependencies = [
"gloo-timers 0.2.6",
"send_wrapper 0.4.0",
]
[[package]]
name = "futures-util"
version = "0.3.31"
@@ -3096,6 +3192,18 @@ dependencies = [
"web-sys",
]
[[package]]
name = "gloo-timers"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
dependencies = [
"futures-channel",
"futures-core",
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "gloo-timers"
version = "0.3.0"
@@ -3383,6 +3491,12 @@ version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0"
[[package]]
name = "hex_fmt"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f"
[[package]]
name = "hickory-proto"
version = "0.25.2"
@@ -4679,6 +4793,215 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]]
name = "libp2p"
version = "0.54.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbbe80f9c7e00526cd6b838075b9c171919404a4732cb2fa8ece0a093223bfc4"
dependencies = [
"bytes",
"either",
"futures",
"futures-timer",
"getrandom 0.2.16",
"libp2p-allow-block-list",
"libp2p-connection-limits",
"libp2p-core",
"libp2p-gossipsub",
"libp2p-identify",
"libp2p-identity",
"libp2p-metrics",
"libp2p-ping",
"libp2p-swarm",
"multiaddr",
"pin-project",
"rw-stream-sink",
"thiserror 1.0.69",
]
[[package]]
name = "libp2p-allow-block-list"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1027ccf8d70320ed77e984f273bc8ce952f623762cb9bf2d126df73caef8041"
dependencies = [
"libp2p-core",
"libp2p-identity",
"libp2p-swarm",
"void",
]
[[package]]
name = "libp2p-connection-limits"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d003540ee8baef0d254f7b6bfd79bac3ddf774662ca0abf69186d517ef82ad8"
dependencies = [
"libp2p-core",
"libp2p-identity",
"libp2p-swarm",
"void",
]
[[package]]
name = "libp2p-core"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a61f26c83ed111104cd820fe9bc3aaabbac5f1652a1d213ed6e900b7918a1298"
dependencies = [
"either",
"fnv",
"futures",
"futures-timer",
"libp2p-identity",
"multiaddr",
"multihash",
"multistream-select",
"once_cell",
"parking_lot",
"pin-project",
"quick-protobuf",
"rand 0.8.5",
"rw-stream-sink",
"smallvec",
"thiserror 1.0.69",
"tracing",
"unsigned-varint 0.8.0",
"void",
"web-time",
]
[[package]]
name = "libp2p-gossipsub"
version = "0.47.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4e830fdf24ac8c444c12415903174d506e1e077fbe3875c404a78c5935a8543"
dependencies = [
"asynchronous-codec",
"base64 0.22.1",
"byteorder",
"bytes",
"either",
"fnv",
"futures",
"futures-ticker",
"getrandom 0.2.16",
"hex_fmt",
"libp2p-core",
"libp2p-identity",
"libp2p-swarm",
"prometheus-client",
"quick-protobuf",
"quick-protobuf-codec",
"rand 0.8.5",
"regex",
"sha2 0.10.9",
"smallvec",
"tracing",
"void",
"web-time",
]
[[package]]
name = "libp2p-identify"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1711b004a273be4f30202778856368683bd9a83c4c7dcc8f848847606831a4e3"
dependencies = [
"asynchronous-codec",
"either",
"futures",
"futures-bounded",
"futures-timer",
"libp2p-core",
"libp2p-identity",
"libp2p-swarm",
"lru",
"quick-protobuf",
"quick-protobuf-codec",
"smallvec",
"thiserror 1.0.69",
"tracing",
"void",
]
[[package]]
name = "libp2p-identity"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0c7892c221730ba55f7196e98b0b8ba5e04b4155651736036628e9f73ed6fc3"
dependencies = [
"bs58",
"ed25519-dalek",
"hkdf",
"multihash",
"quick-protobuf",
"rand 0.8.5",
"sha2 0.10.9",
"thiserror 2.0.17",
"tracing",
"zeroize",
]
[[package]]
name = "libp2p-metrics"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ebafa94a717c8442d8db8d3ae5d1c6a15e30f2d347e0cd31d057ca72e42566"
dependencies = [
"futures",
"libp2p-core",
"libp2p-identify",
"libp2p-identity",
"libp2p-ping",
"libp2p-swarm",
"pin-project",
"prometheus-client",
"web-time",
]
[[package]]
name = "libp2p-ping"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "005a34420359223b974ee344457095f027e51346e992d1e0dcd35173f4cdd422"
dependencies = [
"either",
"futures",
"futures-timer",
"libp2p-core",
"libp2p-identity",
"libp2p-swarm",
"rand 0.8.5",
"tracing",
"void",
"web-time",
]
[[package]]
name = "libp2p-swarm"
version = "0.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7dd6741793d2c1fb2088f67f82cf07261f25272ebe3c0b0c311e0c6b50e851a"
dependencies = [
"either",
"fnv",
"futures",
"futures-timer",
"getrandom 0.2.16",
"libp2p-core",
"libp2p-identity",
"lru",
"multistream-select",
"once_cell",
"rand 0.8.5",
"smallvec",
"tracing",
"void",
"wasm-bindgen-futures",
"web-time",
]
[[package]]
name = "libredox"
version = "0.1.12"
@@ -4758,6 +5081,15 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "lru"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
dependencies = [
"hashbrown 0.15.5",
]
[[package]]
name = "lru-slab"
version = "0.1.2"
@@ -4835,6 +5167,17 @@ dependencies = [
"web_atoms",
]
[[package]]
name = "match-lookup"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "match_token"
version = "0.35.0"
@@ -5035,6 +5378,60 @@ dependencies = [
"version_check",
]
[[package]]
name = "multiaddr"
version = "0.18.2"
source = "git+https://github.com/mfahampshire/rust-multiaddr?branch=nym-protocol#b9966d6eedd09236ee76f157f985d04e02f8b880"
dependencies = [
"arrayref",
"byteorder",
"data-encoding",
"libp2p-identity",
"multibase",
"multihash",
"percent-encoding",
"serde",
"static_assertions",
"unsigned-varint 0.8.0",
"url",
]
[[package]]
name = "multibase"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77"
dependencies = [
"base-x",
"base256emoji",
"data-encoding",
"data-encoding-macro",
]
[[package]]
name = "multihash"
version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d"
dependencies = [
"core2",
"unsigned-varint 0.8.0",
]
[[package]]
name = "multistream-select"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea0df8e5eec2298a62b326ee4f0d7fe1a6b90a09dfcf9df37b38f947a8c42f19"
dependencies = [
"bytes",
"futures",
"log",
"pin-project",
"smallvec",
"unsigned-varint 0.7.2",
]
[[package]]
name = "netlink-packet-core"
version = "0.8.1"
@@ -5522,7 +5919,7 @@ dependencies = [
"clap",
"clap_complete",
"clap_complete_fig",
"const-str",
"const-str 0.5.7",
"log",
"opentelemetry",
"opentelemetry-jaeger",
@@ -5687,12 +6084,13 @@ version = "1.20.1"
dependencies = [
"async-trait",
"base64 0.22.1",
"bincode",
"bs58",
"cfg-if",
"clap",
"comfy-table",
"futures",
"gloo-timers",
"gloo-timers 0.3.0",
"http-body-util",
"humantime",
"hyper 1.8.1",
@@ -5732,6 +6130,7 @@ dependencies = [
"tokio",
"tokio-stream",
"tokio-tungstenite",
"tokio-util",
"tokio_with_wasm",
"tracing",
"tungstenite 0.20.1",
@@ -5800,7 +6199,7 @@ version = "1.4.1"
dependencies = [
"anyhow",
"futures",
"gloo-timers",
"gloo-timers 0.3.0",
"js-sys",
"nym-bin-common",
"nym-gateway-requests",
@@ -5814,6 +6213,7 @@ dependencies = [
"serde-wasm-bindgen 0.6.5",
"serde_json",
"thiserror 2.0.17",
"tokio",
"tokio_with_wasm",
"tsify",
"wasm-bindgen",
@@ -6834,6 +7234,38 @@ dependencies = [
"thiserror 2.0.17",
]
[[package]]
name = "nym-libp2p-wasm"
version = "0.1.0"
dependencies = [
"futures",
"getrandom 0.2.16",
"gloo-timers 0.3.0",
"hex",
"js-sys",
"libp2p",
"libp2p-identify",
"libp2p-identity",
"libp2p-ping",
"libp2p-swarm",
"log",
"nym-client-wasm",
"nym-sphinx-addressing",
"nym-sphinx-anonymous-replies",
"nym-wasm-client-core",
"nym-wasm-utils",
"parking_lot",
"send_wrapper 0.6.0",
"serde",
"serde-wasm-bindgen 0.6.5",
"thiserror 2.0.17",
"tokio_with_wasm",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-bindgen-test",
"web-sys",
]
[[package]]
name = "nym-lp"
version = "0.1.0"
@@ -7101,6 +7533,7 @@ dependencies = [
"time",
"tokio",
"tokio-tungstenite",
"tokio-util",
"url",
"zeroize",
]
@@ -7714,6 +8147,7 @@ dependencies = [
"tap",
"thiserror 2.0.17",
"tokio",
"tokio-util",
"url",
]
@@ -7751,6 +8185,7 @@ dependencies = [
name = "nym-sphinx"
version = "1.20.1"
dependencies = [
"log",
"nym-crypto",
"nym-metrics",
"nym-mixnet-contract-common",
@@ -7768,6 +8203,7 @@ dependencies = [
"rand 0.8.5",
"rand_chacha 0.3.1",
"rand_distr",
"serde",
"thiserror 2.0.17",
"tokio",
"tracing",
@@ -7817,6 +8253,7 @@ dependencies = [
"nym-topology",
"rand 0.8.5",
"rand_chacha 0.3.1",
"serde",
"thiserror 2.0.17",
"tracing",
"wasm-bindgen",
@@ -7865,6 +8302,7 @@ dependencies = [
"nym-sphinx-anonymous-replies",
"nym-sphinx-params",
"nym-sphinx-types",
"serde",
"thiserror 2.0.17",
]
@@ -8005,6 +8443,7 @@ dependencies = [
"futures",
"log",
"nym-test-utils",
"serde",
"thiserror 2.0.17",
"tokio",
"tokio-util",
@@ -9372,6 +9811,29 @@ dependencies = [
"thiserror 2.0.17",
]
[[package]]
name = "prometheus-client"
version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "504ee9ff529add891127c4827eb481bd69dc0ebc72e9a682e187db4caa60c3ca"
dependencies = [
"dtoa",
"itoa",
"parking_lot",
"prometheus-client-derive-encode",
]
[[package]]
name = "prometheus-client-derive-encode"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "prost"
version = "0.13.5"
@@ -9455,6 +9917,28 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-protobuf"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d6da84cc204722a989e01ba2f6e1e276e190f22263d0cb6ce8526fcdb0d2e1f"
dependencies = [
"byteorder",
]
[[package]]
name = "quick-protobuf-codec"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15a0580ab32b169745d7a39db2ba969226ca16738931be152a3209b409de2474"
dependencies = [
"asynchronous-codec",
"bytes",
"quick-protobuf",
"thiserror 1.0.69",
"unsigned-varint 0.8.0",
]
[[package]]
name = "quinn"
version = "0.11.9"
@@ -10096,6 +10580,17 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "rw-stream-sink"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8c9026ff5d2f23da5e45bbc283f156383001bfb09c4e44256d02c1a685fe9a1"
dependencies = [
"futures",
"pin-project",
"static_assertions",
]
[[package]]
name = "ryu"
version = "1.0.22"
@@ -10298,6 +10793,21 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "send_wrapper"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0"
[[package]]
name = "send_wrapper"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
dependencies = [
"futures-core",
]
[[package]]
name = "serde"
version = "1.0.228"
@@ -12665,6 +13175,18 @@ version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "unsigned-varint"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105"
[[package]]
name = "unsigned-varint"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06"
[[package]]
name = "untrusted"
version = "0.9.0"
@@ -12888,6 +13410,12 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "void"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
[[package]]
name = "vt100"
version = "0.16.2"
+6
View File
@@ -172,6 +172,7 @@ members = [
# "wasm/full-nym-wasm", # If we uncomment this again, remember to also uncomment the profile settings below
"wasm/mix-fetch",
"wasm/node-tester",
"wasm/libp2p-nym",
"wasm/zknym-lib",
"nym-gateway-probe",
"integration-tests", "common/nym-lp-transport", "common/nym-kkt-ciphersuite",
@@ -543,6 +544,11 @@ web-sys = "0.3.76"
#[patch.crates-io]
#sphinx-packet = { path = "../sphinx" }
# Patch multiaddr with Protocol::Nym support for libp2p-nym transport
[patch.crates-io.multiaddr]
git = "https://github.com/mfahampshire/rust-multiaddr"
branch = "nym-protocol"
# Profile settings for individual crates
# Compile-time verified queries do quite a bit of work at compile time. Incremental
+2
View File
@@ -14,6 +14,7 @@ documentation.workspace = true
[dependencies]
async-trait = { workspace = true }
bincode = { workspace = true }
base64 = { workspace = true }
bs58 = { workspace = true }
clap = { workspace = true, optional = true }
@@ -29,6 +30,7 @@ sha2 = { workspace = true }
si-scale = { workspace = true }
thiserror = { workspace = true }
url = { workspace = true, features = ["serde"] }
tokio-util = { workspace = true, features = ["codec"] }
time = { workspace = true }
tokio = { workspace = true, features = ["sync", "macros"] }
tracing = { workspace = true }
@@ -36,6 +36,7 @@ use crate::init::{
types::{GatewaySetup, InitialisationResult},
};
use futures::channel::mpsc;
use futures::SinkExt;
use nym_bandwidth_controller::BandwidthController;
use nym_client_core_config_types::{ForgetMe, RememberMe};
use nym_client_core_gateways_storage::{GatewayDetails, GatewaysDetailsStore};
@@ -66,7 +67,7 @@ use std::os::raw::c_int as RawFd;
use std::path::Path;
use std::sync::Arc;
use time::OffsetDateTime;
use tokio::sync::mpsc::Sender;
use tokio_util::sync::{PollSendError, PollSender};
use url::Url;
#[cfg(target_arch = "wasm32")]
@@ -112,10 +113,7 @@ pub struct ClientInput {
}
impl ClientInput {
pub async fn send(
&self,
message: InputMessage,
) -> Result<(), tokio::sync::mpsc::error::SendError<InputMessage>> {
pub async fn send(&mut self, message: InputMessage) -> Result<(), PollSendError<InputMessage>> {
self.input_sender.send(message).await
}
}
@@ -745,7 +743,7 @@ where
config: &Config,
user_agent: Option<UserAgent>,
client_stats_id: String,
input_sender: Sender<InputMessage>,
input_sender: PollSender<InputMessage>,
shutdown_tracker: &ShutdownTracker,
) -> ClientStatsSender {
tracing::info!("Starting statistics control...");
@@ -1013,7 +1011,7 @@ where
&self.config,
self.user_agent.clone(),
generate_client_stats_id(*self_address.identity()),
input_sender.clone(),
tokio_util::sync::PollSender::new(input_sender.clone()),
&shutdown_tracker.clone(),
);
@@ -1139,7 +1137,7 @@ where
client_input: ClientInputStatus::AwaitingProducer {
client_input: ClientInput {
connection_command_sender: client_connection_tx,
input_sender,
input_sender: PollSender::new(input_sender),
client_request_sender,
},
},
@@ -1,22 +1,36 @@
// Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::ClientCoreError;
use crate::make_bincode_serializer;
use bincode::Options;
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::forwarding::packet::MixPacket;
use nym_sphinx::params::PacketType;
use nym_sphinx::receiver::ReconstructedMessage;
use nym_task::connections::TransmissionLane;
use serde::{Deserialize, Serialize};
use std::convert::TryInto;
use tokio_util::{
bytes::Buf,
bytes::BytesMut,
codec::{Decoder, Encoder},
};
pub type InputMessageSender = tokio::sync::mpsc::Sender<InputMessage>;
pub type InputMessageSender = tokio_util::sync::PollSender<InputMessage>;
pub type InputMessageReceiver = tokio::sync::mpsc::Receiver<InputMessage>;
#[derive(Debug)]
const LENGHT_ENCODING_PREFIX_SIZE: usize = 4;
#[derive(Serialize, Deserialize, 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.
///
/// Packets are stored as pre-serialized bytes, which avoids
/// requiring Serde on MixPacket.
Premade {
msgs: Vec<MixPacket>,
packet_bytes: Vec<Vec<u8>>,
lane: TransmissionLane,
},
@@ -65,12 +79,16 @@ pub enum InputMessage {
}
impl InputMessage {
pub fn simple(data: &[u8], recipient: Recipient) -> Self {
InputMessage::new_regular(recipient, data.to_vec(), TransmissionLane::General, None)
}
pub fn new_premade(
msgs: Vec<MixPacket>,
packet_bytes: Vec<Vec<u8>>,
lane: TransmissionLane,
packet_type: PacketType,
) -> Self {
let message = InputMessage::Premade { msgs, lane };
let message = InputMessage::Premade { packet_bytes, lane };
if packet_type == PacketType::Mix {
message
} else {
@@ -185,4 +203,394 @@ impl InputMessage {
self.set_max_retransmissions(max_retransmissions);
self
}
#[allow(clippy::expect_used)]
pub fn serialized_size(&self) -> u64 {
make_bincode_serializer()
.serialized_size(self)
.expect("failed to get serialized InputMessage size")
+ LENGHT_ENCODING_PREFIX_SIZE as u64
}
}
pub struct AdressedInputMessageCodec(pub Recipient);
impl Encoder<&[u8]> for AdressedInputMessageCodec {
type Error = ClientCoreError;
fn encode(&mut self, item: &[u8], buf: &mut BytesMut) -> Result<(), Self::Error> {
let mut codec = InputMessageCodec;
let input_message = InputMessage::simple(item, self.0);
codec.encode(input_message, buf)?;
Ok(())
}
}
pub struct InputMessageCodec;
impl Encoder<InputMessage> for InputMessageCodec {
type Error = ClientCoreError;
fn encode(&mut self, item: InputMessage, buf: &mut BytesMut) -> Result<(), Self::Error> {
#[allow(clippy::expect_used)]
let encoded = make_bincode_serializer().serialize(&item)?;
let encoded_len = encoded.len() as u32;
let mut encoded_with_len = encoded_len.to_le_bytes().to_vec();
encoded_with_len.extend(encoded);
buf.reserve(encoded_with_len.len());
buf.extend_from_slice(&encoded_with_len);
Ok(())
}
}
impl Decoder for InputMessageCodec {
type Item = InputMessage;
type Error = ClientCoreError;
fn decode(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
if buf.len() < LENGHT_ENCODING_PREFIX_SIZE {
return Ok(None);
}
#[allow(clippy::expect_used)]
let len = u32::from_le_bytes(buf[0..LENGHT_ENCODING_PREFIX_SIZE].try_into()?) as usize;
if buf.len() < len + LENGHT_ENCODING_PREFIX_SIZE {
return Ok(None);
}
let decoded = make_bincode_serializer()
.deserialize(&buf[LENGHT_ENCODING_PREFIX_SIZE..len + LENGHT_ENCODING_PREFIX_SIZE])?;
buf.advance(len + LENGHT_ENCODING_PREFIX_SIZE);
Ok(Some(decoded))
}
}
/// Codec for encoding/decoding `ReconstructedMessage` for the AsyncRead interface.
///
/// This codec was moved from `nymsphinx::receiver` to keep bincode serialization
/// out of the core sphinx crate.
///
/// Uses a simple length-prefixed binary format:
/// - 4 bytes: length of encoded message
/// - N bytes: encoded message using `ReconstructedMessage::encode()`
///
/// The message encoding format is:
/// - Without sender_tag: `[0][payload...]`
/// - With sender_tag: `[1][16-byte tag][payload...]`
pub struct ReconstructedMessageCodec;
impl Encoder<ReconstructedMessage> for ReconstructedMessageCodec {
type Error = ClientCoreError;
fn encode(
&mut self,
item: ReconstructedMessage,
buf: &mut BytesMut,
) -> Result<(), Self::Error> {
let encoded = item.encode();
let encoded_len = encoded.len() as u32;
buf.reserve(LENGHT_ENCODING_PREFIX_SIZE + encoded.len());
buf.extend_from_slice(&encoded_len.to_le_bytes());
buf.extend_from_slice(&encoded);
Ok(())
}
}
impl Decoder for ReconstructedMessageCodec {
type Item = ReconstructedMessage;
type Error = ClientCoreError;
fn decode(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
if buf.len() < LENGHT_ENCODING_PREFIX_SIZE {
return Ok(None);
}
let len = u32::from_le_bytes(buf[0..LENGHT_ENCODING_PREFIX_SIZE].try_into()?) as usize;
if buf.len() < len + LENGHT_ENCODING_PREFIX_SIZE {
return Ok(None);
}
let decoded = ReconstructedMessage::decode(
&buf[LENGHT_ENCODING_PREFIX_SIZE..len + LENGHT_ENCODING_PREFIX_SIZE],
)
.map_err(|e| ClientCoreError::CodecError(e.to_string()))?;
buf.advance(len + LENGHT_ENCODING_PREFIX_SIZE);
Ok(Some(decoded))
}
}
#[cfg(test)]
mod tests {
use super::*;
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::params::PacketType;
use rand::SeedableRng;
fn test_recipient() -> Recipient {
Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@4sBbL1ngf1vtNqykydQKTFh26sQCw888GpUqvPvyNB4f").unwrap()
}
fn test_sender_tag() -> AnonymousSenderTag {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
AnonymousSenderTag::new_random(&mut rng)
}
#[test]
fn encode_decode_all_variants() {
let mut codec = InputMessageCodec;
{
let mut buf = BytesMut::new();
let msg = InputMessage::new_anonymous(
test_recipient(),
vec![1, 2, 3, 4, 5],
3,
TransmissionLane::General,
None,
);
codec.encode(msg, &mut buf).unwrap();
let decoded = codec
.decode(&mut buf)
.unwrap()
.expect("Should decode message");
match decoded {
InputMessage::Anonymous {
data, reply_surbs, ..
} => {
assert_eq!(data, vec![1, 2, 3, 4, 5]);
assert_eq!(reply_surbs, 3);
}
_ => panic!("Expected Anonymous variant"),
}
}
{
let mut buf = BytesMut::new();
let msg = InputMessage::new_reply(
test_sender_tag(),
vec![6, 7, 8],
TransmissionLane::General,
None,
);
codec.encode(msg, &mut buf).unwrap();
let decoded = codec
.decode(&mut buf)
.unwrap()
.expect("Should decode message");
match decoded {
InputMessage::Reply { data, .. } => {
assert_eq!(data, vec![6, 7, 8]);
}
_ => panic!("Expected Reply variant"),
}
}
{
let mut buf = BytesMut::new();
let inner = InputMessage::new_anonymous(
test_recipient(),
vec![9, 10],
2,
TransmissionLane::General,
None,
);
let msg = InputMessage::new_wrapper(inner, PacketType::Mix);
codec.encode(msg, &mut buf).unwrap();
let decoded = codec
.decode(&mut buf)
.unwrap()
.expect("Should decode message");
match decoded {
InputMessage::MessageWrapper {
message,
packet_type,
} => {
assert_eq!(packet_type, PacketType::Mix);
match *message {
InputMessage::Anonymous {
data, reply_surbs, ..
} => {
assert_eq!(data, vec![9, 10]);
assert_eq!(reply_surbs, 2);
}
_ => panic!("Expected Anonymous inner message"),
}
}
_ => panic!("Expected MessageWrapper variant"),
}
}
}
#[test]
fn encode_decode_sequential_messages() {
let mut codec = InputMessageCodec;
let mut buf = BytesMut::new();
codec
.encode(
InputMessage::new_anonymous(
test_recipient(),
vec![1, 2, 3],
1,
TransmissionLane::General,
None,
),
&mut buf,
)
.unwrap();
codec
.encode(
InputMessage::new_anonymous(
test_recipient(),
vec![4, 5, 6, 7],
2,
TransmissionLane::General,
None,
),
&mut buf,
)
.unwrap();
codec
.encode(
InputMessage::new_anonymous(
test_recipient(),
vec![8, 9],
3,
TransmissionLane::General,
None,
),
&mut buf,
)
.unwrap();
let decoded1 = codec
.decode(&mut buf)
.unwrap()
.expect("Should decode first message");
match decoded1 {
InputMessage::Anonymous {
data, reply_surbs, ..
} => {
assert_eq!(data, vec![1, 2, 3]);
assert_eq!(reply_surbs, 1);
}
_ => panic!("Wrong variant"),
}
let decoded2 = codec
.decode(&mut buf)
.unwrap()
.expect("Should decode second message");
match decoded2 {
InputMessage::Anonymous {
data, reply_surbs, ..
} => {
assert_eq!(data, vec![4, 5, 6, 7]);
assert_eq!(reply_surbs, 2);
}
_ => panic!("Wrong variant"),
}
let decoded3 = codec
.decode(&mut buf)
.unwrap()
.expect("Should decode third message");
match decoded3 {
InputMessage::Anonymous {
data, reply_surbs, ..
} => {
assert_eq!(data, vec![8, 9]);
assert_eq!(reply_surbs, 3);
}
_ => panic!("Wrong variant"),
}
// Buffer should be empty
let decoded4 = codec.decode(&mut buf).unwrap();
assert!(decoded4.is_none(), "Should have no more messages");
assert_eq!(buf.len(), 0, "Buffer should be empty");
}
#[test]
fn partial_message_handling() {
let mut codec = InputMessageCodec;
let mut buf = BytesMut::new();
// Empty @ beginning
assert!(codec.decode(&mut buf).unwrap().is_none());
let mut buf = BytesMut::from(&[0x10, 0x00][..]);
assert!(codec.decode(&mut buf).unwrap().is_none());
assert_eq!(buf.len(), 2, "Buffer should be unchanged");
let mut full_buf = BytesMut::new();
codec
.encode(
InputMessage::new_anonymous(
test_recipient(),
vec![1, 2, 3, 4, 5],
2,
TransmissionLane::General,
None,
),
&mut full_buf,
)
.unwrap();
// Only first half of the message
let partial_len = full_buf.len() / 2;
let mut partial_buf = full_buf.split_to(partial_len);
assert!(codec.decode(&mut partial_buf).unwrap().is_none());
assert_eq!(partial_buf.len(), partial_len, "Buffer should be unchanged");
partial_buf.unsplit(full_buf);
let decoded = codec.decode(&mut partial_buf).unwrap();
assert!(decoded.is_some(), "Should decode complete message");
match decoded.unwrap() {
InputMessage::Anonymous { data, .. } => {
assert_eq!(data, vec![1, 2, 3, 4, 5]);
}
_ => panic!("Expected Anonymous variant"),
}
}
#[test]
fn addressed_codec_compatibility() {
let recipient = test_recipient();
let data = b"test message payload";
let mut addressed_codec = AdressedInputMessageCodec(recipient);
let mut buf = BytesMut::new();
addressed_codec.encode(data.as_ref(), &mut buf).unwrap();
let mut input_codec = InputMessageCodec;
let decoded = input_codec
.decode(&mut buf)
.unwrap()
.expect("Should decode");
match decoded {
InputMessage::Regular {
data: decoded_data,
recipient: decoded_recipient,
lane,
..
} => {
assert_eq!(decoded_data, data, "Data should match");
assert_eq!(decoded_recipient, recipient, "Recipient should match");
assert_eq!(lane, TransmissionLane::General, "Should use General lane");
}
_ => panic!("Expected Regular variant"),
}
}
}
@@ -45,7 +45,25 @@ where
}
}
async fn handle_premade_packets(&mut self, packets: Vec<MixPacket>, lane: TransmissionLane) {
async fn handle_premade_packets(&mut self, packet_bytes: Vec<Vec<u8>>, lane: TransmissionLane) {
// Deserialize packet bytes back to MixPacket
let packets: Vec<MixPacket> = packet_bytes
.into_iter()
.filter_map(|bytes| {
MixPacket::try_from_v2_bytes(&bytes)
.map_err(|e| {
warn!("Failed to deserialize premade packet: {}", e);
e
})
.ok()
})
.collect();
if packets.is_empty() {
warn!("No valid premade packets to send");
return;
}
self.message_handler
.send_premade_mix_packets(
packets
@@ -156,7 +174,9 @@ where
self.handle_reply(recipient_tag, data, lane, max_retransmissions)
.await;
}
InputMessage::Premade { msgs, lane } => self.handle_premade_packets(msgs, lane).await,
InputMessage::Premade { packet_bytes, lane } => {
self.handle_premade_packets(packet_bytes, lane).await
}
InputMessage::MessageWrapper {
message,
packet_type,
@@ -202,8 +222,8 @@ where
self.handle_reply(recipient_tag, data, lane, max_retransmissions)
.await;
}
InputMessage::Premade { msgs, lane } => {
self.handle_premade_packets(msgs, lane).await
InputMessage::Premade { packet_bytes, lane } => {
self.handle_premade_packets(packet_bytes, lane).await
}
// MessageWrappers can't be nested
InputMessage::MessageWrapper { .. } => {
@@ -17,7 +17,7 @@
#![warn(clippy::dbg_macro)]
use crate::client::inbound_messages::{InputMessage, InputMessageSender};
use futures::StreamExt;
use futures::{SinkExt, StreamExt};
use nym_client_core_config_types::StatsReporting;
use nym_sphinx::addressing::Recipient;
use nym_statistics_common::clients::{
+9
View File
@@ -255,6 +255,15 @@ pub enum ClientCoreError {
#[error("Could not access task registry, {0}")]
RegistryAccess(#[from] RegistryAccessError),
#[error("Serialization error: {0}")]
BincodeError(#[from] Box<bincode::ErrorKind>),
#[error("Could not coarce to array")]
ArrayCreationFailure(#[from] std::array::TryFromSliceError),
#[error("Codec error: {0}")]
CodecError(String),
}
impl From<tungstenite::Error> for ClientCoreError {
+8
View File
@@ -1,3 +1,4 @@
#![allow(deprecated)] // silences clippy warning: use of deprecated associated function `nym_crypto::generic_array::GenericArray::<T, N>::clone_from_slice`: please upgrade to generic-array 1.x - TODO
use std::future::Future;
#[cfg(all(
@@ -39,3 +40,10 @@ where
{
tokio::spawn(future);
}
fn make_bincode_serializer() -> impl bincode::Options {
use bincode::Options;
bincode::DefaultOptions::new()
.with_big_endian()
.with_varint_encoding()
}
@@ -1,6 +1,7 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![allow(deprecated)] // silences clippy warning: use of deprecated associated function `nym_crypto::generic_array::GenericArray::<T, N>::from_exact_iter`: please upgrade to generic-array 1.x - TODO
pub use backend::*;
pub use combined::CombinedReplyStorage;
pub use key_storage::SentReplyKeys;
@@ -1,6 +1,8 @@
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![allow(clippy::derivable_impls)]
// MAX: surpressing warning for the moment, will be dealt with in a different PR (TODO)
use cosmwasm_schema::cw_serde;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
+2
View File
@@ -1,6 +1,8 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![allow(deprecated)] // silences clippy warning: deprecated associated function `generic_array::GenericArray::<T, N>::from_exact_iter`: please upgrade to generic-array 1.x - TODO
#[cfg(feature = "asymmetric")]
pub mod asymmetric;
pub mod bech32_address_validation;
+1
View File
@@ -1,5 +1,6 @@
// Copyright 2020-2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![allow(deprecated)] // silences clippy warning: deprecated associated function `nym_crypto::generic_array::GenericArray::<T, N>::clone_from_slice`: please upgrade to generic-array 1.x - TODO
pub use nym_crypto::generic_array;
use nym_crypto::OutputSizeUser;
@@ -70,7 +70,7 @@ impl BinaryRequest {
let plaintext = match self {
BinaryRequest::ForwardSphinx { packet } => packet.into_v1_bytes()?,
BinaryRequest::ForwardSphinxV2 { packet } => packet.into_v2_bytes()?,
BinaryRequest::ForwardSphinxV2 { packet } => packet.to_v2_bytes()?,
};
BinaryData::make_encrypted_blob(kind as u8, &plaintext, shared_key)
+3
View File
@@ -1,6 +1,9 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![allow(deprecated)]
// silences clippy warning: use of deprecated tuple variant `HttpClientError::GenericRequestFailure`: use another more strongly typed variant - this variant is only left for compatibility reasons - TODO
//! Nym HTTP API Client
//!
//! Centralizes and implements the core API client functionality. This crate provides custom,
@@ -28,7 +28,7 @@ impl MixnetConnectionBeacon {
}
}
async fn send_mixnet_self_ping(&self) -> Result<u64> {
async fn send_mixnet_self_ping(&mut self) -> Result<u64> {
trace!("Sending mixnet self ping");
let (input_message, request_id) = create_self_ping(self.our_address);
self.mixnet_client_sender
@@ -38,7 +38,7 @@ impl MixnetConnectionBeacon {
Ok(request_id)
}
pub async fn run(self, shutdown: CancellationToken) -> Result<()> {
pub async fn run(mut self, shutdown: CancellationToken) -> Result<()> {
debug!("Mixnet connection beacon is running");
let mut ping_interval = tokio::time::interval(MIXNET_SELF_PING_INTERVAL);
loop {
@@ -3,7 +3,6 @@
use std::time::Duration;
use futures::StreamExt;
use nym_sdk::mixnet::{MixnetClient, MixnetMessageSender, Recipient};
use tracing::{debug, error};
@@ -22,23 +21,25 @@ pub async fn self_ping_and_wait(
wait_for_self_ping_return(mixnet_client, &request_ids).await
}
async fn send_self_pings(our_address: Recipient, mixnet_client: &MixnetClient) -> Result<Vec<u64>> {
// Send pings
let request_ids = futures::stream::iter(1..=3)
.then(|_| async {
async fn send_self_pings(
our_address: Recipient,
mixnet_client: &mut MixnetClient,
) -> Result<Vec<u64>> {
let sender = mixnet_client.split_sender();
let futures = (1..=3).map(|_| {
let mut sender = sender.clone();
async move {
let (input_message, request_id) = create_self_ping(our_address);
mixnet_client
sender
.send(input_message)
.await
.map_err(|err| Error::NymSdkError(Box::new(err)))?;
Ok::<u64, Error>(request_id)
})
.collect::<Vec<_>>()
.await;
.map_err(|e| Error::NymSdkError(Box::new(e)))?;
Ok::<_, Error>(request_id)
}
});
// Check the vec of results and return the first error, if any. If there are not errors, unwrap
// all the results into a vec of u64s.
request_ids.into_iter().collect::<Result<Vec<_>>>()
futures::future::try_join_all(futures).await
}
async fn wait_for_self_ping_return(
+4 -8
View File
@@ -13,6 +13,8 @@ rand = { workspace = true }
rand_distr = { workspace = true }
rand_chacha = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true, features = ["derive"] }
log = { workspace = true }
nym-sphinx-acknowledgements = { workspace = true }
nym-sphinx-addressing = { workspace = true }
@@ -47,11 +49,5 @@ features = ["sync"]
[features]
default = ["sphinx"]
sphinx = [
"nym-sphinx-params/sphinx",
"nym-sphinx-types/sphinx",
]
outfox = [
"nym-sphinx-params/outfox",
"nym-sphinx-types/outfox",
]
sphinx = ["nym-sphinx-params/sphinx", "nym-sphinx-types/sphinx"]
outfox = ["nym-sphinx-params/outfox", "nym-sphinx-types/outfox"]
@@ -1,5 +1,6 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![allow(deprecated)] // silences clippy warning: deprecated associated function `nym_crypto::generic_array::GenericArray::<T, N>::clone_from_slice`: please upgrade to generic-array 1.x - TODO
pub mod identifier;
pub mod key;
@@ -12,6 +12,7 @@ rand = { workspace = true }
bs58 = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
serde = { workspace = true }
nym-crypto = { workspace = true, features = ["stream_cipher", "rand"] }
nym-sphinx-addressing = { workspace = true }
@@ -1,6 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![allow(deprecated)] // silences clippy warning: deprecated struct `nym_crypto::generic_array::GenericArray`: please upgrade to generic-array 1.x - TODO
pub mod encryption_key;
pub mod reply_surb;
pub mod requests;
@@ -7,6 +7,7 @@ use crate::{ReplySurbError, ReplySurbWithKeyRotation};
use nym_sphinx_addressing::clients::{Recipient, RecipientFormattingError};
use nym_sphinx_params::key_rotation::InvalidSphinxKeyRotation;
use rand::{CryptoRng, RngCore};
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::mem;
use thiserror::Error;
@@ -30,7 +31,7 @@ pub enum InvalidAnonymousSenderTagRepresentation {
InvalidLength { received: usize, expected: usize },
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
pub struct AnonymousSenderTag([u8; SENDER_TAG_SIZE]);
+1
View File
@@ -13,3 +13,4 @@ nym-sphinx-params = { workspace = true }
nym-sphinx-types = { workspace = true, features = ["sphinx", "outfox"] }
nym-sphinx-anonymous-replies = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true }
+5 -3
View File
@@ -174,13 +174,15 @@ impl MixPacket {
})
}
pub fn into_v2_bytes(self) -> Result<Vec<u8>, MixPacketFormattingError> {
pub fn to_v2_bytes(&self) -> Result<Vec<u8>, MixPacketFormattingError> {
Ok(std::iter::once(self.packet_type as u8)
.chain(std::iter::once(self.key_rotation as u8))
.chain(self.next_hop.as_bytes())
.chain(self.packet.to_bytes()?)
.collect())
}
}
// TODO: test for serialization and errors!
pub fn into_v2_bytes(self) -> Result<Vec<u8>, MixPacketFormattingError> {
self.to_v2_bytes()
}
}
+149 -2
View File
@@ -1,6 +1,8 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::io;
use crate::message::{NymMessage, NymMessageError, PaddedMessage, PlainMessage};
use nym_crypto::aes::cipher::{KeyIvInit, StreamCipher};
use nym_crypto::asymmetric::x25519;
@@ -9,6 +11,7 @@ use nym_crypto::symmetric::stream_cipher;
use nym_crypto::symmetric::stream_cipher::CipherKey;
use nym_sphinx_anonymous_replies::SurbEncryptionKey;
use nym_sphinx_anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx_anonymous_replies::requests::SENDER_TAG_SIZE;
use nym_sphinx_chunking::ChunkingError;
use nym_sphinx_chunking::fragment::Fragment;
use nym_sphinx_chunking::reconstruction::MessageReconstructor;
@@ -17,8 +20,27 @@ use nym_sphinx_params::{
};
use thiserror::Error;
// TODO: should this live in this file?
#[derive(Debug)]
/// Error when decoding a `ReconstructedMessage` from bytes.
#[derive(Debug, Error)]
pub enum ReconstructedMessageError {
#[error("Not enough bytes to decode message: expected at least {expected}, got {received}")]
TooShort { expected: usize, received: usize },
#[error("Invalid sender tag flag: expected 0 or 1, got {0}")]
InvalidSenderTagFlag(u8),
}
/// A message that has been reconstructed from sphinx packets.
///
/// Format:
/// This type uses a simple binary encoding for serialization:
/// - Without sender_tag: `[0][payload...]`
/// - With sender_tag: `[1][16-byte tag][payload...]`
///
/// The first byte indicates whether a sender tag is present (1) or not (0).
/// If present, it is followed by the 16-byte sender tag. The remaining bytes
/// are the message payload.
#[derive(Debug, Clone)]
pub struct ReconstructedMessage {
/// The actual plaintext message that was received.
pub message: Vec<u8>,
@@ -45,6 +67,71 @@ impl ReconstructedMessage {
pub fn into_inner(self) -> (Vec<u8>, Option<AnonymousSenderTag>) {
self.into()
}
/// Encodes this message into bytes.
///
/// Format:
/// - Without sender_tag: `[0][payload...]`
/// - With sender_tag: `[1][16-byte tag][payload...]`
pub fn encode(&self) -> Vec<u8> {
match &self.sender_tag {
Some(tag) => {
let mut buf = Vec::with_capacity(1 + SENDER_TAG_SIZE + self.message.len());
buf.push(1);
buf.extend_from_slice(&tag.to_bytes());
buf.extend_from_slice(&self.message);
buf
}
None => {
let mut buf = Vec::with_capacity(1 + self.message.len());
buf.push(0);
buf.extend_from_slice(&self.message);
buf
}
}
}
/// Decodes a message from bytes.
///
/// Format:
/// - Without sender_tag: `[0][payload...]`
/// - With sender_tag: `[1][16-byte tag][payload...]`
pub fn decode(bytes: &[u8]) -> Result<Self, ReconstructedMessageError> {
if bytes.is_empty() {
return Err(ReconstructedMessageError::TooShort {
expected: 1,
received: 0,
});
}
match bytes[0] {
0 => Ok(ReconstructedMessage {
message: bytes[1..].to_vec(),
sender_tag: None,
}),
1 => {
if bytes.len() < 1 + SENDER_TAG_SIZE {
return Err(ReconstructedMessageError::TooShort {
expected: 1 + SENDER_TAG_SIZE,
received: bytes.len(),
});
}
let tag_bytes: [u8; SENDER_TAG_SIZE] = bytes[1..1 + SENDER_TAG_SIZE]
.try_into()
.expect("slice length verified above");
Ok(ReconstructedMessage {
message: bytes[1 + SENDER_TAG_SIZE..].to_vec(),
sender_tag: Some(AnonymousSenderTag::from_bytes(tag_bytes)),
})
}
flag => Err(ReconstructedMessageError::InvalidSenderTagFlag(flag)),
}
}
/// Returns the encoded size of this message.
pub fn encoded_size(&self) -> usize {
1 + self.sender_tag.map_or(0, |_| SENDER_TAG_SIZE) + self.message.len()
}
}
impl From<PlainMessage> for ReconstructedMessage {
@@ -75,6 +162,9 @@ pub enum MessageRecoveryError {
#[error("Failed to recover message fragment - {0}")]
FragmentRecoveryError(#[from] ChunkingError),
#[error("Failed to recover message fragment - {0}")]
MessageRecoveryError(#[from] io::Error),
}
pub trait MessageReceiver {
@@ -185,3 +275,60 @@ impl MessageReceiver for SphinxMessageReceiver {
&mut self.reconstructor
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_decode_without_sender_tag() {
let msg = ReconstructedMessage {
message: b"hello world".to_vec(),
sender_tag: None,
};
let encoded = msg.encode();
assert_eq!(encoded[0], 0); // flag byte
assert_eq!(&encoded[1..], b"hello world");
let decoded = ReconstructedMessage::decode(&encoded).unwrap();
assert_eq!(decoded.message, b"hello world");
assert!(decoded.sender_tag.is_none());
}
#[test]
fn encode_decode_with_sender_tag() {
let tag = AnonymousSenderTag::from_bytes([42u8; SENDER_TAG_SIZE]);
let msg = ReconstructedMessage {
message: b"hello world".to_vec(),
sender_tag: Some(tag),
};
let encoded = msg.encode();
assert_eq!(encoded[0], 1); // flag byte
assert_eq!(&encoded[1..1 + SENDER_TAG_SIZE], &[42u8; SENDER_TAG_SIZE]);
assert_eq!(&encoded[1 + SENDER_TAG_SIZE..], b"hello world");
let decoded = ReconstructedMessage::decode(&encoded).unwrap();
assert_eq!(decoded.message, b"hello world");
assert_eq!(
decoded.sender_tag.unwrap().to_bytes(),
[42u8; SENDER_TAG_SIZE]
);
}
#[test]
fn encoded_size_matches() {
let msg_no_tag = ReconstructedMessage {
message: b"test".to_vec(),
sender_tag: None,
};
assert_eq!(msg_no_tag.encoded_size(), msg_no_tag.encode().len());
let msg_with_tag = ReconstructedMessage {
message: b"test".to_vec(),
sender_tag: Some(AnonymousSenderTag::from_bytes([0u8; SENDER_TAG_SIZE])),
};
assert_eq!(msg_with_tag.encoded_size(), msg_with_tag.encode().len());
}
}
+1
View File
@@ -23,6 +23,7 @@ serde = { workspace = true, features = ["derive"] } # for config serialization/d
tap = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "net", "signal"] }
tokio-util = { workspace = true }
url = { workspace = true }
nym-bandwidth-controller = { workspace = true }
@@ -7,6 +7,7 @@ use super::{SocksVersion, RESERVED, SOCKS4_VERSION, SOCKS5_VERSION};
use crate::config;
use futures::channel::mpsc;
use futures::task::{Context, Poll};
use futures::SinkExt;
use log::*;
use nym_client_core::client::inbound_messages::{InputMessage, InputMessageSender};
use nym_service_providers_common::interface::{ProviderInterfaceVersion, RequestVersion};
@@ -3,11 +3,12 @@
use crate::proxy_runner::MixProxySender;
use bytes::Bytes;
use futures::SinkExt;
use log::{debug, error};
use nym_socks5_requests::{ConnectionId, SocketData};
use std::io;
pub(crate) struct OrderedMessageSender<F, S> {
pub(crate) struct OrderedMessageSender<F, S: Send + 'static> {
connection_id: ConnectionId,
// addresses are provided for better logging
local_destination_address: String,
@@ -18,7 +19,7 @@ pub(crate) struct OrderedMessageSender<F, S> {
mix_message_adapter: F,
}
impl<F, S> OrderedMessageSender<F, S>
impl<F, S: Send + 'static> OrderedMessageSender<F, S>
where
F: Fn(SocketData) -> S,
{
@@ -55,7 +56,7 @@ where
(self.mix_message_adapter)(data)
}
async fn send_message(&self, message: S) {
async fn send_message(&mut self, message: S) {
if self.mixnet_sender.send(message).await.is_err() {
panic!("BatchRealMessageReceiver has stopped receiving!")
}
@@ -74,7 +74,7 @@ async fn wait_for_lane(
}
}
pub(super) async fn run_inbound<F, S>(
pub(super) async fn run_inbound<F, S: Send>(
mut reader: OwnedReadHalf,
mut message_sender: OrderedMessageSender<F, S>,
connection_id: ConnectionId,
@@ -9,6 +9,7 @@ use nym_task::ShutdownTracker;
use std::fmt::Debug;
use std::{sync::Arc, time::Duration};
use tokio::{net::TcpStream, sync::Notify};
use tokio_util::sync::PollSender;
mod inbound;
mod outbound;
@@ -35,7 +36,7 @@ impl From<(Vec<u8>, bool)> for ProxyMessage {
}
}
pub type MixProxySender<S> = tokio::sync::mpsc::Sender<S>;
pub type MixProxySender<S> = PollSender<S>;
pub type MixProxyReader<S> = tokio::sync::mpsc::Receiver<S>;
// TODO: when we finally get to implementing graceful shutdown,
+1
View File
@@ -1,6 +1,7 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![allow(deprecated)]
use aes_gcm::aead::{Aead, Nonce};
use aes_gcm::{AeadCore, AeadInPlace, KeyInit};
use rand::{thread_rng, CryptoRng, Fill, RngCore};
+1
View File
@@ -15,6 +15,7 @@ thiserror = { workspace = true }
tokio = { workspace = true, features = ["macros", "sync"] }
tokio-util = { workspace = true, features = ["rt"] }
tracing = { workspace = true }
serde = { workspace = true }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
workspace = true
+2 -1
View File
@@ -2,13 +2,14 @@
// SPDX-License-Identifier: Apache-2.0
use futures::channel::mpsc;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
// const LANE_CONSIDERED_CLEAR: usize = 10;
pub type ConnectionId = u64;
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub enum TransmissionLane {
General,
// we need to treat surb-related requests and responses at higher priority
@@ -928,7 +928,7 @@ impl MixnetListener {
// When an incoming mixnet message triggers a response that we send back.
async fn handle_response(
&self,
&mut self,
response: Vec<u8>,
recipient: Option<Recipient>,
sender_tag: Option<AnonymousSenderTag>,
+2 -2
View File
@@ -20,7 +20,7 @@ pub fn icmp_identifier() -> u16 {
}
pub async fn send_ping_v4(
mixnet_client: &MixnetClient,
mixnet_client: &mut MixnetClient,
our_ips: IpPair,
sequence_number: u16,
destination: Ipv4Addr,
@@ -42,7 +42,7 @@ pub async fn send_ping_v4(
}
pub async fn send_ping_v6(
mixnet_client: &MixnetClient,
mixnet_client: &mut MixnetClient,
our_ips: IpPair,
sequence_number: u16,
destination: Ipv6Addr,
+1 -1
View File
@@ -532,7 +532,7 @@ pub async fn do_ping_exit(
}
async fn send_icmp_pings(
mixnet_client: &MixnetClient,
mixnet_client: &mut MixnetClient,
our_ips: IpPair,
exit_router_address: Recipient,
) -> anyhow::Result<()> {
+1 -3
View File
@@ -30,8 +30,6 @@ enum ConnectionState {
Disconnected,
Connecting,
Connected,
#[allow(unused)]
Disconnecting,
}
pub struct IprClientConnect {
@@ -83,7 +81,7 @@ impl IprClientConnect {
self.listen_for_connect_response(request_id).await
}
async fn send_connect_request(&self, ip_packet_router_address: Recipient) -> Result<u64> {
async fn send_connect_request(&mut self, ip_packet_router_address: Recipient) -> Result<u64> {
let (request, request_id) = IpPacketRequest::new_connect_request(None);
// We use 20 surbs for the connect request because typically the IPR is configured to have
+7
View File
@@ -18,6 +18,9 @@ pub enum Error {
)]
ReceivedResponseWithNewVersion { expected: u8, received: u8 },
#[error("got reply for connect request, but it appears intended for the wrong address?")]
GotReplyIntendedForWrongAddress,
#[error("unexpected connect response")]
UnexpectedConnectResponse,
@@ -41,6 +44,10 @@ pub enum Error {
#[error(transparent)]
Bincode(#[from] bincode::Error),
#[error("failed to create connect request")]
FailedToCreateConnectRequest {
source: nym_ip_packet_requests::sign::SignatureError,
},
}
// Result type based on our error type
+1 -1
View File
@@ -220,7 +220,7 @@ async fn send_receive_mixnet(state: AppState) -> Result<String, StatusCode> {
});
let send_handle = tokio::spawn(async move {
let mixnet_sender = sender.read().await.split_sender();
let mut mixnet_sender = sender.read().await.split_sender();
let our_address = *sender.read().await.nym_address();
match timeout(
Duration::from_secs(5),
+3
View File
@@ -1,3 +1,6 @@
// MAX: temp ignore deprecated, can be dealt with in its own PR
#![allow(deprecated)] // silences clippy warning: deprecated associated function `chacha20::cipher::generic_array::GenericArray::<T, N>::from_slice`: please upgrade to generic-array 1.x - TODO
pub mod constants;
pub mod error;
pub mod format;
+4 -4
View File
@@ -84,12 +84,12 @@ pub fn send_message_internal(
message: &str,
// TODO add Option<surb_amount>, if Some(surb_amount) call send_message() instead with specified #, else send_plain_message as this uses the default
) -> Result<(), Error> {
let client = NYM_CLIENT.lock().expect("could not lock NYM_CLIENT");
let mut client = NYM_CLIENT.lock().expect("could not lock NYM_CLIENT");
if client.is_none() {
bail!("Client is not yet initialised");
}
let nym_client = client
.as_ref()
.as_mut()
.ok_or_else(|| anyhow!("could not get client as_ref()"))?;
RUNTIME.block_on(async move {
@@ -102,12 +102,12 @@ pub fn send_message_internal(
// TODO send_raw_message_internal
pub fn reply_internal(recipient: AnonymousSenderTag, message: &str) -> Result<(), Error> {
let client = NYM_CLIENT.lock().expect("could not lock NYM_CLIENT");
let mut client = NYM_CLIENT.lock().expect("could not lock NYM_CLIENT");
if client.is_none() {
bail!("Client is not yet initialised");
}
let nym_client = client
.as_ref()
.as_mut()
.ok_or_else(|| anyhow!("could not get client as_ref()"))?;
RUNTIME.block_on(async move {
@@ -16,7 +16,7 @@ async fn main() {
let our_address = *client.nym_address();
println!("Our client nym address is: {our_address}");
let sender = client.split_sender();
let mut sender = client.split_sender();
// receiving task
let receiving_task_handle = tokio::spawn(async move {
@@ -0,0 +1,72 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_sdk::mixnet;
use nym_sdk::mixnet::MixnetMessageSender;
use nym_sdk::Error;
use tokio::io::AsyncWriteExt;
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_tracing_logger();
let mut client = mixnet::MixnetClient::connect_new().await.unwrap();
let our_address = *client.nym_address();
println!("Our client nym address is: {our_address}");
// Message-based API works before stream mode is activated
println!("\nTesting message-based API (should work)");
client
.send_plain_message(our_address, "hello via message API")
.await
.unwrap();
println!("Message sent successfully via message-based API");
// Now activate stream mode by using AsyncWrite
println!("\nActivating stream mode via AsyncWrite");
// Note: This write will likely fail since we're not sending valid framed data,
// but it will still activate stream mode
let _ = client.write_all(&[0u8; 10]).await;
println!("Stream mode is now active");
// Message-based API should now fail
println!("\nTesting message-based API again (should fail)");
let result = client
.send_plain_message(our_address, "this should fail")
.await;
match result {
Err(Error::StreamModeActive) => {
println!("Got expected error: StreamModeActive");
println!("Message-based API correctly blocked after stream mode activation");
}
Err(e) => {
println!("Got unexpected error: {e:?}");
}
Ok(()) => {
println!("ERROR: send() should have failed but succeeded!");
}
}
// split_sender shares the stream_mode flag
println!("\nTesting split_sender (shares stream_mode)");
let mut sender = client.split_sender();
let result = sender
.send_plain_message(our_address, "this should also fail")
.await;
match result {
Err(Error::StreamModeActive) => {
println!("Got expected error: StreamModeActive on split sender");
println!("Split sender correctly shares stream_mode with parent client");
}
Err(e) => {
println!("Got unexpected error: {e:?}");
}
Ok(()) => {
println!("ERROR: split_sender.send() should have failed but succeeded!");
}
}
client.disconnect().await;
}
+1
View File
@@ -29,6 +29,7 @@ where
St: Storage + Clone,
<St as Storage>::StorageError: Send + Sync + 'static,
{
#[allow(clippy::result_large_err)]
pub(crate) fn new(
network_details: NymNetworkDetails,
mnemonic: String,
+3
View File
@@ -99,6 +99,9 @@ pub enum Error {
#[error("Failed to get shutdown tracker from the task runtime registry: {0}")]
RegistryAccess(#[from] nym_task::RegistryAccessError),
#[error("Cannot use message-based functions after stream mode is activated")]
StreamModeActive,
}
impl Error {
+2
View File
@@ -340,6 +340,7 @@ where
}
/// Construct a [`DisconnectedMixnetClient`] from the setup specified.
#[allow(clippy::result_large_err)]
pub fn build(self) -> Result<DisconnectedMixnetClient<S>> {
let mut client = DisconnectedMixnetClient::new(
self.config,
@@ -445,6 +446,7 @@ where
/// Callers have the option of supplying further parameters to:
/// - store persistent identities at a location on-disk, if desired;
/// - use SOCKS5 mode
#[allow(clippy::result_large_err)]
fn new(
config: Config,
socks5_config: Option<Socks5>,
+345 -5
View File
@@ -2,9 +2,11 @@ use crate::mixnet::client::MixnetClientBuilder;
use crate::mixnet::traits::MixnetMessageSender;
use crate::{Error, Result};
use async_trait::async_trait;
use futures::{ready, Stream, StreamExt};
use bytes::BytesMut;
use futures::{ready, Future, FutureExt, Sink, SinkExt, Stream, StreamExt};
use log::{debug, error};
use nym_client_core::client::base_client::GatewayConnection;
use nym_client_core::client::inbound_messages::{InputMessageCodec, ReconstructedMessageCodec};
use nym_client_core::client::mix_traffic::ClientRequestSender;
use nym_client_core::client::{
base_client::{ClientInput, ClientOutput, ClientState},
@@ -21,9 +23,12 @@ use nym_task::connections::{ConnectionCommandSender, LaneQueueLengths};
use nym_task::ShutdownTracker;
use nym_topology::{NymRouteProvider, NymTopology};
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::task::{Context, Poll};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio::sync::RwLockReadGuard;
use tokio_util::codec::{Encoder, FramedRead};
use tokio_util::sync::CancellationToken;
/// Client connected to the Nym mixnet.
@@ -56,10 +61,47 @@ pub struct MixnetClient {
pub(crate) shutdown_handle: ShutdownTracker,
pub(crate) packet_type: Option<PacketType>,
// internal state used for the `Stream` implementation
/// Internal state used for the `Stream` implementation
_buffered: Vec<ReconstructedMessage>,
pub(crate) forget_me: ForgetMe,
pub(crate) remember_me: RememberMe,
/// Internal state used for AsyncRead
_read: ReadBuffer,
/// Internal state used for AsyncWrite
_write: Option<PendingWrite>,
/// Set to `true` when AsyncRead/AsyncWrite is used to
/// prevent mixing stream- and message-based functions.
stream_mode: Arc<AtomicBool>,
}
#[derive(Debug, Default)]
struct ReadBuffer {
buffer: BytesMut,
}
impl ReadBuffer {
fn clear(&mut self) {
self.buffer.clear();
}
fn pending(&self) -> bool {
!self.buffer.is_empty()
}
}
struct PendingWrite {
future: Pin<
Box<
dyn Future<Output = Result<(), tokio_util::sync::PollSendError<InputMessage>>>
+ Send
+ Sync,
>,
>,
bytes_to_report: usize,
}
impl MixnetClient {
@@ -90,6 +132,9 @@ impl MixnetClient {
_buffered: Vec::new(),
forget_me,
remember_me,
_read: ReadBuffer::default(),
_write: None,
stream_mode: Arc::new(AtomicBool::new(false)),
}
}
@@ -156,6 +201,8 @@ impl MixnetClient {
MixnetClientSender {
client_input: self.client_input.clone(),
packet_type: self.packet_type,
_write: None,
stream_mode: self.stream_mode.clone(),
}
}
@@ -280,18 +327,295 @@ impl MixnetClient {
}
}
}
fn read_buffer_to_slice(
&mut self,
buf: &mut ReadBuf,
cx: &mut Context<'_>,
) -> Poll<tokio::io::Result<()>> {
let available = self._read.buffer.len();
let capacity = buf.capacity();
if available <= capacity {
buf.put_slice(&self._read.buffer);
self._read.clear();
Poll::Ready(Ok(()))
} else {
let chunk = self._read.buffer.split_to(capacity);
buf.put_slice(&chunk);
cx.waker().wake_by_ref();
Poll::Ready(Ok(()))
}
}
}
#[derive(Clone)]
pub struct MixnetClientSender {
client_input: ClientInput,
packet_type: Option<PacketType>,
_write: Option<PendingWrite>,
stream_mode: Arc<AtomicBool>,
}
impl Clone for MixnetClientSender {
fn clone(&self) -> Self {
Self {
client_input: self.client_input.clone(),
packet_type: self.packet_type,
_write: None, // Don't clone pending write state
stream_mode: self.stream_mode.clone(),
}
}
}
impl AsyncRead for MixnetClient {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf,
) -> Poll<tokio::io::Result<()>> {
self.stream_mode.store(true, Ordering::SeqCst);
let mut codec = ReconstructedMessageCodec {};
if self._read.pending() {
return self.read_buffer_to_slice(buf, cx);
}
let msg = match self.as_mut().poll_next(cx) {
Poll::Ready(Some(msg)) => msg,
Poll::Ready(None) => return Poll::Ready(Ok(())),
Poll::Pending => return Poll::Pending,
};
match codec.encode(msg, &mut self._read.buffer) {
Ok(_) => {}
Err(e) => {
error!("failed to encode reconstructed message: {:?}", e);
return Poll::Ready(Err(tokio::io::Error::other(
"failed to encode reconstructed message",
)));
}
};
self.read_buffer_to_slice(buf, cx)
}
}
impl AsyncWrite for MixnetClient {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>> {
self.stream_mode.store(true, Ordering::SeqCst);
// Complete any pending write first
if let Some(pending) = &mut self._write {
match pending.future.as_mut().poll(cx) {
Poll::Ready(Ok(())) => {
let bytes = pending.bytes_to_report;
self._write = None;
return Poll::Ready(Ok(bytes));
}
Poll::Ready(Err(_)) => {
self._write = None;
return Poll::Ready(Err(std::io::Error::other(
"failed to send message to mixnet",
)));
}
Poll::Pending => return Poll::Pending,
}
}
// No pending write, parse new message from buffer
let codec = InputMessageCodec {};
let mut reader = FramedRead::new(buf, codec);
let mut fut = reader.next();
let msg = match fut.poll_unpin(cx) {
Poll::Ready(Some(Ok(msg))) => msg,
Poll::Ready(Some(Err(_))) => {
return Poll::Ready(Err(std::io::Error::other(
"failed to read message from input",
)))
}
Poll::Pending => return Poll::Pending,
Poll::Ready(None) => return Poll::Ready(Ok(0)),
};
let msg_size = msg.serialized_size() as usize;
// Create and store the send future
let mut client_input = self.client_input.clone();
let future = Box::pin(async move { client_input.send(msg).await });
self._write = Some(PendingWrite {
future,
bytes_to_report: msg_size,
});
// Poll new future immediately
self.poll_write(cx, buf)
}
fn poll_flush(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<std::prelude::v1::Result<(), std::io::Error>> {
if self._write.is_some() {
ready!(self.as_mut().poll_write(cx, &[]))?;
}
Poll::Ready(Ok(()))
}
fn poll_shutdown(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<std::prelude::v1::Result<(), std::io::Error>> {
AsyncWrite::poll_flush(self, cx)
}
}
impl Sink<InputMessage> for MixnetClient {
type Error = Error;
fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<()>> {
self.stream_mode.store(true, Ordering::SeqCst);
match self.sender().poll_ready_unpin(cx) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(())),
Poll::Ready(Err(_)) => Poll::Ready(Err(Error::MessageSendingFailure)),
Poll::Pending => Poll::Pending,
}
}
fn start_send(mut self: Pin<&mut Self>, item: InputMessage) -> Result<()> {
self.sender()
.start_send_unpin(item)
.map_err(|_| Error::MessageSendingFailure)
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<()>> {
self.sender()
.poll_flush_unpin(cx)
.map_err(|_| Error::MessageSendingFailure)
}
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<()>> {
self.sender()
.poll_close_unpin(cx)
.map_err(|_| Error::MessageSendingFailure)
}
}
// TODO: there should be a better way of implementing Sink and AsyncWrite over T: MixnetMessageSender
impl AsyncWrite for MixnetClientSender {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>> {
self.stream_mode.store(true, Ordering::SeqCst);
// Complete any pending write first
if let Some(pending) = &mut self._write {
match pending.future.as_mut().poll(cx) {
Poll::Ready(Ok(())) => {
let bytes = pending.bytes_to_report;
self._write = None;
return Poll::Ready(Ok(bytes));
}
Poll::Ready(Err(_)) => {
self._write = None;
return Poll::Ready(Err(std::io::Error::other(
"failed to send message to mixnet",
)));
}
Poll::Pending => return Poll::Pending,
}
}
// No pending write, parse new message from buffer
let codec = InputMessageCodec {};
let mut reader = FramedRead::new(buf, codec);
let mut fut = reader.next();
let msg = match fut.poll_unpin(cx) {
Poll::Ready(Some(Ok(msg))) => msg,
Poll::Ready(Some(Err(_))) => {
return Poll::Ready(Err(std::io::Error::other(
"failed to read message from input",
)))
}
Poll::Pending => return Poll::Pending,
Poll::Ready(None) => return Poll::Ready(Ok(0)),
};
let msg_size = msg.serialized_size() as usize;
// Create and store the send future
let mut client_input = self.client_input.clone();
let future = Box::pin(async move { client_input.send(msg).await });
self._write = Some(PendingWrite {
future,
bytes_to_report: msg_size,
});
// Poll the new future immediately
self.poll_write(cx, buf)
}
fn poll_flush(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<std::prelude::v1::Result<(), std::io::Error>> {
if self._write.is_some() {
ready!(self.as_mut().poll_write(cx, &[]))?;
}
Poll::Ready(Ok(()))
}
fn poll_shutdown(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<std::prelude::v1::Result<(), std::io::Error>> {
AsyncWrite::poll_flush(self, cx)
}
}
impl Sink<InputMessage> for MixnetClientSender {
type Error = Error;
fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<()>> {
match self.sender().poll_ready_unpin(cx) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(())),
Poll::Ready(Err(_)) => Poll::Ready(Err(Error::MessageSendingFailure)),
Poll::Pending => Poll::Pending,
}
}
fn start_send(mut self: Pin<&mut Self>, item: InputMessage) -> Result<()> {
self.sender()
.start_send_unpin(item)
.map_err(|_| Error::MessageSendingFailure)
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<()>> {
self.sender()
.poll_flush_unpin(cx)
.map_err(|_| Error::MessageSendingFailure)
}
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<()>> {
self.sender()
.poll_close_unpin(cx)
.map_err(|_| Error::MessageSendingFailure)
}
}
impl Stream for MixnetClient {
type Item = ReconstructedMessage;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if self.stream_mode.load(Ordering::SeqCst) {
tracing::warn!("Stream::poll_next() called after stream mode activated");
return Poll::Ready(None);
}
if let Some(next) = self._buffered.pop() {
cx.waker().wake_by_ref();
return Poll::Ready(Some(next));
@@ -326,12 +650,20 @@ impl MixnetMessageSender for MixnetClient {
self.packet_type
}
async fn send(&self, message: InputMessage) -> Result<()> {
async fn send(&mut self, message: InputMessage) -> Result<()> {
if self.stream_mode.load(Ordering::SeqCst) {
tracing::warn!("send() called after stream mode activated");
return Err(Error::StreamModeActive);
}
self.client_input
.send(message)
.await
.map_err(|_| Error::MessageSendingFailure)
}
fn sender(&mut self) -> &mut tokio_util::sync::PollSender<InputMessage> {
&mut self.client_input.input_sender
}
}
#[async_trait]
@@ -340,10 +672,18 @@ impl MixnetMessageSender for MixnetClientSender {
self.packet_type
}
async fn send(&self, message: InputMessage) -> Result<()> {
async fn send(&mut self, message: InputMessage) -> Result<()> {
if self.stream_mode.load(Ordering::SeqCst) {
tracing::warn!("send() called after stream mode activated");
return Err(Error::StreamModeActive);
}
self.client_input
.send(message)
.await
.map_err(|_| Error::MessageSendingFailure)
}
fn sender(&mut self) -> &mut tokio_util::sync::PollSender<InputMessage> {
&mut self.client_input.input_sender
}
}
+1
View File
@@ -53,6 +53,7 @@ impl StoragePaths {
///
/// This function will return an error if it is passed a path to an existing file instead of a
/// directory.
#[allow(clippy::result_large_err)]
pub fn new_from_dir<P: AsRef<Path>>(dir: P) -> Result<Self> {
let dir = dir.as_ref();
if dir.is_file() {
+2 -1
View File
@@ -26,6 +26,7 @@ const SINK_BUFFER_SIZE_IN_MESSAGES: usize = 8;
/// Traits that represents the ability to convert bytes into InputMessages that can be sent to the
/// mixnet. This is typically used to set the destination and other sending parameters.
pub trait MixnetMessageSinkTranslator: Unpin {
#[allow(clippy::result_large_err)]
fn to_input_message(&self, bytes: &[u8]) -> Result<InputMessage, Error>;
}
@@ -175,7 +176,7 @@ where
}
fn start_sender_task<Sender>(
mixnet_client_sender: Sender,
mut mixnet_client_sender: Sender,
) -> (mpsc::Sender<InputMessage>, JoinHandle<()>)
where
Sender: MixnetMessageSender + Send + 'static,
+6 -4
View File
@@ -16,9 +16,11 @@ pub trait MixnetMessageSender {
None
}
fn sender(&mut self) -> &mut tokio_util::sync::PollSender<InputMessage>;
/// Sends a [`InputMessage`] to the mixnet. This is the most low-level sending function, for
/// full customization.
async fn send(&self, message: InputMessage) -> Result<()>;
async fn send(&mut self, message: InputMessage) -> Result<()>;
/// Sends data to the supplied Nym address with the default surb behaviour.
///
@@ -35,7 +37,7 @@ pub trait MixnetMessageSender {
/// client.send_plain_message(recipient, "hi").await.unwrap();
/// }
/// ```
async fn send_plain_message<M>(&self, address: Recipient, message: M) -> Result<()>
async fn send_plain_message<M>(&mut self, address: Recipient, message: M) -> Result<()>
where
M: AsRef<[u8]> + Send,
{
@@ -61,7 +63,7 @@ pub trait MixnetMessageSender {
/// }
/// ```
async fn send_message<M>(
&self,
&mut self,
address: Recipient,
message: M,
surbs: IncludedSurbs,
@@ -103,7 +105,7 @@ pub trait MixnetMessageSender {
/// client.send_reply(tag, b"hi").await.unwrap();
/// }
/// ```
async fn send_reply<M>(&self, recipient_tag: AnonymousSenderTag, message: M) -> Result<()>
async fn send_reply<M>(&mut self, recipient_tag: AnonymousSenderTag, message: M) -> Result<()>
where
M: AsRef<[u8]> + Send,
{
+1 -1
View File
@@ -163,7 +163,7 @@
//! let codec = BytesCodec::new();
//! let mut framed_read = FramedRead::new(read, codec);
//! // Much like the tcpstream, split our Nym client into a sender and receiver for concurrent read/write
//! let sender = client.split_sender();
//! let mut sender = client.split_sender();
//! // The server / service provider address our client is sending messages to will remain static
//! let server_addr = server_address;
//! // Store outgoing messages in instance of Dashset abstraction
@@ -161,7 +161,7 @@ impl NymProxyClient {
let codec = BytesCodec::new();
let mut framed_read = FramedRead::new(read, codec);
// Much like the tcpstream, split our Nym client into a sender and receiver for concurrent read/write
let sender = client.split_sender();
let mut sender = client.split_sender();
// The server / service provider address our client is sending messages to will remain static
let server_addr = server_address;
// Store outgoing messages in instance of Dashset abstraction
@@ -217,7 +217,7 @@ impl NymProxyServer {
sender
.write()
.await
.send_reply(surb, bincode::serialize(&reply)?)
.send_reply(surb, &bincode::serialize(&reply)?)
.await?
}
info!(
@@ -219,14 +219,14 @@ impl MixnetMessageSinkTranslator for ToIprDataResponse {
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use bytes::Bytes;
use futures::SinkExt;
use nym_sdk::mixnet::{AnonymousSenderTag, MixnetMessageSender, MixnetMessageSink};
use std::sync::{Arc, Mutex};
use tokio::sync::Notify;
use tokio_util::codec::FramedWrite;
use tokio_util::sync::PollSender;
use super::*;
@@ -264,12 +264,15 @@ mod tests {
#[async_trait]
impl MixnetMessageSender for MockMixnetClientSender {
async fn send(&self, message: InputMessage) -> std::result::Result<(), nym_sdk::Error> {
async fn send(&mut self, message: InputMessage) -> std::result::Result<(), nym_sdk::Error> {
let mut sent_messages = self.sent_messages.lock().unwrap();
sent_messages.push(message);
self.notify.notify_one();
Ok(())
}
fn sender(&mut self) -> &mut PollSender<nym_sdk::mixnet::InputMessage> {
todo!()
}
}
#[tokio::test]
@@ -607,7 +607,7 @@ impl MixnetListener {
// When an incoming mixnet message triggers a response that we send back, such as during
// connect handshake.
async fn handle_response(&self, response: VersionedResponse) -> Result<()> {
async fn handle_response(&mut self, response: VersionedResponse) -> Result<()> {
let send_to = response.reply_to.clone();
let response_bytes = response.try_into_bytes()?;
let input_message =
@@ -622,7 +622,7 @@ impl MixnetListener {
// A single incoming request can trigger multiple responses, such as when data requests contain
// multiple IP packets.
async fn handle_responses(&self, responses: Vec<PacketHandleResult>) {
async fn handle_responses(&mut self, responses: Vec<PacketHandleResult>) {
for response in responses {
match response {
Ok(Some(response)) => {
@@ -38,6 +38,7 @@ tap = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["net", "rt-multi-thread", "macros"] }
tokio-tungstenite = { workspace = true }
tokio-util = { workspace = true }
url = { workspace = true }
time = { workspace = true }
zeroize = { workspace = true }
@@ -7,6 +7,7 @@ use crate::reply::MixnetMessage;
use crate::request_filter::RequestFilter;
use crate::{reply, socks5};
use async_trait::async_trait;
use futures::SinkExt;
use futures::channel::{mpsc, oneshot};
use futures::stream::StreamExt;
use log::{debug, error, warn};
@@ -15,7 +16,7 @@ use nym_client_core::HardcodedTopologyProvider;
use nym_client_core::client::mix_traffic::transceiver::GatewayTransceiver;
use nym_client_core::config::disk_persistence::CommonClientPaths;
use nym_network_defaults::NymNetworkDetails;
use nym_sdk::mixnet::{MixnetMessageSender, TopologyProvider};
use nym_sdk::mixnet::TopologyProvider;
use nym_service_providers_common::ServiceProvider;
use nym_service_providers_common::interface::{
BinaryInformation, ProviderInterfaceVersion, Request, RequestVersion,
@@ -37,6 +38,7 @@ use nym_task::ShutdownTracker;
use nym_task::connections::LaneQueueLengths;
use std::path::Path;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio_util::sync::PollSender;
// Since it's an atomic, it's safe to be kept static and shared across threads
static ACTIVE_PROXIES: AtomicUsize = AtomicUsize::new(0);
@@ -240,6 +242,8 @@ impl NRServiceProviderBuilder {
// going to be used by `mixnet_response_listener`
let (mix_input_sender, mix_input_receiver) = tokio::sync::mpsc::channel::<MixnetMessage>(1);
let mix_input_sender = PollSender::new(mix_input_sender);
// Controller for managing all active connections.
let (mut active_connections_controller, controller_sender) = Controller::new(
mixnet_client.connection_command_sender(),
@@ -337,7 +341,7 @@ impl NRServiceProvider {
/// Listens for any messages from `mix_reader` that should be written back to the mix network
/// via the `websocket_writer`.
async fn mixnet_response_listener(
mixnet_client_sender: nym_sdk::mixnet::MixnetClientSender,
mut mixnet_client_sender: nym_sdk::mixnet::MixnetClientSender,
mut mix_input_reader: MixProxyReader<MixnetMessage>,
packet_type: PacketType,
) {
@@ -346,7 +350,7 @@ impl NRServiceProvider {
socks5_msg = mix_input_reader.recv() => {
if let Some(msg) = socks5_msg {
let response_message = msg.into_input_message(packet_type);
mixnet_client_sender.send(response_message).await.unwrap();
nym_sdk::mixnet::MixnetMessageSender::send(&mut mixnet_client_sender, response_message).await.unwrap();
} else {
log::error!("Exiting: channel closed!");
break;
@@ -364,7 +368,7 @@ impl NRServiceProvider {
return_address: reply::MixnetAddress,
biggest_packet_size: PacketSize,
controller_sender: ControllerSender,
mix_input_sender: MixProxySender<MixnetMessage>,
mut mix_input_sender: MixProxySender<MixnetMessage>,
lane_queue_lengths: LaneQueueLengths,
shutdown: ShutdownTracker,
) {
@@ -457,7 +461,7 @@ impl NRServiceProvider {
.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 mut mix_input_sender_clone = self.mix_input_sender.clone();
let lane_queue_lengths_clone = self.mixnet_client.shared_lane_queue_lengths();
// we're just cloning the underlying pointer, nothing expensive is happening here
+1 -1
View File
@@ -344,7 +344,7 @@ mod tests {
let mut client = MixnetClient::connect_new().await?;
println!("sending client addr {}", client.nym_address());
let sender = client.split_sender();
let mut sender = client.split_sender();
let receiving_task_handle = tokio::spawn(async move {
println!("in handle");
+1
View File
@@ -32,6 +32,7 @@ once_cell = { workspace = true }
thiserror = { workspace = true }
tsify = { workspace = true, features = ["js"] }
web-sys = { workspace = true }
tokio = { workspace = true, default-features = false, features = ["sync"] }
nym-bin-common = { workspace = true }
nym-wasm-client-core = { workspace = true }
+71 -4
View File
@@ -34,7 +34,7 @@ use nym_wasm_utils::error::PromisableResult;
use nym_wasm_utils::{check_promise_result, console_error, console_log};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio_with_wasm::sync::mpsc;
use tokio_with_wasm::sync::{mpsc, RwLock};
use tsify::Tsify;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::future_to_promise;
@@ -54,7 +54,7 @@ pub type ClientRequestSender = mpsc::Sender<ClientRequest>;
#[wasm_bindgen]
pub struct NymClient {
self_address: String,
client_input: Arc<ClientInput>,
client_input: Arc<RwLock<ClientInput>>,
client_state: Arc<ClientState>,
// keep track of the "old" topology for the purposes of node tester
@@ -84,7 +84,10 @@ pub struct NymClientBuilder {
#[wasm_bindgen]
impl NymClientBuilder {
fn new(
/// Create a new NymClientBuilder.
///
/// For transport use (libp2p), pass a dummy handler and use `start_client_for_transport()`.
pub fn new(
config: ClientConfig,
on_message: js_sys::Function,
force_tls: bool,
@@ -253,7 +256,7 @@ impl NymClientBuilder {
Ok(NymClient {
self_address,
client_input: Arc::new(client_input),
client_input: Arc::new(RwLock::new(client_input)),
client_state: Arc::new(started_client.client_state),
_full_topology: None,
_task_manager: started_client.shutdown_handle,
@@ -271,6 +274,62 @@ impl NymClientBuilder {
}
}
// Rust-only methods (not exposed to JavaScript)
impl NymClientBuilder {
/// Start the client for transport use, returning both the client and raw ClientOutput.
///
/// Unlike `start_client`, this doesn't consume the ClientOutput with a JS callback,
/// allowing Rust code (like libp2p transport) to poll for messages directly.
pub async fn start_client_for_transport(
mut self,
) -> Result<(NymClient, ClientOutput), WasmClientError> {
self.config.base.debug.topology.ignore_egress_epoch_role = true;
let client_store = self.initialise_client_storage().await?;
if !self.has_active_gateway(&client_store).await?
|| !self.try_set_preferred_gateway(&client_store).await?
{
add_gateway(
self.preferred_gateway.clone(),
self.latency_based_selection,
self.force_tls,
&self.config.base.client.nym_api_urls,
bin_info!().into(),
self.config.base.debug.topology.minimum_gateway_performance,
self.config.base.debug.topology.ignore_ingress_epoch_role,
&client_store,
)
.await?;
}
let packet_type = self.config.base.debug.traffic.packet_type;
let storage = Self::initialise_storage(&self.config, client_store);
let base_builder =
BaseClientBuilder::<QueryReqwestRpcNyxdClient, _>::new(self.config.base, storage, None);
let mut started_client = base_builder.start_base().await?;
let self_address = started_client.address.to_string();
let client_input = started_client.client_input.register_producer();
let client_output = started_client.client_output.register_consumer();
// Don't start the reconstructed pusher - let the caller handle messages directly
let client = NymClient {
self_address,
client_input: Arc::new(RwLock::new(client_input)),
client_state: Arc::new(started_client.client_state),
_full_topology: None,
_task_manager: started_client.shutdown_handle,
packet_type,
};
Ok((client, client_output))
}
}
#[derive(Tsify, Debug, Clone, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[serde(rename_all = "camelCase")]
@@ -523,3 +582,11 @@ impl NymClient {
self.client_input.send_message(input_msg)
}
}
// Rust-only methods (not exposed to JavaScript)
impl NymClient {
/// Get the client input channel for use by other Rust code (e.g., libp2p transport).
pub fn client_input(&self) -> Arc<RwLock<ClientInput>> {
self.client_input.clone()
}
}
+3
View File
@@ -39,6 +39,9 @@ pub enum WasmClientError {
#[error("the node testing features are currently disabled")]
DisabledTester,
#[error("failed to send message to mixnet: {0}")]
SendFailure(String),
}
// I dislike this so much - there must be a better way.
+5 -1
View File
@@ -1,6 +1,7 @@
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use futures::SinkExt;
use js_sys::Promise;
use nym_wasm_client_core::client::base_client::{ClientInput, ClientState};
use nym_wasm_client_core::client::inbound_messages::InputMessage;
@@ -10,6 +11,7 @@ use nym_wasm_client_core::NymTopology;
use nym_wasm_utils::error::simple_js_error;
use nym_wasm_utils::{check_promise_result, console_log};
use std::sync::Arc;
use tokio_with_wasm::sync::RwLock;
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::future_to_promise;
@@ -48,10 +50,11 @@ pub(crate) trait InputSender {
fn send_messages(&self, messages: Vec<InputMessage>) -> Promise;
}
impl InputSender for Arc<ClientInput> {
impl InputSender for Arc<RwLock<ClientInput>> {
fn send_message(&self, message: InputMessage) -> Promise {
let this = Arc::clone(self);
future_to_promise(async move {
let mut this = this.write().await;
match this.input_sender.send(message).await {
Ok(_) => Ok(JsValue::null()),
Err(_) => Err(simple_js_error(
@@ -64,6 +67,7 @@ impl InputSender for Arc<ClientInput> {
fn send_messages(&self, messages: Vec<InputMessage>) -> Promise {
let this = Arc::clone(self);
future_to_promise(async move {
let mut this = this.write().await;
for message in messages {
if this.input_sender.send(message).await.is_err() {
return Err(simple_js_error(
+2
View File
@@ -14,6 +14,8 @@ mod helpers;
#[cfg(target_arch = "wasm32")]
mod response_pusher;
#[cfg(target_arch = "wasm32")]
pub mod stream;
#[cfg(target_arch = "wasm32")]
use nym_wasm_utils::set_panic_hook;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
+136
View File
@@ -0,0 +1,136 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! Stream wrapper for NymClient.
//!
//! This module provides `NymClientStream`, a Rust-only wrapper around `NymClient`
//! that implements `futures::Stream` for receiving messages. This makes it easy
//! to use the Nym client with async patterns, particularly for libp2p transport
//! integration.
use futures::channel::mpsc::UnboundedReceiver;
use futures::{ready, SinkExt, Stream, StreamExt};
use nym_wasm_client_core::client::base_client::{ClientInput, ClientOutput};
use nym_wasm_client_core::client::inbound_messages::InputMessage;
use nym_wasm_client_core::client::received_buffer::ReceivedBufferMessage;
use nym_wasm_client_core::ReconstructedMessage;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use tokio_with_wasm::sync::RwLock;
use crate::client::NymClient;
use crate::error::WasmClientError;
/// A stream wrapper around `NymClient` that implements `futures::Stream`.
///
/// This type is for Rust-only use (not exposed to JavaScript) and provides
/// a cleaner API for async message handling, particularly useful for
/// libp2p transport integration.
///
/// # Example
/// ```ignore
/// use nym_client_wasm::client::NymClientBuilder;
/// use nym_client_wasm::stream::NymClientStream;
/// use futures::StreamExt;
///
/// let (client, client_output) = builder.start_client_for_transport().await?;
/// let mut stream = NymClientStream::new(client, client_output);
///
/// // Use as a Stream
/// while let Some(msg) = stream.next().await {
/// println!("Received: {:?}", msg);
/// }
/// ```
pub struct NymClientStream {
/// The underlying NymClient (kept alive to prevent shutdown)
#[allow(dead_code)]
client: NymClient,
/// Receiver for reconstructed messages from the mixnet
reconstructed_receiver: UnboundedReceiver<Vec<ReconstructedMessage>>,
/// Buffer for messages received in batches
buffered_messages: Vec<ReconstructedMessage>,
/// Client input for sending messages
client_input: Arc<RwLock<ClientInput>>,
}
impl NymClientStream {
/// Create a new `NymClientStream` from a `NymClient` and `ClientOutput`.
///
/// Use `NymClientBuilder::start_client_for_transport()` to get both components.
pub fn new(client: NymClient, client_output: ClientOutput) -> Self {
// Register to receive reconstructed messages
let (tx, rx) = futures::channel::mpsc::unbounded();
client_output
.received_buffer_request_sender
.unbounded_send(ReceivedBufferMessage::ReceiverAnnounce(tx))
.expect("Failed to register for reconstructed messages");
let client_input = client.client_input();
Self {
client,
reconstructed_receiver: rx,
buffered_messages: Vec::new(),
client_input,
}
}
/// Get our Nym address as a string.
pub fn self_address(&self) -> String {
self.client.self_address()
}
/// Get access to the underlying client input for sending messages directly.
pub fn client_input(&self) -> Arc<RwLock<ClientInput>> {
self.client_input.clone()
}
/// Send an input message to the mixnet.
pub async fn send(&self, message: InputMessage) -> Result<(), WasmClientError> {
let mut input = self.client_input.write().await;
input
.input_sender
.send(message)
.await
.map_err(|e| WasmClientError::SendFailure(e.to_string()))
}
}
impl Stream for NymClientStream {
type Item = ReconstructedMessage;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
// First, return any buffered messages
if let Some(msg) = self.buffered_messages.pop() {
// If there are more buffered, wake immediately
if !self.buffered_messages.is_empty() {
cx.waker().wake_by_ref();
}
return Poll::Ready(Some(msg));
}
// Poll for new batch of messages
match ready!(self.reconstructed_receiver.poll_next_unpin(cx)) {
None => Poll::Ready(None),
Some(mut msgs) => {
if let Some(msg) = msgs.pop() {
// Buffer remaining messages
if !msgs.is_empty() {
self.buffered_messages = msgs;
cx.waker().wake_by_ref();
}
Poll::Ready(Some(msg))
} else {
// Empty batch, poll again
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
}
}
+3
View File
@@ -0,0 +1,3 @@
[env]
# Increase wasm-bindgen-test timeout for headless browser tests (default is 20 but we need far longer)
WASM_BINDGEN_TEST_TIMEOUT = "300"
+62
View File
@@ -0,0 +1,62 @@
[package]
name = "nym-libp2p-wasm"
version = "0.1.0"
edition = "2021"
description = "libp2p transport over Nym mixnet for WASM/browser environments"
rust-version = "1.85"
publish = false
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
# Async
futures = { workspace = true }
# libp2p - minimal WASM-compatible features
libp2p = { version = "0.54.1", default-features = false, features = [
"wasm-bindgen",
"ping",
"identify",
] }
libp2p-identity = { version = "0.2.10", features = ["ed25519"] }
libp2p-ping = "0.45.0"
libp2p-identify = "0.45.0"
libp2p-swarm = { version = "0.45.1", features = ["wasm-bindgen"] }
# Nym dependencies
nym-client-wasm = { path = "../client", default-features = false }
nym-wasm-client-core = { path = "../../common/wasm/client-core" }
nym-wasm-utils = { path = "../../common/wasm/utils" }
nym-sphinx-addressing = { path = "../../common/nymsphinx/addressing" }
nym-sphinx-anonymous-replies = { path = "../../common/nymsphinx/anonymous-replies" }
# WASM essentials
wasm-bindgen = { workspace = true }
wasm-bindgen-futures = { workspace = true }
js-sys = { workspace = true }
gloo-timers = { workspace = true, features = ["futures"] }
getrandom = { workspace = true, features = ["js"] }
# Tokio for WASM (provides sync primitives)
tokio_with_wasm = { workspace = true, features = ["full"] }
# Utilities
parking_lot = { workspace = true }
send_wrapper = { version = "0.6", features = ["futures"] }
thiserror = { workspace = true }
log = { workspace = true }
hex = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde-wasm-bindgen = { workspace = true }
[dev-dependencies]
wasm-bindgen-test = { workspace = true }
web-sys = { workspace = true, features = ["console"] }
[features]
default = ["console_error_panic_hook"]
console_error_panic_hook = ["nym-wasm-client-core/console_error_panic_hook"]
[package.metadata.wasm-pack.profile.release]
wasm-opt = false
+45
View File
@@ -0,0 +1,45 @@
# nym-libp2p-wasm
This crate provides a libp2p `Transport` implementation that uses the Nym mixnet for p2p communication in browser environments.
This **implements Nym Client -> Nym Client communication** - using the Mixnet as a 'proxy' (allowing for client-only modifications, and e.g. using existing `/tcp/` multiaddrs for bootnodes and addressing) is not yet available.
## Features
- libp2p `Transport` trait implementation
- Stream multiplexing via `StreamMuxer`
- Message ordering over the unordered mixnet
- Anonymous replies using SURBs
- Browser compatible
## Building
```sh
# Build with wasm-pack (for browser):
cd wasm/libp2p-nym
wasm-pack build --target web
```
## Testing
```sh
# Browser tests (headless)
wasm-pack test --headless --firefox
wasm-pack test --headless --chrome
# If you want to see the logs in a browser window run one of the above commands without the --headless flag and open the window console
```
## Modules
| Module | Description |
|--------|-------------|
| `lib.rs` | Crate root, re-exports public API and JS bindings |
| `client.rs` | Client initialization, connects to Mixnet |
| `transport.rs` | libp2p `Transport` trait implementation |
| `connection.rs` | libp2p `StreamMuxer` implementation |
| `substream.rs` | `AsyncRead`/`AsyncWrite` implementation |
| `mixnet.rs` | Bridges Nym client to async channels |
| `queue.rs` | Message ordering (mixnet doesn't guarantee order) |
| `message.rs` | Wire protocol for connections/substreams |
| `error.rs` | Error types |
+104
View File
@@ -0,0 +1,104 @@
// Copyright 2024 Nym Technologies SA
// SPDX-License-Identifier: Apache-2.0
//! Client initialization for the libp2p transport.
//!
//! This module provides a simple way to create a Nym client configured
//! for use with the libp2p transport.
use nym_client_wasm::client::NymClientBuilder;
use nym_client_wasm::config::{ClientConfig, ClientConfigOpts};
use nym_client_wasm::stream::NymClientStream;
use nym_sphinx_addressing::clients::Recipient;
use nym_wasm_utils::console_log;
use std::str::FromStr;
use crate::error::Error;
// Type alias to help with inference
type WasmClientError = nym_client_wasm::error::WasmClientError;
/// Result of creating a transport client.
pub struct TransportClient {
/// Our Nym address
pub self_address: Recipient,
/// The client stream for sending/receiving messages
pub stream: NymClientStream,
}
/// Options for creating a transport client.
#[derive(Default)]
pub struct TransportClientOpts {
/// Optional Nym API URL override
pub nym_api_url: Option<String>,
/// Force TLS connections to gateways (required for browser environments)
pub force_tls: bool,
/// Client ID for storage namespace (avoids conflicts with other clients)
pub client_id: Option<String>,
}
/// Create a Nym client configured for libp2p transport use.
///
/// This connects to the Nym network and returns a `NymClientStream`
/// that implements `Stream` for receiving messages.
///
/// # Example
/// ```ignore
/// use nym_libp2p_wasm::{create_transport_client_async, TransportClientOpts, NymTransport};
/// use libp2p_identity::Keypair;
/// use futures::StreamExt;
///
/// // Create the transport client (connects to Nym network)
/// let opts = TransportClientOpts { force_tls: true, ..Default::default() };
/// let result = create_transport_client_async(opts).await?;
///
/// // Use the stream directly
/// while let Some(msg) = result.stream.next().await {
/// println!("Received: {:?}", msg);
/// }
/// ```
pub async fn create_transport_client_async(
opts: TransportClientOpts,
) -> Result<TransportClient, Error> {
let client_id = opts
.client_id
.unwrap_or_else(|| "libp2p-transport".to_string());
console_log!(
"Creating transport client (id={}, force_tls={})...",
client_id,
opts.force_tls
);
// Create config with client ID for storage namespace isolation
let config_opts = ClientConfigOpts {
id: Some(client_id),
nym_api: opts.nym_api_url,
nyxd: None,
debug: None,
};
let config: ClientConfig = ClientConfig::new(config_opts)
.map_err(|e: WasmClientError| Error::ClientCreationFailed(e.to_string()))?;
// Create builder with a dummy handler (we won't use JS callbacks)
let dummy_handler = js_sys::Function::new_no_args("");
let builder = NymClientBuilder::new(config, dummy_handler, opts.force_tls, None, None);
// Start client for transport use - returns (NymClient, ClientOutput)
let (client, client_output) = builder
.start_client_for_transport()
.await
.map_err(|e: WasmClientError| Error::ClientCreationFailed(e.to_string()))?;
let address_str = client.self_address();
let self_address = Recipient::from_str(&address_str).map_err(Error::InvalidRecipientBytes)?;
console_log!("Transport client ready at: {}", address_str);
// Wrap in NymClientStream for easy async usage
let stream = NymClientStream::new(client, client_output);
Ok(TransportClient {
self_address,
stream,
})
}
+385
View File
@@ -0,0 +1,385 @@
// Copyright 2024 Nym Technologies SA
// SPDX-License-Identifier: Apache-2.0
//! Connection (StreamMuxer) implementation for multiplexing substreams over Nym.
use futures::channel::{
mpsc::{unbounded, UnboundedReceiver, UnboundedSender},
oneshot,
};
use futures::StreamExt;
use libp2p::core::{muxing::StreamMuxerEvent, PeerId, StreamMuxer};
use log::debug;
use nym_sphinx_addressing::clients::Recipient;
use nym_sphinx_anonymous_replies::requests::AnonymousSenderTag;
use nym_wasm_utils::console_log;
use std::{
collections::{HashMap, HashSet},
pin::Pin,
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
task::{Context, Poll, Waker},
};
use super::error::Error;
use super::message::{
ConnectionId, Message, OutboundMessage, SubstreamId, SubstreamMessage, SubstreamMessageType,
TransportMessage,
};
use super::substream::Substream;
/// Connection represents the result of a connection setup process.
/// It implements `StreamMuxer` and thus has stream multiplexing built in.
#[derive(Debug)]
pub struct Connection {
pub(crate) peer_id: PeerId,
/// This will be Some(Recipient) for dialing connections since the outbound conn knows the nym/ multiaddr of the recipient, whereas receivers of connection requests will reply with SURBs
pub(crate) remote_recipient: Option<Recipient>,
pub(crate) id: ConnectionId,
/// receive inbound messages from the `InnerConnection`
pub(crate) inbound_rx: UnboundedReceiver<SubstreamMessage>,
/// substream ID -> outbound pending substream exists
/// the key is deleted when the response is received, or the request times out
pending_substreams: HashSet<SubstreamId>,
/// substream ID -> substream's inbound_tx channel
substream_inbound_txs: HashMap<SubstreamId, UnboundedSender<Vec<u8>>>,
/// substream ID -> substream's close_tx channel
substream_close_txs: HashMap<SubstreamId, oneshot::Sender<()>>,
/// send messages to the mixnet
/// used for sending `SubstreamMessageType::OpenRequest` messages
/// also passed to each substream so they can write to the mixnet
pub(crate) mixnet_outbound_tx: UnboundedSender<OutboundMessage>,
/// sender_tag for SURB replies to incoming messages
pub(crate) sender_tag: Option<AnonymousSenderTag>,
/// inbound substream open requests; used in poll_inbound
inbound_open_tx: UnboundedSender<Substream>,
inbound_open_rx: UnboundedReceiver<Substream>,
/// closed substream IDs; used in poll_close
close_tx: UnboundedSender<SubstreamId>,
close_rx: UnboundedReceiver<SubstreamId>,
/// message nonce contains the next nonce that should be used when
/// sending a message over the connection
pub(crate) message_nonce: Arc<AtomicU64>,
waker: Option<Waker>,
}
impl Connection {
pub(crate) fn new_with_sender_tag(
peer_id: PeerId,
remote_recipient: Option<Recipient>,
id: ConnectionId,
inbound_rx: UnboundedReceiver<SubstreamMessage>,
mixnet_outbound_tx: UnboundedSender<OutboundMessage>,
sender_tag: Option<AnonymousSenderTag>,
) -> Self {
let (inbound_open_tx, inbound_open_rx) = unbounded();
let (close_tx, close_rx) = unbounded();
Connection {
peer_id,
remote_recipient,
id,
inbound_rx,
pending_substreams: HashSet::new(),
substream_inbound_txs: HashMap::new(),
substream_close_txs: HashMap::new(),
mixnet_outbound_tx,
sender_tag,
inbound_open_tx,
inbound_open_rx,
close_tx,
close_rx,
message_nonce: Arc::new(AtomicU64::new(1)),
waker: None,
}
}
/// Returns the remote peer's libp2p PeerId.
pub fn peer_id(&self) -> PeerId {
self.peer_id
}
/// Returns the remote peer's Nym address, if known.
///
/// This is `Some(Recipient)` for connections we initiated (we know who we dialed),
/// and `None` for incoming connections (we use SURBs to reply, never learning their address).
///
/// **Privacy property**: For incoming connections, this is always `None` - the listener
/// never learns the dialer's Nym address.
pub fn remote_nym_address(&self) -> Option<Recipient> {
self.remote_recipient
}
/// Returns true if this connection uses anonymous replies (SURBs).
///
/// This is true for incoming connections where we don't know the remote's address.
pub fn uses_anonymous_replies(&self) -> bool {
self.sender_tag.is_some()
}
fn new_outbound_substream(&mut self) -> Result<Substream, Error> {
debug!("new_outbound_substream called");
let substream_id = SubstreamId::generate();
debug!("Generated substream_id: {:?}", substream_id);
let nonce = self.message_nonce.fetch_add(1, Ordering::SeqCst);
debug!("Using nonce {}", nonce);
debug!("Connection sender_tag: {:?}", self.sender_tag);
debug!(
"About to send with sender_tag: {:?}",
self.sender_tag.is_some()
);
let outbound_msg = OutboundMessage {
recipient: self.remote_recipient, // Some(Recipient) for dialer, None for receiver
message: Message::TransportMessage(TransportMessage {
nonce,
id: self.id.clone(),
message: SubstreamMessage {
substream_id: substream_id.clone(),
message_type: SubstreamMessageType::OpenRequest,
},
}),
sender_tag: self.sender_tag.clone(), // None for dialer, Some(sender_tag) for receiver
};
debug!("Sending OpenRequest for substream: {:?}", substream_id);
// Send the outbound message
self.mixnet_outbound_tx
.unbounded_send(outbound_msg)
.map_err(|e| {
debug!("Failed to send outbound message: {}", e);
Error::OutboundSendFailure(e.to_string())
})?;
debug!("Creating substream");
// track pending outbound substreams
let res = self.new_substream(substream_id.clone());
if res.is_ok() {
debug!("Adding to pending_substreams");
self.pending_substreams.insert(substream_id);
} else {
debug!("Failed to create substream: {:?}", res);
}
res
}
// creates a new substream instance with the given ID.
fn new_substream(&mut self, id: SubstreamId) -> Result<Substream, Error> {
// check we don't already have a substream with this ID
if self.substream_inbound_txs.contains_key(&id) {
return Err(Error::SubstreamIdExists(id));
}
let (inbound_tx, inbound_rx) = unbounded::<Vec<u8>>();
let (close_tx, close_rx) = oneshot::channel::<()>();
self.substream_inbound_txs.insert(id.clone(), inbound_tx);
self.substream_close_txs.insert(id.clone(), close_tx);
if let Some(waker) = self.waker.take() {
waker.wake();
}
Ok(Substream::new_with_sender_tag(
self.remote_recipient,
self.id.clone(),
id,
inbound_rx,
self.mixnet_outbound_tx.clone(),
close_rx,
self.message_nonce.clone(),
self.sender_tag.clone(), // Pass the connection's SURB directly
))
}
fn handle_close(&mut self, substream_id: SubstreamId) -> Result<(), Error> {
if self.substream_inbound_txs.remove(&substream_id).is_none() {
return Err(Error::SubstreamIdDoesNotExist(substream_id));
}
// notify substream that it's closed
let close_tx = self.substream_close_txs.remove(&substream_id);
if let Some(tx) = close_tx {
let _ = tx.send(());
}
// notify poll_close that the substream is closed
self.close_tx
.unbounded_send(substream_id)
.map_err(|e| Error::InboundSendFailure(e.to_string()))
}
}
impl StreamMuxer for Connection {
type Substream = Substream;
type Error = Error;
fn poll_inbound(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<Self::Substream, Self::Error>> {
console_log!("[Connection::poll_inbound] checking for inbound substreams");
if let Poll::Ready(Some(substream)) = self.inbound_open_rx.poll_next_unpin(cx) {
console_log!(
"[Connection::poll_inbound] got inbound substream: {:?}",
substream.substream_id
);
return Poll::Ready(Ok(substream));
}
Poll::Pending
}
fn poll_outbound(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Result<Self::Substream, Self::Error>> {
console_log!("[Connection::poll_outbound] called");
debug!("poll_outbound called");
let result = self.new_outbound_substream();
console_log!("[Connection::poll_outbound] result: {:?}", result.is_ok());
debug!("poll_outbound result: {:?}", result.is_ok());
Poll::Ready(result)
}
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
if let Poll::Ready(Some(_)) = self.close_rx.poll_next_unpin(cx) {
return Poll::Ready(Ok(()));
}
Poll::Pending
}
fn poll(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<StreamMuxerEvent, Self::Error>> {
while let Poll::Ready(Some(msg)) = self.inbound_rx.poll_next_unpin(cx) {
debug!(
"Connection poll received message type: {:?} for substream: {:?}",
msg.message_type, msg.substream_id
);
match msg.message_type {
SubstreamMessageType::OpenRequest => {
debug!(
"Processing OpenRequest for substream: {:?}",
msg.substream_id
);
if self.remote_recipient.is_none() {
debug!("Listener received OpenRequest - correct");
} else {
debug!("Dialer received OpenRequest - something is not right here");
}
// create a new substream with the given ID
let substream = self.new_substream(msg.substream_id.clone())?;
let nonce = self.message_nonce.fetch_add(1, Ordering::SeqCst);
debug!("About to send OpenResponse with nonce: {}", nonce);
debug!("Using sender_tag: {:?}", self.sender_tag);
// send the response to the remote peer
let response_msg = OutboundMessage {
recipient: self.remote_recipient,
message: Message::TransportMessage(TransportMessage {
nonce,
id: self.id.clone(),
message: SubstreamMessage {
substream_id: msg.substream_id.clone(),
message_type: SubstreamMessageType::OpenResponse,
},
}),
sender_tag: self.sender_tag.clone(),
};
debug!("Created OutboundMessage: {:?}", response_msg);
self.mixnet_outbound_tx
.unbounded_send(response_msg)
.map_err(|e| {
debug!("FAILED to send OpenResponse: {}", e);
Error::OutboundSendFailure(e.to_string())
})?;
debug!("Queued OpenResponse for mixnet");
// send the substream to our own channel to be returned in poll_inbound
self.inbound_open_tx
.unbounded_send(substream)
.map_err(|e| Error::InboundSendFailure(e.to_string()))?;
debug!("new inbound substream: {:?}", &msg.substream_id);
}
SubstreamMessageType::OpenResponse => {
debug!(
"Processing OpenResponse for substream: {:?}",
msg.substream_id
);
if !self.pending_substreams.remove(&msg.substream_id) {
debug!(
"SubstreamMessageType::OpenResponse no substream pending for ID: {:?}",
&msg.substream_id
);
}
}
SubstreamMessageType::Close => {
debug!("Processing Close for substream: {:?}", msg.substream_id);
self.handle_close(msg.substream_id)?;
}
SubstreamMessageType::Data(data) => {
console_log!(
"[Connection::poll] Data received: {} bytes for substream {:?}",
data.len(),
msg.substream_id
);
debug!("Processing Data: {:?}", &data);
let inbound_tx = self.substream_inbound_txs.get_mut(&msg.substream_id);
match inbound_tx {
Some(tx) => {
console_log!("[Connection::poll] Forwarding data to substream channel");
if let Err(e) = tx.unbounded_send(data) {
console_log!("[Connection::poll] ERROR: Channel closed for substream {:?}: {}", msg.substream_id, e);
}
}
None => {
console_log!("[Connection::poll] WARNING: No channel for substream {:?}, dropping data", msg.substream_id);
}
}
}
}
}
self.waker = Some(cx.waker().clone());
Poll::Pending
}
}
/// PendingConnection represents a connection that's been initiated, but not completed.
pub(crate) struct PendingConnection {
pub(crate) remote_recipient: Recipient,
pub(crate) connection_tx: oneshot::Sender<Connection>,
}
impl PendingConnection {
pub(crate) fn new(
remote_recipient: Recipient,
connection_tx: oneshot::Sender<Connection>,
) -> Self {
PendingConnection {
remote_recipient,
connection_tx,
}
}
}
+67
View File
@@ -0,0 +1,67 @@
// Copyright 2024 Nym Technologies SA
// SPDX-License-Identifier: Apache-2.0
//! Error types for the Nym libp2p WASM transport.
use libp2p::core::multiaddr;
use nym_sphinx_addressing::clients::RecipientFormattingError;
use super::message::SubstreamId;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("unimplemented")]
Unimplemented,
#[error("failed to format multiaddress from nym address")]
FailedToFormatMultiaddr(#[from] multiaddr::Error),
#[error("unexpected protocol in multiaddress")]
InvalidProtocolForMultiaddr,
#[error("failed to decode message")]
InvalidMessageBytes,
#[error("no connection found for ConnectionResponse")]
NoConnectionForResponse,
#[error("received ConnectionResponse but connection was already established")]
ConnectionAlreadyEstablished,
#[error("cannot handle connection request; already have connection with given ID")]
ConnectionIDExists,
#[error("no connection found for TransportMessage")]
NoConnectionForTransportMessage,
#[error("failed to decode ConnectionMessage; too short")]
ConnectionMessageBytesTooShort,
#[error("failed to decode ConnectionMessage; no peer ID")]
ConnectionMessageBytesNoPeerId,
#[error("invalid peer ID bytes")]
InvalidPeerIdBytes,
#[error("invalid recipient bytes")]
InvalidRecipientBytes(#[from] RecipientFormattingError),
#[error("failed to decode TransportMessage; too short")]
TransportMessageBytesTooShort,
#[error("failed to decode TransportMessage; invalid nonce")]
InvalidNonce,
#[error("invalid substream ID")]
InvalidSubstreamMessageBytes,
#[error("invalid substream message type byte")]
InvalidSubstreamMessageType,
#[error("substream with given ID already exists")]
SubstreamIdExists(SubstreamId),
#[error("no substream found for given ID")]
SubstreamIdDoesNotExist(SubstreamId),
#[error("recv error: channel closed")]
OneshotRecvFailure,
#[error("recv error: channel closed")]
RecvFailure,
#[error("outbound send error")]
OutboundSendFailure(String),
#[error("inbound send error")]
InboundSendFailure(String),
#[error("failed to send new connection; receiver dropped")]
ConnectionSendFailure,
#[error("failed to send initial TransportEvent::NewAddress")]
SendErrorTransportEvent,
#[error("dial timed out")]
DialTimeout,
#[error("mixnet client error: {0}")]
MixnetClientError(String),
#[error("failed to create client: {0}")]
ClientCreationFailed(String),
}
+69
View File
@@ -0,0 +1,69 @@
// Copyright 2024 Nym Technologies SA
// SPDX-License-Identifier: Apache-2.0
//! libp2p transport over Nym mixnet for WASM/browser environments.
//!
//! This crate provides a libp2p Transport implementation that uses the Nym mixnet
//! for privacy-preserving peer-to-peer communication in browser environments.
//!
//! # Features
//!
//! - Full libp2p Transport trait implementation
//! - Stream multiplexing via StreamMuxer
//! - Message ordering over the unordered mixnet
//! - WASM/browser compatible (uses futures channels, gloo_timers)
//!
//! # Example
//!
//! ```ignore
//! use nym_libp2p_wasm::{create_transport_client_async, NymTransport};
//! use libp2p_identity::Keypair;
//!
//! async fn example() {
//! // Create the transport client (connects to Nym network)
//! let result = create_transport_client_async(None).await.unwrap();
//!
//! // Create the transport
//! let keypair = Keypair::generate_ed25519();
//! let transport = NymTransport::new(
//! result.self_address,
//! result.stream,
//! keypair,
//! ).await.unwrap();
//! // Use transport with libp2p Swarm...
//! }
//! ```
#[cfg(target_arch = "wasm32")]
pub mod client;
#[cfg(target_arch = "wasm32")]
pub(crate) mod connection;
#[cfg(target_arch = "wasm32")]
pub mod error;
#[cfg(target_arch = "wasm32")]
pub(crate) mod message;
#[cfg(target_arch = "wasm32")]
pub(crate) mod mixnet;
#[cfg(target_arch = "wasm32")]
pub(crate) mod queue;
#[cfg(target_arch = "wasm32")]
pub mod substream;
#[cfg(target_arch = "wasm32")]
pub mod transport;
// Re-exports for convenience
#[cfg(target_arch = "wasm32")]
pub use client::{create_transport_client_async, TransportClient, TransportClientOpts};
#[cfg(target_arch = "wasm32")]
pub use connection::Connection;
#[cfg(target_arch = "wasm32")]
pub use error::Error;
#[cfg(target_arch = "wasm32")]
pub use nym_sphinx_addressing::clients::Recipient;
#[cfg(target_arch = "wasm32")]
pub use substream::Substream;
pub use transport::{nym_address_to_multiaddress, NymTransport, Upgrade};
/// The default timeout in seconds for the transport upgrade/handshake.
#[cfg(target_arch = "wasm32")]
const DEFAULT_HANDSHAKE_TIMEOUT_SECS: u64 = 30;
+298
View File
@@ -0,0 +1,298 @@
// Copyright 2024 Nym Technologies SA
// SPDX-License-Identifier: Apache-2.0
//! Wire protocol message types for the Nym libp2p transport.
use libp2p::core::PeerId;
use nym_sphinx_addressing::clients::Recipient;
use nym_sphinx_anonymous_replies::requests::AnonymousSenderTag;
use std::fmt::{Debug, Formatter};
use super::error::Error;
const CONNECTION_ID_LENGTH: usize = 32;
const SUBSTREAM_ID_LENGTH: usize = 32;
const NONCE_BYTES_LEN: usize = 8; // length of u64
const MIN_CONNECTION_MESSAGE_LEN: usize = CONNECTION_ID_LENGTH + NONCE_BYTES_LEN;
/// Generate random bytes using getrandom (works in WASM with js feature)
fn generate_random_bytes<const N: usize>() -> [u8; N] {
let mut bytes = [0u8; N];
getrandom::getrandom(&mut bytes).expect("getrandom failed");
bytes
}
/// ConnectionId is a unique, randomly-generated per-connection ID that's used to
/// identify which connection a message belongs to.
#[derive(Clone, Default, Eq, Hash, PartialEq)]
pub(crate) struct ConnectionId([u8; 32]);
impl ConnectionId {
pub(crate) fn generate() -> Self {
ConnectionId(generate_random_bytes())
}
fn from_bytes(bytes: &[u8]) -> Self {
let mut id = [0u8; 32];
id[..].copy_from_slice(&bytes[0..CONNECTION_ID_LENGTH]);
ConnectionId(id)
}
}
impl Debug for ConnectionId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", hex::encode(self.0))
}
}
/// SubstreamId is a unique, randomly-generated per-substream ID that's used to
/// identify which substream a message belongs to.
#[derive(Clone, Default, Eq, Hash, PartialEq)]
pub struct SubstreamId(pub(crate) [u8; 32]);
impl SubstreamId {
pub(crate) fn generate() -> Self {
SubstreamId(generate_random_bytes())
}
fn from_bytes(bytes: &[u8]) -> Self {
let mut id = [0u8; 32];
id[..].copy_from_slice(&bytes[0..SUBSTREAM_ID_LENGTH]);
SubstreamId(id)
}
}
impl Debug for SubstreamId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", hex::encode(self.0))
}
}
#[derive(Debug)]
#[allow(clippy::enum_variant_names)]
pub(crate) enum Message {
ConnectionRequest(ConnectionMessage),
ConnectionResponse(ConnectionMessage),
TransportMessage(TransportMessage),
}
/// ConnectionMessage is exchanged to open a new connection.
#[derive(Debug)]
pub(crate) struct ConnectionMessage {
pub(crate) peer_id: PeerId,
pub(crate) id: ConnectionId,
}
/// TransportMessage is sent over a connection after establishment.
#[derive(Debug, Clone)]
pub(crate) struct TransportMessage {
/// increments by 1 for every TransportMessage sent over a connection.
/// required for ordering, since Nym does not guarantee ordering.
/// ConnectionMessages do not need nonces, as we know that they will
/// be the first messages sent over a connection.
/// the first TransportMessage sent over a connection will have nonce 1.
pub(crate) nonce: u64,
pub(crate) message: SubstreamMessage,
pub(crate) id: ConnectionId,
}
impl Message {
fn try_from_bytes(bytes: Vec<u8>) -> Result<Self, Error> {
if bytes.len() < 2 {
return Err(Error::InvalidMessageBytes);
}
Ok(match bytes[0] {
0 => Message::ConnectionRequest(ConnectionMessage::try_from_bytes(&bytes[1..])?),
1 => Message::ConnectionResponse(ConnectionMessage::try_from_bytes(&bytes[1..])?),
2 => Message::TransportMessage(TransportMessage::try_from_bytes(&bytes[1..])?),
_ => return Err(Error::InvalidMessageBytes),
})
}
}
impl ConnectionMessage {
fn to_bytes(&self) -> Vec<u8> {
let mut bytes = self.id.0.to_vec();
bytes.append(&mut self.peer_id.to_bytes());
bytes
}
fn try_from_bytes(bytes: &[u8]) -> Result<Self, Error> {
if bytes.len() < CONNECTION_ID_LENGTH + 1 {
return Err(Error::ConnectionMessageBytesTooShort);
}
let id = ConnectionId::from_bytes(&bytes[0..CONNECTION_ID_LENGTH]);
let peer_id = PeerId::from_bytes(&bytes[CONNECTION_ID_LENGTH..])
.map_err(|_| Error::InvalidPeerIdBytes)?;
Ok(ConnectionMessage { peer_id, id })
}
}
impl TransportMessage {
fn to_bytes(&self) -> Vec<u8> {
let mut bytes = self.nonce.to_be_bytes().to_vec();
bytes.extend_from_slice(self.id.0.as_ref());
bytes.extend_from_slice(&self.message.to_bytes());
bytes
}
fn try_from_bytes(bytes: &[u8]) -> Result<Self, Error> {
if bytes.len() < MIN_CONNECTION_MESSAGE_LEN + 1 {
return Err(Error::TransportMessageBytesTooShort);
}
let nonce = u64::from_be_bytes(
bytes[0..NONCE_BYTES_LEN]
.to_vec()
.try_into()
.map_err(|_| Error::InvalidNonce)?,
);
let id = ConnectionId::from_bytes(&bytes[NONCE_BYTES_LEN..MIN_CONNECTION_MESSAGE_LEN]);
let message = SubstreamMessage::try_from_bytes(&bytes[MIN_CONNECTION_MESSAGE_LEN..])?;
Ok(TransportMessage { nonce, message, id })
}
}
impl Ord for TransportMessage {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.nonce.cmp(&other.nonce)
}
}
impl std::cmp::PartialOrd for TransportMessage {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl std::cmp::Eq for TransportMessage {}
impl std::cmp::PartialEq for TransportMessage {
fn eq(&self, other: &Self) -> bool {
self.nonce == other.nonce
}
}
#[derive(Debug, Clone)]
pub(crate) enum SubstreamMessageType {
OpenRequest,
OpenResponse,
Close,
Data(Vec<u8>),
}
impl SubstreamMessageType {
fn to_u8(&self) -> u8 {
match self {
SubstreamMessageType::OpenRequest => 0,
SubstreamMessageType::OpenResponse => 1,
SubstreamMessageType::Close => 2,
SubstreamMessageType::Data(_) => 3,
}
}
}
/// SubstreamMessage is a message sent over a substream.
#[derive(Debug, Clone)]
pub(crate) struct SubstreamMessage {
pub(crate) substream_id: SubstreamId,
pub(crate) message_type: SubstreamMessageType,
}
impl SubstreamMessage {
pub(crate) fn new_with_data(substream_id: SubstreamId, message: Vec<u8>) -> Self {
SubstreamMessage {
substream_id,
message_type: SubstreamMessageType::Data(message),
}
}
pub(crate) fn new_close(substream_id: SubstreamId) -> Self {
SubstreamMessage {
substream_id,
message_type: SubstreamMessageType::Close,
}
}
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let mut bytes = self.substream_id.0.clone().to_vec();
bytes.push(self.message_type.to_u8());
if let SubstreamMessageType::Data(message) = &self.message_type {
bytes.extend_from_slice(message);
}
bytes
}
pub(crate) fn try_from_bytes(bytes: &[u8]) -> Result<Self, Error> {
if bytes.len() < SUBSTREAM_ID_LENGTH + 1 {
return Err(Error::InvalidSubstreamMessageBytes);
}
let substream_id = SubstreamId::from_bytes(&bytes[0..SUBSTREAM_ID_LENGTH]);
let message_type = match bytes[SUBSTREAM_ID_LENGTH] {
0 => SubstreamMessageType::OpenRequest,
1 => SubstreamMessageType::OpenResponse,
2 => SubstreamMessageType::Close,
3 => {
if bytes.len() < SUBSTREAM_ID_LENGTH + 2 {
return Err(Error::InvalidSubstreamMessageBytes);
}
SubstreamMessageType::Data(bytes[SUBSTREAM_ID_LENGTH + 1..].to_vec())
}
_ => return Err(Error::InvalidSubstreamMessageType),
};
Ok(SubstreamMessage {
substream_id,
message_type,
})
}
}
impl Message {
pub(crate) fn to_bytes(&self) -> Vec<u8> {
match self {
Message::ConnectionRequest(msg) => {
let mut bytes = 0_u8.to_be_bytes().to_vec();
bytes.append(&mut msg.to_bytes());
bytes
}
Message::ConnectionResponse(msg) => {
let mut bytes = 1_u8.to_be_bytes().to_vec();
bytes.append(&mut msg.to_bytes());
bytes
}
Message::TransportMessage(msg) => {
let mut bytes = 2_u8.to_be_bytes().to_vec();
bytes.append(&mut msg.to_bytes());
bytes
}
}
}
}
/// InboundMessage represents an inbound mixnet message.
pub(crate) struct InboundMessage(pub(crate) Message, pub(crate) Option<AnonymousSenderTag>);
/// OutboundMessage represents an outbound mixnet message.
#[derive(Debug)]
pub(crate) struct OutboundMessage {
pub(crate) message: Message,
pub(crate) recipient: Option<Recipient>,
pub(crate) sender_tag: Option<AnonymousSenderTag>,
}
pub(crate) fn parse_message_data(
data: &[u8],
sender_tag: Option<AnonymousSenderTag>,
) -> Result<InboundMessage, Error> {
if data.len() < 2 {
return Err(Error::InvalidMessageBytes);
}
let msg = Message::try_from_bytes(data.to_vec())?;
Ok(InboundMessage(msg, sender_tag))
}
+200
View File
@@ -0,0 +1,200 @@
// Copyright 2024 Nym Technologies SA
// SPDX-License-Identifier: Apache-2.0
//! Mixnet bridge for WASM - adapts the Nym client stream to channel-based async.
//!
//! This module bridges the NymClientStream to the channel-based architecture
//! used by the libp2p transport.
use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
use futures::{SinkExt, StreamExt};
use log::debug;
use nym_client_wasm::stream::NymClientStream;
use nym_sphinx_addressing::clients::Recipient;
use nym_wasm_client_core::client::inbound_messages::InputMessage;
use nym_wasm_client_core::nym_task::connections::TransmissionLane;
use std::sync::Arc;
use tokio_with_wasm::sync::RwLock;
use wasm_bindgen_futures::spawn_local;
use super::error::Error;
use super::message::*;
/// Default number of reply SURBs to attach when sending anonymous messages.
const DEFAULT_REPLY_SURBS: u32 = 10;
/// Initialize the mixnet bridge for libp2p transport.
///
/// This function bridges the NymClientStream to the channel-based async pattern
/// used by the libp2p transport. It spawns background tasks to:
/// 1. Forward outbound messages from the transport to the mixnet client
/// 2. Forward inbound messages from the mixnet client to the transport
pub(crate) async fn initialize_mixnet(
self_address: Recipient,
stream: NymClientStream,
notify_inbound_tx: Option<UnboundedSender<()>>,
) -> Result<
(
Recipient,
UnboundedReceiver<InboundMessage>,
UnboundedSender<OutboundMessage>,
),
Error,
> {
// Channel for inbound messages from the mixnet to the transport
let (inbound_tx, inbound_rx) = unbounded::<InboundMessage>();
// Channel for outbound messages from the transport to the mixnet
let (outbound_tx, outbound_rx) = unbounded::<OutboundMessage>();
// Get client_input for sending before we move stream
let client_input = stream.client_input();
// Wrap stream in Arc<RwLock> so we can share it
let stream = Arc::new(RwLock::new(stream));
// Spawn the outbound message handler
spawn_local(run_outbound_loop(client_input, outbound_rx));
// Spawn the inbound message handler
spawn_local(run_inbound_loop(stream, inbound_tx, notify_inbound_tx));
Ok((self_address, inbound_rx, outbound_tx))
}
/// Background task that forwards outbound messages from the transport to the mixnet.
async fn run_outbound_loop(
client_input: Arc<RwLock<nym_wasm_client_core::client::base_client::ClientInput>>,
mut outbound_rx: UnboundedReceiver<OutboundMessage>,
) {
while let Some(message) = outbound_rx.next().await {
if let Err(e) = send_outbound_message(&client_input, message).await {
debug!("Failed to send outbound message: {:?}", e);
}
}
debug!("Outbound message loop ended");
}
/// Send a single outbound message via the mixnet client.
async fn send_outbound_message(
client_input: &Arc<RwLock<nym_wasm_client_core::client::base_client::ClientInput>>,
message: OutboundMessage,
) -> Result<(), Error> {
log_outbound_message(&message);
let data = message.message.to_bytes();
let input_msg = match (&message.recipient, &message.sender_tag) {
// Reply using SURB (anonymous reply)
(_, Some(sender_tag)) => {
debug!(
"Sending reply to sender_tag {:?}",
sender_tag.to_base58_string()
);
InputMessage::new_reply(sender_tag.clone(), data, TransmissionLane::General, None)
}
// Regular message with recipient, include SURBs for reply capability
(Some(recipient), None) => {
debug!("Sending anonymous message to recipient {}", recipient);
InputMessage::new_anonymous(
*recipient,
data,
DEFAULT_REPLY_SURBS,
TransmissionLane::General,
None,
)
}
// No recipient or sender_tag - cannot route
(None, None) => {
debug!("No recipient or sender_tag provided, cannot route message");
return Err(Error::OutboundSendFailure(
"No recipient or sender_tag provided".to_string(),
));
}
};
// Send via the client input
let mut client = client_input.write().await;
client
.input_sender
.send(input_msg)
.await
.map_err(|_| Error::OutboundSendFailure("InputMessageReceiver stopped".to_string()))
}
/// Log outbound message details for debugging.
fn log_outbound_message(message: &OutboundMessage) {
match &message.message {
Message::TransportMessage(tm) => match &tm.message.message_type {
SubstreamMessageType::OpenResponse => {
debug!(
"Outbound OpenResponse: nonce={}, substream={:?}",
tm.nonce, tm.message.substream_id
);
}
SubstreamMessageType::OpenRequest => {
debug!(
"Outbound OpenRequest: nonce={}, substream={:?}",
tm.nonce, tm.message.substream_id
);
}
SubstreamMessageType::Data(_) => {
debug!(
"Outbound Data: nonce={}, substream={:?}",
tm.nonce, tm.message.substream_id
);
}
SubstreamMessageType::Close => {
debug!(
"Outbound Close: nonce={}, substream={:?}",
tm.nonce, tm.message.substream_id
);
}
},
Message::ConnectionRequest(_) => debug!("Outbound ConnectionRequest"),
Message::ConnectionResponse(_) => debug!("Outbound ConnectionResponse"),
}
}
/// Background task that forwards inbound messages from the mixnet to the transport.
async fn run_inbound_loop(
stream: Arc<RwLock<NymClientStream>>,
inbound_tx: UnboundedSender<InboundMessage>,
notify_inbound_tx: Option<UnboundedSender<()>>,
) {
loop {
// Lock the stream to poll for next message
let msg = {
let mut stream_guard = stream.write().await;
stream_guard.next().await
};
match msg {
Some(reconstructed_msg) => {
// Notify if requested
if let Some(ref notify_tx) = notify_inbound_tx {
let _ = notify_tx.unbounded_send(());
}
let (message_bytes, sender_tag) = reconstructed_msg.into_inner();
match parse_message_data(&message_bytes, sender_tag) {
Ok(data) => {
if inbound_tx.unbounded_send(data).is_err() {
debug!("Inbound channel closed");
break;
}
}
Err(e) => {
debug!("Failed to parse inbound message: {:?}", e);
}
}
}
None => {
debug!("Inbound stream ended");
break;
}
}
}
debug!("Inbound message loop ended");
}
+144
View File
@@ -0,0 +1,144 @@
// Copyright 2024 Nym Technologies SA
// SPDX-License-Identifier: Apache-2.0
//! Message queue for ordering messages received from the mixnet.
//! The Nym mixnet does not guarantee message ordering, so we need to
//! reorder messages based on their nonce before processing.
use log::{debug, warn};
use std::collections::BTreeSet;
use super::message::TransportMessage;
/// MessageQueue is a queue of messages, ordered by nonce, that we've
/// received but are not yet able to process because we're waiting for
/// a message with the next expected nonce first.
/// This is required because Nym does not guarantee any sort of message
/// ordering, only delivery.
/// TODO: is there a DOS vector here where a malicious peer sends us
/// messages only with nonce higher than the next expected nonce?
pub(crate) struct MessageQueue {
/// nonce of the next message we expect to receive on the
/// connection.
/// any messages with a nonce greater than this are pushed into
/// the queue.
/// if we get a message with a nonce equal to this, then we
/// immediately handle it in the transport and increment the nonce.
next_expected_nonce: u64,
/// the actual queue of messages, ordered by nonce.
/// the head of the queue's nonce is always greater
/// than the next expected nonce.
queue: BTreeSet<TransportMessage>,
}
impl MessageQueue {
pub(crate) fn new() -> Self {
MessageQueue {
next_expected_nonce: 0,
queue: BTreeSet::new(),
}
}
pub(crate) fn print_nonces(&self) {
let nonces = self.queue.iter().map(|msg| msg.nonce).collect::<Vec<_>>();
debug!("MessageQueue: {:?}", nonces);
}
/// sets the next expected nonce to 1, indicating that we've received
/// a ConnectionRequest or ConnectionResponse.
pub(crate) fn set_connection_message_received(&mut self) {
if self.next_expected_nonce != 0 {
panic!("connection message received twice");
}
self.next_expected_nonce = self.next_expected_nonce.wrapping_add(1);
}
/// tries to push a message into the queue.
/// if the message has the next expected nonce, then the message is returned,
/// and should be processed by the caller.
/// in that case, the internal queue's next expected nonce is incremented.
pub(crate) fn try_push(&mut self, msg: TransportMessage) -> Option<TransportMessage> {
if msg.nonce == self.next_expected_nonce {
self.next_expected_nonce = self.next_expected_nonce.wrapping_add(1);
Some(msg)
} else {
if msg.nonce < self.next_expected_nonce {
// this shouldn't happen normally, only if the other node
// is not following the protocol
warn!("received a message with a nonce that is too low");
return None;
}
if !self.queue.insert(msg) {
// this shouldn't happen normally, only if the other node
// is not following the protocol
warn!("received a message with a duplicate nonce");
return None;
}
None
}
}
pub(crate) fn pop(&mut self) -> Option<TransportMessage> {
let head = self.queue.first()?;
if head.nonce == self.next_expected_nonce {
self.next_expected_nonce = self.next_expected_nonce.wrapping_add(1);
Some(self.queue.pop_first().unwrap())
} else {
None
}
}
}
#[cfg(test)]
mod test {
use super::super::message::{ConnectionId, SubstreamId, SubstreamMessage};
use super::*;
impl TransportMessage {
fn new(nonce: u64, message: SubstreamMessage, id: ConnectionId) -> Self {
TransportMessage { nonce, message, id }
}
}
#[test]
fn test_message_queue() {
let mut queue = MessageQueue::new();
let test_substream_message =
SubstreamMessage::new_with_data(SubstreamId::generate(), vec![1, 2, 3]);
let connection_id = ConnectionId::generate();
let msg1 = TransportMessage::new(1, test_substream_message.clone(), connection_id.clone());
let msg2 = TransportMessage::new(2, test_substream_message.clone(), connection_id.clone());
let msg3 = TransportMessage::new(3, test_substream_message.clone(), connection_id.clone());
assert_eq!(queue.try_push(msg1.clone()), None);
assert_eq!(queue.try_push(msg3.clone()), None);
assert_eq!(queue.try_push(msg2.clone()), None);
assert_eq!(queue.pop(), None);
// set expected nonce to 1
queue.set_connection_message_received();
assert_eq!(queue.pop(), Some(msg1));
let msg4 = TransportMessage::new(4, test_substream_message.clone(), connection_id.clone());
assert_eq!(queue.try_push(msg4.clone()), None);
assert_eq!(queue.pop(), Some(msg2));
assert_eq!(queue.pop(), Some(msg3));
assert_eq!(queue.pop(), Some(msg4));
assert_eq!(queue.pop(), None);
assert_eq!(queue.next_expected_nonce, 5);
// should just return the message and increment nonce when message nonce = next expected nonce
let msg5 = TransportMessage::new(5, test_substream_message, connection_id);
assert_eq!(queue.try_push(msg5.clone()), Some(msg5));
assert_eq!(queue.next_expected_nonce, 6);
}
}
+284
View File
@@ -0,0 +1,284 @@
// Copyright 2024 Nym Technologies SA
// SPDX-License-Identifier: Apache-2.0
//! Substream implementation providing AsyncRead + AsyncWrite over Nym mixnet.
use super::message::{
ConnectionId, Message, OutboundMessage, SubstreamId, SubstreamMessage, TransportMessage,
};
use futures::{
channel::{mpsc::UnboundedReceiver, oneshot::Receiver},
io::{Error as IoError, ErrorKind},
AsyncRead, AsyncWrite, FutureExt, StreamExt,
};
use nym_sphinx_addressing::clients::Recipient;
use nym_sphinx_anonymous_replies::requests::AnonymousSenderTag;
use nym_wasm_utils::console_log;
use parking_lot::Mutex;
use std::{
pin::Pin,
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
task::{Context, Poll},
};
// Re-export UnboundedSender for use in other modules
pub(crate) use futures::channel::mpsc::UnboundedSender;
#[derive(Debug)]
pub struct Substream {
remote_recipient: Option<Recipient>,
connection_id: ConnectionId,
pub(crate) substream_id: SubstreamId,
/// inbound messages; inbound_tx is in the corresponding Connection
pub(crate) inbound_rx: UnboundedReceiver<Vec<u8>>,
/// outbound messages; go directly to the mixnet
outbound_tx: UnboundedSender<OutboundMessage>,
sender_tag: Option<AnonymousSenderTag>,
/// used to signal when the substream is closed
close_rx: Receiver<()>,
closed: Mutex<bool>,
// buffer of data that's been written to the stream,
// but not yet read by the application.
unread_data: Mutex<Vec<u8>>,
message_nonce: Arc<AtomicU64>,
}
impl Substream {
pub(crate) fn new_with_sender_tag(
remote_recipient: Option<Recipient>,
connection_id: ConnectionId,
substream_id: SubstreamId,
inbound_rx: UnboundedReceiver<Vec<u8>>,
outbound_tx: UnboundedSender<OutboundMessage>,
close_rx: Receiver<()>,
message_nonce: Arc<AtomicU64>,
sender_tag: Option<AnonymousSenderTag>,
) -> Self {
Substream {
remote_recipient,
connection_id,
substream_id,
inbound_rx,
outbound_tx,
sender_tag,
close_rx,
closed: Mutex::new(false),
unread_data: Mutex::new(vec![]),
message_nonce,
}
}
pub(crate) fn new(
remote_recipient: Option<Recipient>,
connection_id: ConnectionId,
substream_id: SubstreamId,
inbound_rx: UnboundedReceiver<Vec<u8>>,
outbound_tx: UnboundedSender<OutboundMessage>,
close_rx: Receiver<()>,
message_nonce: Arc<AtomicU64>,
) -> Self {
Self::new_with_sender_tag(
remote_recipient,
connection_id,
substream_id,
inbound_rx,
outbound_tx,
close_rx,
message_nonce,
None,
)
}
fn check_closed(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Result<(), IoError> {
let closed_err = IoError::new(ErrorKind::Other, "stream closed");
// Poll the close receiver to check if close was signaled
let received_closed = self.close_rx.poll_unpin(cx);
let mut closed = self.closed.lock();
if *closed {
return Err(closed_err);
}
if let Poll::Ready(Ok(())) = received_closed {
*closed = true;
return Err(closed_err);
}
Ok(())
}
}
impl AsyncRead for Substream {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<Result<usize, IoError>> {
console_log!("[Substream::poll_read] called, buf size: {}", buf.len());
// First, check for any buffered unread data
let filled_len = {
let mut unread_data = self.unread_data.lock();
if !unread_data.is_empty() {
let unread_len = unread_data.len();
let buf_len = buf.len();
let copy_len = std::cmp::min(unread_len, buf_len);
buf[..copy_len].copy_from_slice(&unread_data[..copy_len]);
*unread_data = unread_data[copy_len..].to_vec();
copy_len
} else {
0
}
};
// Then check for new data from the channel
let inbound_rx_data = self.inbound_rx.poll_next_unpin(cx);
if let Poll::Ready(Some(data)) = inbound_rx_data {
console_log!(
"[Substream::poll_read] received {} bytes from channel",
data.len()
);
let mut unread_data = self.unread_data.lock();
if filled_len == buf.len() {
// we've filled the buffer, so we'll have to save the rest for later
let mut new = vec![];
new.extend(unread_data.drain(..));
new.extend(data.iter());
*unread_data = new;
return Poll::Ready(Ok(filled_len));
}
// otherwise, there's still room in the buffer, so we'll copy the rest of the data
let remaining_len = buf.len() - filled_len;
let data_len = data.len();
// we have more data than buffer room remaining, save the extra for later
if remaining_len < data_len {
unread_data.extend_from_slice(&data[remaining_len..]);
}
let copied = std::cmp::min(remaining_len, data_len);
buf[filled_len..filled_len + copied].copy_from_slice(&data[..copied]);
console_log!(
"[Substream::poll_read] copied {} bytes total",
filled_len + copied
);
return Poll::Ready(Ok(filled_len + copied));
}
// If we have buffered data, return it
if filled_len > 0 {
console_log!(
"[Substream::poll_read] returning {} buffered bytes",
filled_len
);
return Poll::Ready(Ok(filled_len));
}
// Only check closed state when there's no data to return
// This ensures we drain all buffered data before reporting close
let closed_result = self.as_mut().check_closed(cx);
if let Err(e) = closed_result {
console_log!("[Substream::poll_read] stream closed (no more data): {}", e);
return Poll::Ready(Err(e));
}
Poll::Pending
}
}
impl AsyncWrite for Substream {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, IoError>> {
console_log!("[Substream::poll_write] writing {} bytes", buf.len());
if let Err(e) = self.as_mut().check_closed(cx) {
console_log!("[Substream::poll_write] stream closed: {}", e);
return Poll::Ready(Err(e));
}
let nonce = self.message_nonce.fetch_add(1, Ordering::SeqCst);
console_log!(
"[Substream::poll_write] nonce: {}, sender_tag: {:?}",
nonce,
self.sender_tag.is_some()
);
self.outbound_tx
.unbounded_send(OutboundMessage {
recipient: self.remote_recipient,
message: Message::TransportMessage(TransportMessage {
nonce,
id: self.connection_id.clone(),
message: SubstreamMessage::new_with_data(
self.substream_id.clone(),
buf.to_vec(),
),
}),
sender_tag: self.sender_tag.clone(),
})
.map_err(|e| {
console_log!("[Substream::poll_write] ERROR: {}", e);
IoError::new(
ErrorKind::Other,
format!("poll_write outbound_tx error: {}", e),
)
})?;
console_log!("[Substream::poll_write] successfully queued message");
Poll::Ready(Ok(buf.len()))
}
fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), IoError>> {
let nonce = self.message_nonce.fetch_add(1, Ordering::SeqCst);
let mut closed = self.closed.lock();
if *closed {
return Poll::Ready(Err(IoError::new(ErrorKind::Other, "stream closed")));
}
*closed = true;
// send a close message to the mixnet
self.outbound_tx
.unbounded_send(OutboundMessage {
recipient: self.remote_recipient,
message: Message::TransportMessage(TransportMessage {
nonce,
id: self.connection_id.clone(),
message: SubstreamMessage::new_close(self.substream_id.clone()),
}),
sender_tag: self.sender_tag.clone(),
})
.map_err(|e| {
IoError::new(
ErrorKind::Other,
format!("poll_close outbound_rx error: {}", e),
)
})?;
Poll::Ready(Ok(()))
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), IoError>> {
if let Err(e) = self.check_closed(cx) {
return Poll::Ready(Err(e));
}
Poll::Ready(Ok(()))
}
}
+607
View File
@@ -0,0 +1,607 @@
// Copyright 2024 Nym Technologies SA
// SPDX-License-Identifier: Apache-2.0
//! NymTransport implementation of libp2p Transport trait for WASM.
use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
use futures::channel::oneshot;
use futures::prelude::*;
use gloo_timers::future::TimeoutFuture;
use libp2p::core::{
multiaddr::Multiaddr,
transport::{DialOpts, ListenerId, TransportError, TransportEvent},
Transport,
};
use libp2p_identity::{Keypair, PeerId};
use log::{debug, info};
use nym_client_wasm::stream::NymClientStream;
use nym_sphinx_addressing::clients::Recipient;
use send_wrapper::SendWrapper;
use std::{
collections::HashMap,
pin::Pin,
str::FromStr,
task::{Context, Poll, Waker},
time::Duration,
};
use super::connection::{Connection, PendingConnection};
use super::error::Error;
use super::message::{
ConnectionId, ConnectionMessage, InboundMessage, Message, OutboundMessage, SubstreamMessage,
TransportMessage,
};
use super::mixnet::initialize_mixnet;
use super::queue::MessageQueue;
use super::DEFAULT_HANDSHAKE_TIMEOUT_SECS;
use nym_sphinx_anonymous_replies::requests::AnonymousSenderTag;
/// InboundTransportEvent represents an inbound event from the mixnet.
pub enum InboundTransportEvent {
ConnectionRequest(Upgrade),
ConnectionResponse,
TransportMessage,
}
/// NymTransport implements the Transport trait using the Nym mixnet.
pub struct NymTransport {
/// our Nym address
self_address: Recipient,
pub(crate) listen_addr: Multiaddr,
pub(crate) listener_id: ListenerId,
/// our libp2p keypair; currently not really used
keypair: Keypair,
/// established connections -> channel which sends messages received from
/// the mixnet to the corresponding Connection
connections: HashMap<ConnectionId, UnboundedSender<SubstreamMessage>>,
/// outbound pending dials
pending_dials: HashMap<ConnectionId, PendingConnection>,
/// connection message queues
message_queues: HashMap<ConnectionId, MessageQueue>,
/// inbound mixnet messages
inbound_rx: UnboundedReceiver<InboundMessage>,
/// outbound mixnet messages
outbound_tx: UnboundedSender<OutboundMessage>,
/// inbound messages for Transport.poll()
poll_rx: UnboundedReceiver<TransportEvent<Upgrade, Error>>,
/// outbound messages to Transport.poll()
poll_tx: UnboundedSender<TransportEvent<Upgrade, Error>>,
waker: Option<Waker>,
/// Timeout for the [`Upgrade`] future (in milliseconds for WASM).
handshake_timeout_ms: u32,
}
impl NymTransport {
/// Create a new NymTransport from a NymClientStream.
///
/// # Example
/// ```ignore
/// use nym_libp2p_wasm::{create_transport_client_async, NymTransport};
/// use libp2p_identity::Keypair;
///
/// // Create the transport client
/// let result = create_transport_client_async(None).await?;
///
/// // Create the transport
/// let keypair = Keypair::generate_ed25519();
/// let transport = NymTransport::new(
/// result.self_address,
/// result.stream,
/// keypair,
/// ).await?;
/// ```
pub async fn new(
self_address: Recipient,
stream: NymClientStream,
keypair: Keypair,
) -> Result<Self, Error> {
Self::new_with_options(self_address, stream, keypair, None, None).await
}
/// Create a new NymTransport with a custom timeout.
pub async fn new_with_timeout(
self_address: Recipient,
stream: NymClientStream,
keypair: Keypair,
timeout: Duration,
) -> Result<Self, Error> {
Self::new_with_options(self_address, stream, keypair, None, Some(timeout)).await
}
/// Add timeout to transport and return self.
#[allow(dead_code)]
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.handshake_timeout_ms = timeout.as_millis() as u32;
self
}
async fn new_with_options(
self_address: Recipient,
stream: NymClientStream,
keypair: Keypair,
notify_inbound_tx: Option<UnboundedSender<()>>,
timeout: Option<Duration>,
) -> Result<Self, Error> {
let (self_address, inbound_rx, outbound_tx) =
initialize_mixnet(self_address, stream, notify_inbound_tx).await?;
let listen_addr = nym_address_to_multiaddress(self_address)?;
let listener_id = ListenerId::next();
let (poll_tx, poll_rx) = unbounded::<TransportEvent<Upgrade, Error>>();
poll_tx
.unbounded_send(TransportEvent::NewAddress {
listener_id,
listen_addr: listen_addr.clone(),
})
.map_err(|_| Error::SendErrorTransportEvent)?;
let handshake_timeout_ms = timeout
.map(|t| t.as_millis() as u32)
.unwrap_or((DEFAULT_HANDSHAKE_TIMEOUT_SECS * 1000) as u32);
Ok(Self {
self_address,
listen_addr,
listener_id,
keypair,
connections: HashMap::new(),
pending_dials: HashMap::new(),
message_queues: HashMap::new(),
inbound_rx,
outbound_tx,
poll_rx,
poll_tx,
waker: None,
handshake_timeout_ms,
})
}
pub(crate) fn peer_id(&self) -> PeerId {
PeerId::from_public_key(&self.keypair.public())
}
fn handle_message_queue_on_connection_initiation(
&mut self,
id: &ConnectionId,
) -> Result<(), Error> {
debug!("handle_message_queue_on_connection_initiation");
let Some(inbound_tx) = self.connections.get(id) else {
// this should not happen
return Err(Error::NoConnectionForTransportMessage);
};
match self.message_queues.get_mut(id) {
Some(queue) => {
// update expected nonce
queue.set_connection_message_received();
// push pending inbound some messages in this case
while let Some(msg) = queue.pop() {
debug!(
"popped queued message with nonce {} for connection",
msg.nonce
);
inbound_tx
.unbounded_send(msg.message.clone())
.map_err(|e| Error::InboundSendFailure(e.to_string()))?;
}
}
None => {
// no queue exists for this connection, create one
let queue = MessageQueue::new();
self.message_queues.insert(id.clone(), queue);
let queue = self.message_queues.get_mut(id).unwrap();
queue.set_connection_message_received();
}
};
debug!("returning from handle_message_queue_on_connection_initiation");
Ok(())
}
// handle_connection_response resolves the pending connection corresponding to the response
// (if there is one) into a Connection.
fn handle_connection_response(
&mut self,
msg: &ConnectionMessage,
sender_tag: Option<AnonymousSenderTag>,
) -> Result<(), Error> {
if self.connections.contains_key(&msg.id) {
return Err(Error::ConnectionAlreadyEstablished);
}
if let Some(pending_conn) = self.pending_dials.remove(&msg.id) {
// Create connection with sender_tag
let (conn, conn_tx) = self.create_connection_types(
msg.peer_id,
Some(pending_conn.remote_recipient), // Dialer knows recipient
msg.id.clone(),
sender_tag,
);
self.connections.insert(msg.id.clone(), conn_tx);
self.handle_message_queue_on_connection_initiation(&msg.id)?;
pending_conn
.connection_tx
.send(conn)
.map_err(|_| Error::ConnectionSendFailure)?;
if let Some(waker) = self.waker.take() {
waker.wake();
}
Ok(())
} else {
Err(Error::NoConnectionForResponse)
}
}
/// handle_connection_request handles an incoming connection request, sends back a
/// connection response, and finally completes the upgrade into a Connection.
fn handle_connection_request(
&mut self,
msg: &ConnectionMessage,
sender_tag: Option<AnonymousSenderTag>,
) -> Result<Connection, Error> {
// ensure we don't already have a conn with the same id
if self.connections.contains_key(&msg.id) {
return Err(Error::ConnectionIDExists);
}
// Create connection with sender_tag
let (conn, conn_tx) = self.create_connection_types(
msg.peer_id,
None, // Receiver doesn't know dialer address
msg.id.clone(),
sender_tag.clone(),
);
info!("Created connection: {:?}", conn);
self.connections.insert(msg.id.clone(), conn_tx);
info!("Current active connections: {}", self.connections.len());
self.handle_message_queue_on_connection_initiation(&msg.id)?;
let resp = ConnectionMessage {
peer_id: self.peer_id(),
id: msg.id.clone(),
};
// Send response using sender_tag if available
self.outbound_tx
.unbounded_send(OutboundMessage {
message: Message::ConnectionResponse(resp),
recipient: None,
sender_tag,
})
.map_err(|e| Error::OutboundSendFailure(e.to_string()))?;
debug!(
"Sent ConnectionResponse with sender_tag: {:?}",
sender_tag.is_some()
);
if let Some(waker) = self.waker.take() {
waker.wake();
}
Ok(conn)
}
fn handle_transport_message(&mut self, msg: TransportMessage) -> Result<(), Error> {
let queue = match self.message_queues.get_mut(&msg.id) {
Some(queue) => queue,
None => {
// no queue exists for this connection, create one
let queue = MessageQueue::new();
self.message_queues.insert(msg.id.clone(), queue);
self.message_queues.get_mut(&msg.id).unwrap()
}
};
queue.print_nonces();
let nonce = msg.nonce;
let Some(msg) = queue.try_push(msg) else {
// don't push the message yet, it's been queued
debug!("message with nonce {} queued for connection", nonce);
return Ok(());
};
let Some(inbound_tx) = self.connections.get(&msg.id) else {
return Err(Error::NoConnectionForTransportMessage);
};
// send original message
debug!(
"sending original message with nonce {} for connection",
nonce
);
inbound_tx
.unbounded_send(msg.message.clone())
.map_err(|e| Error::InboundSendFailure(e.to_string()))?;
// try to pop queued messages and send them on inbound channel
while let Some(msg) = queue.pop() {
debug!(
"popped queued message with nonce {} for connection",
msg.nonce
);
inbound_tx
.unbounded_send(msg.message.clone())
.map_err(|e| Error::InboundSendFailure(e.to_string()))?;
}
if let Some(waker) = self.waker.clone().take() {
waker.wake();
}
Ok(())
}
fn create_connection_types(
&self,
remote_peer_id: PeerId,
remote_recipient: Option<Recipient>,
id: ConnectionId,
sender_tag: Option<AnonymousSenderTag>,
) -> (Connection, UnboundedSender<SubstreamMessage>) {
let (inbound_tx, inbound_rx) = unbounded::<SubstreamMessage>();
let conn = Connection::new_with_sender_tag(
remote_peer_id,
remote_recipient,
id,
inbound_rx,
self.outbound_tx.clone(),
sender_tag,
);
(conn, inbound_tx)
}
/// handle_inbound handles an inbound message from the mixnet, received via self.inbound_rx.
fn handle_inbound(
&mut self,
msg: Message,
sender_tag: Option<AnonymousSenderTag>,
) -> Result<InboundTransportEvent, Error> {
match msg {
Message::ConnectionRequest(inner) => {
debug!("got inbound connection request {:?}", inner);
match self.handle_connection_request(&inner, sender_tag) {
Ok(conn) => {
let (connection_tx, connection_rx) =
oneshot::channel::<(PeerId, Connection)>();
let upgrade = Upgrade::new(connection_rx);
connection_tx
.send((inner.peer_id, conn))
.map_err(|_| Error::ConnectionSendFailure)?;
Ok(InboundTransportEvent::ConnectionRequest(upgrade))
}
Err(e) => Err(e),
}
}
Message::ConnectionResponse(msg) => {
debug!("got inbound connection response {:?}", msg);
self.handle_connection_response(&msg, sender_tag)
.map(|_| InboundTransportEvent::ConnectionResponse)
}
Message::TransportMessage(msg) => {
debug!(
"Transport received TransportMessage: nonce={}, substream={:?}, msg_type={:?}",
msg.nonce, msg.message.substream_id, msg.message.message_type
);
self.handle_transport_message(msg)
.map(|_| InboundTransportEvent::TransportMessage)
}
}
}
}
/// Upgrade represents a transport listener upgrade.
/// Note: we immediately upgrade a connection request to a connection,
/// so this only contains a channel for receiving that connection.
pub struct Upgrade {
connection_rx: oneshot::Receiver<(PeerId, Connection)>,
}
impl Upgrade {
fn new(connection_rx: oneshot::Receiver<(PeerId, Connection)>) -> Upgrade {
Upgrade { connection_rx }
}
}
impl Future for Upgrade {
type Output = Result<(PeerId, Connection), Error>;
// poll checks if the upgrade has turned into a connection yet
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.connection_rx.poll_unpin(cx) {
Poll::Ready(Ok(conn)) => Poll::Ready(Ok(conn)),
Poll::Ready(Err(_)) => Poll::Ready(Err(Error::RecvFailure)),
Poll::Pending => Poll::Pending,
}
}
}
impl Transport for NymTransport {
type Output = (PeerId, Connection);
type Error = Error;
type ListenerUpgrade = Upgrade;
// Use SendWrapper to make the future Send for libp2p's SwarmBuilder
// This is safe in WASM's single-threaded environment
type Dial = futures::future::BoxFuture<'static, Result<Self::Output, Self::Error>>;
fn listen_on(
&mut self,
_listener_id: ListenerId,
_multi_addr: libp2p::Multiaddr,
) -> Result<(), TransportError<Self::Error>> {
info!("called listen_on, this is currently just a dummy function - client starts listening on new()");
Ok(())
}
fn remove_listener(&mut self, id: ListenerId) -> bool {
if self.listener_id != id {
return false;
}
let _ = self.poll_tx.unbounded_send(TransportEvent::ListenerClosed {
listener_id: id,
reason: Ok(()),
});
true
}
fn dial(
&mut self,
addr: Multiaddr,
_dial_opts: DialOpts,
) -> Result<Self::Dial, TransportError<Self::Error>> {
debug!("dialing {}", addr);
let id = ConnectionId::generate();
// create remote recipient address
let recipient = multiaddress_to_nym_address(addr).map_err(TransportError::Other)?;
// create pending conn structs and store
let (connection_tx, connection_rx) = oneshot::channel::<Connection>();
let inner_pending_conn = PendingConnection::new(recipient, connection_tx);
self.pending_dials.insert(id.clone(), inner_pending_conn);
let local_key = Keypair::generate_ed25519();
let connection_peer_id = PeerId::from(local_key.public());
// put ConnectionRequest message into outbound message channel
let msg = ConnectionMessage {
peer_id: connection_peer_id,
id,
};
let outbound_tx = self.outbound_tx.clone();
let mut waker = self.waker.clone();
let handshake_timeout_ms = self.handshake_timeout_ms;
// Wrap in SendWrapper to satisfy Send bounds for SwarmBuilder
// This is safe because WASM is single-threaded
Ok(SendWrapper::new(async move {
outbound_tx
.unbounded_send(OutboundMessage {
message: Message::ConnectionRequest(msg),
recipient: Some(recipient),
sender_tag: None,
})
.map_err(|e| Error::OutboundSendFailure(e.to_string()))?;
debug!("sent outbound ConnectionRequest");
if let Some(waker) = waker.take() {
waker.wake();
};
// Use gloo_timers for WASM-compatible timeout
let timeout_future = TimeoutFuture::new(handshake_timeout_ms);
futures::select! {
conn_result = connection_rx.fuse() => {
match conn_result {
Ok(conn) => Ok((conn.peer_id, conn)),
Err(_) => Err(Error::RecvFailure),
}
}
_ = timeout_future.fuse() => {
Err(Error::DialTimeout)
}
}
})
.boxed())
}
fn poll(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<TransportEvent<Self::ListenerUpgrade, Self::Error>> {
// new addresses + listener close events
if let Poll::Ready(Some(res)) = self.poll_rx.poll_next_unpin(cx) {
return Poll::Ready(res);
}
// check for and handle inbound messages
while let Poll::Ready(Some(msg)) = self.inbound_rx.poll_next_unpin(cx) {
debug!(
"TRANSPORT: Received inbound message type: {:?}",
match &msg.0 {
Message::ConnectionRequest(_) => "ConnectionRequest",
Message::ConnectionResponse(_) => "ConnectionResponse",
Message::TransportMessage(_) => "TransportMessage",
}
);
match self.handle_inbound(msg.0, msg.1) {
Ok(event) => match event {
InboundTransportEvent::ConnectionRequest(upgrade) => {
info!("InboundTransportEvent::ConnectionRequest");
return Poll::Ready(TransportEvent::Incoming {
listener_id: self.listener_id,
upgrade,
local_addr: self.listen_addr.clone(),
send_back_addr: self.listen_addr.clone(),
});
}
InboundTransportEvent::ConnectionResponse => {
info!("InboundTransportEvent::ConnectionResponse");
}
InboundTransportEvent::TransportMessage => {
debug!("InboundTransportEvent::TransportMessage");
}
},
Err(e) => {
return Poll::Ready(TransportEvent::ListenerError {
listener_id: self.listener_id,
error: e,
});
}
};
}
self.waker = Some(cx.waker().clone());
Poll::Pending
}
}
/// Convert a Nym Recipient address to a libp2p Multiaddr.
///
/// Format: `/nym/<base58-encoded-nym-address>`
pub fn nym_address_to_multiaddress(addr: Recipient) -> Result<Multiaddr, Error> {
// Create a multiaddr using the Nym protocol
// Format: /nym/<base58-encoded-nym-address>
// This requires the ChainSafe multiaddr fork with Protocol::Nym support
Multiaddr::from_str(&format!("/nym/{}", addr)).map_err(Error::FailedToFormatMultiaddr)
}
fn multiaddress_to_nym_address(multiaddr: Multiaddr) -> Result<Recipient, Error> {
// Parse the Nym address from the multiaddr
// We expect format: /nym/<nym-address>
let addr_str = multiaddr.to_string();
if let Some(nym_addr) = addr_str.strip_prefix("/nym/") {
return Recipient::from_str(nym_addr).map_err(Error::InvalidRecipientBytes);
}
Err(Error::InvalidProtocolForMultiaddr)
}
File diff suppressed because it is too large Load Diff
+9 -2
View File
@@ -8,6 +8,7 @@ use crate::go_bridge::goWasmSetMixFetchRequestTimeout;
use crate::request_writer::RequestWriter;
use crate::socks_helpers::{socks5_connect_request, socks5_data_request};
use crate::{config, RequestId};
use futures::SinkExt;
use js_sys::Promise;
use nym_bin_common::bin_info;
use nym_socks5_requests::RemoteAddress;
@@ -24,7 +25,9 @@ use nym_wasm_client_core::{IdentityKey, QueryReqwestRpcNyxdClient, Recipient};
use nym_wasm_utils::console_log;
use nym_wasm_utils::error::PromisableResult;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::sync::RwLock;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::future_to_promise;
@@ -36,7 +39,7 @@ pub struct MixFetchClient {
self_address: Recipient,
client_input: ClientInput,
client_input: Arc<RwLock<ClientInput>>,
requests: ActiveRequests,
@@ -185,7 +188,7 @@ impl MixFetchClientBuilder {
invalidated: AtomicBool::new(false),
mix_fetch_config: self.config.mix_fetch,
self_address,
client_input,
client_input: Arc::new(RwLock::new(client_input)),
requests: active_requests,
_shutdown_manager: Mutex::new(started_client.shutdown_handle),
})
@@ -261,6 +264,8 @@ impl MixFetchClient {
// the expect here is fine as it implies an unrecoverable failure since one of the client core
// tasks has terminated
self.client_input
.write()
.await
.input_sender
.send(input)
.await
@@ -288,6 +293,8 @@ impl MixFetchClient {
// the expect here is fine as it implies an unrecoverable failure since one of the client core
// tasks has terminated
self.client_input
.write()
.await
.input_sender
.send(input)
.await