Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a520df064 | |||
| 6bb5f1bcae | |||
| e6b10c708c | |||
| 56fc135c2f | |||
| 24ff4272b6 | |||
| e6d5f463e7 | |||
| cad47732b7 | |||
| f4ff6717e0 | |||
| d8b8b38101 | |||
| 8e6ceddc66 | |||
| d029a58e13 | |||
| 21f8cb89d6 | |||
| 4ac9cdb1b1 | |||
| 296f243433 | |||
| 10b6ad050b | |||
| 5441960976 | |||
| e6e25dacea | |||
| 77ab256588 | |||
| fa2cbb5d21 | |||
| c3e4f944d5 | |||
| 933bbbb67d | |||
| 3f300cc2c1 | |||
| 30da87bf41 | |||
| e32783bced | |||
| 49687270b1 | |||
| a49957cb5c | |||
| 8b8c583ac5 | |||
| 9d808d30c2 | |||
| 880a33fb20 | |||
| 932bd0660f | |||
| 80f3ddca89 | |||
| c7b0eed15f | |||
| 9224f01d49 | |||
| 878fe85d66 | |||
| f060d63f2e |
Generated
+531
-3
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
-4
@@ -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::{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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 }
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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};
|
||||
|
||||
@@ -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,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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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 |
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(()))
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user