Port native reference implementation to WASM compatible state
This commit is contained in:
Generated
+521
-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",
|
||||
@@ -5693,7 +6090,7 @@ dependencies = [
|
||||
"clap",
|
||||
"comfy-table",
|
||||
"futures",
|
||||
"gloo-timers",
|
||||
"gloo-timers 0.3.0",
|
||||
"http-body-util",
|
||||
"humantime",
|
||||
"hyper 1.8.1",
|
||||
@@ -5802,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",
|
||||
@@ -6837,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"
|
||||
@@ -9382,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"
|
||||
@@ -9465,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"
|
||||
@@ -10106,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"
|
||||
@@ -10308,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"
|
||||
@@ -12675,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"
|
||||
@@ -12898,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
|
||||
|
||||
@@ -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,48 @@
|
||||
# nym-libp2p-wasm
|
||||
|
||||
This crate provides a libp2p `Transport` implementation that uses the Nym mixnet for p2p communication in browser environments.
|
||||
|
||||
## Features
|
||||
|
||||
- libp2p `Transport` trait implementation
|
||||
- Stream multiplexing via `StreamMuxer`
|
||||
- Message ordering over the unordered mixnet
|
||||
- Anonymous replies using SURBs
|
||||
- Browser compatible
|
||||
|
||||
## Building
|
||||
|
||||
```sh
|
||||
# Check
|
||||
cargo check -p nym-libp2p-wasm --target wasm32-unknown-unknown
|
||||
cargo build -p nym-libp2p-wasm --target wasm32-unknown-unknown
|
||||
|
||||
# Build with wasm-pack (for browser):
|
||||
cd wasm/libp2p-nym
|
||||
wasm-pack build --target web
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run WASM tests with wasm-pack:
|
||||
```sh
|
||||
cd wasm/libp2p-nym
|
||||
|
||||
# Browser tests (headless)
|
||||
wasm-pack test --headless --firefox
|
||||
wasm-pack test --headless --chrome
|
||||
```
|
||||
|
||||
## 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,367 @@
|
||||
// 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 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>> {
|
||||
if let Poll::Ready(Some(substream)) = self.inbound_open_rx.poll_next_unpin(cx) {
|
||||
return Poll::Ready(Ok(substream));
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
|
||||
fn poll_outbound(
|
||||
mut self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Result<Self::Substream, Self::Error>> {
|
||||
debug!("poll_outbound called");
|
||||
let result = self.new_outbound_substream();
|
||||
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) => {
|
||||
debug!("Processing Data: {:?}", &data);
|
||||
let inbound_tx = self
|
||||
.substream_inbound_txs
|
||||
.get_mut(&msg.substream_id)
|
||||
.expect("must have a substream channel for substream");
|
||||
|
||||
// NOTE: this ignores channel closed errors, which is fine because the substream
|
||||
// might have been closed/dropped
|
||||
let _ = inbound_tx.unbounded_send(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,124 @@
|
||||
// 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;
|
||||
|
||||
// WASM-bindgen exports for JavaScript
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod js_api {
|
||||
use js_sys::Promise;
|
||||
use libp2p_identity::Keypair;
|
||||
use nym_wasm_utils::console_log;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::future_to_promise;
|
||||
|
||||
use crate::client::{create_transport_client_async, TransportClientOpts};
|
||||
use crate::transport::NymTransport;
|
||||
|
||||
/// Initialize and test the libp2p-nym transport.
|
||||
/// Returns a Promise that resolves with the Nym address on success.
|
||||
///
|
||||
/// @param force_tls - If true, forces TLS connections to gateways (required for browsers)
|
||||
/// @param client_id - Optional client ID for storage namespace (defaults to "libp2p-transport")
|
||||
#[wasm_bindgen(js_name = "testTransport")]
|
||||
pub fn test_transport(
|
||||
nym_api_url: Option<String>,
|
||||
force_tls: Option<bool>,
|
||||
client_id: Option<String>,
|
||||
) -> Promise {
|
||||
future_to_promise(async move {
|
||||
let opts = TransportClientOpts {
|
||||
nym_api_url,
|
||||
force_tls: force_tls.unwrap_or(true), // Default to true for browser safety
|
||||
client_id, // Uses "libp2p-transport" if None
|
||||
};
|
||||
console_log!("testTransport: force_tls={}", opts.force_tls);
|
||||
|
||||
let result = create_transport_client_async(opts)
|
||||
.await
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
|
||||
let address = result.self_address.to_string();
|
||||
console_log!("Client connected! Address: {}", address);
|
||||
|
||||
// Create a libp2p keypair
|
||||
let keypair = Keypair::generate_ed25519();
|
||||
console_log!("Generated libp2p keypair");
|
||||
|
||||
// Create the transport
|
||||
let _transport = NymTransport::new(result.self_address, result.stream, keypair)
|
||||
.await
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
|
||||
console_log!("Transport created successfully!");
|
||||
console_log!("Listening on: /nym/{}", address);
|
||||
|
||||
Ok(JsValue::from_str(&address))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,254 @@
|
||||
// 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 log::debug;
|
||||
use nym_sphinx_addressing::clients::Recipient;
|
||||
use nym_sphinx_anonymous_replies::requests::AnonymousSenderTag;
|
||||
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>> {
|
||||
let closed_result = self.as_mut().check_closed(cx);
|
||||
if let Err(e) = closed_result {
|
||||
return Poll::Ready(Err(e));
|
||||
}
|
||||
|
||||
let inbound_rx_data = self.inbound_rx.poll_next_unpin(cx);
|
||||
|
||||
// first, write any previously unread data to the buf
|
||||
let mut unread_data = self.unread_data.lock();
|
||||
let filled_len = 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
|
||||
};
|
||||
|
||||
if let Poll::Ready(Some(data)) = inbound_rx_data {
|
||||
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]);
|
||||
debug!("poll_read copied {} bytes", copied);
|
||||
return Poll::Ready(Ok(copied));
|
||||
}
|
||||
|
||||
if filled_len > 0 {
|
||||
debug!("poll_read copied {} bytes", filled_len);
|
||||
return Poll::Ready(Ok(filled_len));
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for Substream {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, IoError>> {
|
||||
if let Err(e) = self.as_mut().check_closed(cx) {
|
||||
return Poll::Ready(Err(e));
|
||||
}
|
||||
|
||||
let nonce = self.message_nonce.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
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| {
|
||||
IoError::new(
|
||||
ErrorKind::Other,
|
||||
format!("poll_write outbound_tx error: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user