# Nym Documentation @version: 1.20.4 @generated: 2026-06-25 @pages: 166 @source: https://github.com/nymtech/nym/tree/develop/documentation/docs --- title: Nym Network Architecture: How the Mixnet Works description: Deep dive into Nym network architecture, cryptographic systems, and how the mixnet provides network-level privacy against end-to-end attackers. url: https://nym.com/docs/network --- # The Nym Network The Nym Network is decentralised privacy infrastructure that protects against **network-level** surveillance. Unlike tools that focus on encrypting message content, Nym protects the metadata surrounding communication: who talks to whom, when, how often, and how much. This metadata is sufficient for observers to map relationships and build behavioural profiles even without access to any message content. See [The Privacy Problem](/network/overview/privacy-problem) for a fuller treatment. Nym offers two operating modes with different privacy/performance trade-offs, both available through [NymVPN](https://nymvpn.com). Developers can also integrate Mixnet mode directly via the [Nym SDKs](/developers). See [Choosing a Mode](/network/overview/choosing-a-mode) for guidance on which fits a given threat model. ### NymVPN [NymVPN](https://nymvpn.com) is a subscription-based application that provides access to both modes: - **dVPN mode** routes traffic through 2 hops using WireGuard with enhanced layer encryption. Fast enough for browsing and streaming, with strong privacy against typical adversaries. - **Mixnet mode** routes traffic through 5 hops with packet mixing, timing delays, and cover traffic. Every packet is the same size, each hop only sees the next destination, and a constant stream of dummy packets hides when real communication is occurring. Designed for privacy against adversaries capable of observing the entire network. Both modes use the same underlying infrastructure. ### Developer SDKs The [Nym SDKs](/developers) allow developers to embed mixnet functionality directly into applications, with the same privacy guarantees as NymVPN's Mixnet mode. SDK usage is currently free for development and testing. The SDKs do **not** provide access to dVPN mode. ## Paying for privacy without losing it A fundamental weakness of traditional VPNs is that payment records can deanonymise users, since most providers link sessions to account IDs. Nym addresses this with **zk-nyms**: zero-knowledge anonymous credentials that prove payment without revealing any other information. Each credential covers a small chunk of bandwidth and is unlinkable to any other. When you pay for NymVPN, your payment is converted into a credential that can be split and re-randomized. Each Gateway connection uses a fresh, unlinkable proof; the Gateway verifies that you have paid without learning who you are. Your subscription cannot be linked to your network activity, even by infrastructure operators. ## Further reading - **Network architecture:** [Overview](/network/overview) · [dVPN Mode](/network/dvpn-mode) · [Mixnet Mode](/network/mixnet-mode) · [Cryptography](/network/cryptography) · [Infrastructure](/network/infrastructure) · [Reference](/network/reference) - **Application development:** [Developer documentation](/developers) - **Node operation:** [Operator documentation](/operators/introduction) --- title: Nym Network Architecture: How the Mixnet Works description: Deep dive into Nym network architecture, cryptographic systems, and how the mixnet provides network-level privacy against end-to-end attackers. url: https://nym.com/docs/network --- # The Nym Network The Nym Network is decentralised privacy infrastructure that protects against **network-level** surveillance. Unlike tools that focus on encrypting message content, Nym protects the metadata surrounding communication: who talks to whom, when, how often, and how much. This metadata is sufficient for observers to map relationships and build behavioural profiles even without access to any message content. See [The Privacy Problem](/network/overview/privacy-problem) for a fuller treatment. Nym offers two operating modes with different privacy/performance trade-offs, both available through [NymVPN](https://nymvpn.com). Developers can also integrate Mixnet mode directly via the [Nym SDKs](/developers). See [Choosing a Mode](/network/overview/choosing-a-mode) for guidance on which fits a given threat model. ### NymVPN [NymVPN](https://nymvpn.com) is a subscription-based application that provides access to both modes: - **dVPN mode** routes traffic through 2 hops using WireGuard with enhanced layer encryption. Fast enough for browsing and streaming, with strong privacy against typical adversaries. - **Mixnet mode** routes traffic through 5 hops with packet mixing, timing delays, and cover traffic. Every packet is the same size, each hop only sees the next destination, and a constant stream of dummy packets hides when real communication is occurring. Designed for privacy against adversaries capable of observing the entire network. Both modes use the same underlying infrastructure. ### Developer SDKs The [Nym SDKs](/developers) allow developers to embed mixnet functionality directly into applications, with the same privacy guarantees as NymVPN's Mixnet mode. SDK usage is currently free for development and testing. The SDKs do **not** provide access to dVPN mode. ## Paying for privacy without losing it A fundamental weakness of traditional VPNs is that payment records can deanonymise users, since most providers link sessions to account IDs. Nym addresses this with **zk-nyms**: zero-knowledge anonymous credentials that prove payment without revealing any other information. Each credential covers a small chunk of bandwidth and is unlinkable to any other. When you pay for NymVPN, your payment is converted into a credential that can be split and re-randomized. Each Gateway connection uses a fresh, unlinkable proof; the Gateway verifies that you have paid without learning who you are. Your subscription cannot be linked to your network activity, even by infrastructure operators. ## Further reading - **Network architecture:** [Overview](/network/overview) · [dVPN Mode](/network/dvpn-mode) · [Mixnet Mode](/network/mixnet-mode) · [Cryptography](/network/cryptography) · [Infrastructure](/network/infrastructure) · [Reference](/network/reference) - **Application development:** [Developer documentation](/developers) - **Node operation:** [Operator documentation](/operators/introduction) --- title: Nym Network Overview description: Introduction to the Nym Network, a privacy infrastructure that protects metadata including who communicates with whom, when, and how often. url: https://nym.com/docs/network/overview --- # Overview The Nym Network is a privacy infrastructure that protects metadata: not just message content, but who is talking to whom, when, and how often. This section explains what the network does, why it exists, and how it compares to other approaches. ## In this section - [The Privacy Problem](/network/overview/privacy-problem): what metadata is, why it matters, and what adversary models Nym is designed against - [Choosing a Mode](/network/overview/choosing-a-mode): how dVPN and Mixnet mode differ, and guidance on which fits your use case - [Nym vs Other Systems](/network/overview/comparisons): how Nym compares to VPNs, Tor, I2P, and E2EE ## Network Components All traffic-routing infrastructure runs on [Nym Nodes](/network/infrastructure/nym-nodes), a single binary that operators configure to serve as an Entry Gateway, Mix Node, or Exit Gateway depending on their setup. Network coordination, token bonding, and the distributed credential system all live on the [Nyx blockchain](/network/infrastructure/nyx), a Cosmos SDK chain whose on-chain topology registry removes the need for a centralised directory server. --- title: The Privacy Problem: Why Metadata Matters description: Why metadata exposure is a critical privacy threat, how adversaries exploit traffic patterns, and why traditional solutions like VPNs, Tor, and E2EE fall short. url: https://nym.com/docs/network/overview/privacy-problem --- # The Privacy Problem ## Metadata When you communicate over the internet, two types of information are in play: - The **content** is the actual message, file, or data being sent. - The **metadata** is everything else: who is talking to whom, when, from where, and how often. Some metadata is visible in every packet (source/destination IPs, timestamps, sizes), whereas other metadata only emerges from patterns over time: interaction frequency, session durations, and behavioural fingerprints that can identify users across sessions. See [Maximum Transmission Units](https://en.wikipedia.org/wiki/Maximum_transmission_unit#MTUs_for_common_media) for one example of what packet sizes reveal. TLS and end-to-end encryption protect content, which is often the [focus of media attention](https://wire.com/en/blog/whatsapp-end-to-end-encryption-risks). However, most solutions don't protect metadata at all, and some falsely claim to. Metadata alone is enough to reconstruct who you talk to, when, and from where. Intelligence agencies know this; as former NSA Director Michael Hayden put it, ["We kill people based on metadata."](https://committees.parliament.uk/writtenevidence/36962/html/) ## The adversary models **Mixnet mode** is designed to protect against **Global Passive Adversaries**: entities that can observe traffic across the entire network at once, such as nation-state level agencies, large corporations with broad network infrastructure, ISPs, or any combination sharing data. The assumption is worst-case: the adversary monitors all entry and exit points, correlates timing, applies machine learning to traffic patterns, and runs long-term statistical analysis. When Tor launched in 2002, this was considered unrealistic - machine learning and the increase in computation power have made this unfortunately more of a potential reality today. **dVPN mode** does not defend against timing analysis, but it splits trust across two independent operators and removes payment linkability, which already addresses the biggest weaknesses of traditional VPNs. For a comparison with VPNs, Tor, and I2P, see [Nym vs Other Systems](/network/overview/comparisons). For help picking a mode, see [Choosing a Mode](/network/overview/choosing-a-mode). --- title: Choosing Between dVPN and Mixnet Mode description: When to use NymVPN's dVPN mode for low-latency browsing versus Mixnet mode for metadata protection against traffic analysis. url: https://nym.com/docs/network/overview/choosing-a-mode --- # Choosing a Mode Both modes run on the same Nym infrastructure but defend against different threat models. dVPN mode hides your IP and splits trust across two operators, and Mixnet mode additionally protects traffic patterns against adversaries capable of observing the entire network. **dVPN mode** routes through 2 hops (Entry Gateway + Exit Gateway) connected via [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/), a WireGuard fork with traffic obfuscation to evade protocol-level detection. Latency is low, but there is no protection against timing analysis. **Mixnet mode** routes through 5 hops (Entry Gateway, three Mix Node layers, Exit Gateway). Each Mix Node adds a random delay and mixes packets with those of other users. Combined with continuous cover traffic, this makes timing correlation impractical even for an adversary watching the entire network. ## Quick comparison | | dVPN Mode | Mixnet Mode | |---|---|---| | **Hops** | 2 (Entry + Exit Gateway) | 5 (Entry + 3 Mix Nodes + Exit) | | **Timing protection** | No | Yes (random delays per hop) | | **Cover traffic** | No | Yes (constant dummy packets) | | **Protects against** | ISPs, websites, advertisers, passive observers | Global passive adversaries, timing correlation, traffic analysis | | **Access** | [NymVPN](https://nymvpn.com) | NymVPN and [Nym SDKs](/developers) | ## Use dVPN mode when - Latency matters: browsing, streaming, downloads, video calls - Your concern is ISPs, advertisers, and websites tracking you, not nation-state surveillance - You want decentralised trust and payment privacy without the overhead of mixing ## Use Mixnet mode when - Metadata exposure is dangerous: journalism, activism, whistleblowing, legal work - Your adversary might be watching traffic across multiple network points - Added latency is an acceptable trade for unlinkability and unobservability ## For developers The [Nym SDKs](/developers) only expose **Mixnet mode**. dVPN mode is specific to the NymVPN application. There are two integration models: **Proxy** (traffic exits to the internet, analogous to Tor's exit relay model): ``` Your App --> Entry --> Mix Nodes --> Exit --> Internet ``` **End-to-end** (Sphinx-encrypted the entire way, traffic stays within the Mixnet): ``` Your App --> Entry --> Mix Nodes --> Exit --> Nym Client ``` See the [developer overview](/developers) for guidance on choosing between them. --- title: Nym vs VPNs, Tor, I2P, and E2EE description: How the Nym Network compares to traditional VPNs, Tor, I2P, and end-to-end encryption in terms of privacy guarantees, metadata protection, and threat models. url: https://nym.com/docs/network/overview/comparisons --- # Nym vs Other Systems There are several existing approaches to network privacy, each with different assumptions about who the adversary is and what they can do. ## Nym vs VPNs A traditional VPN creates an encrypted tunnel between your device and a VPN server, hiding your IP from destination websites and encrypting traffic from local observers like your ISP. The fundamental limitation is that the VPN provider itself sees all your traffic (every site you visit, when you visit it, how long you stay) and can log this voluntarily or be compelled to by legal process, with your payment information linking your account directly to your activity. Nym's dVPN mode splits this trust across two independent operators so that the Entry Gateway knows your IP but not your destination, the Exit Gateway knows your destination but not your IP, and neither can build a complete picture. Payment is handled through [zk-nyms](/network/cryptography/zk-nym), making subscriptions unlinkable to activity. Nym's mixnet mode goes further by adding timing obfuscation and cover traffic, which no traditional VPN offers; see [Mixnet Mode](/network/mixnet-mode) for details. ## Nym vs Tor [Tor](https://www.torproject.org/) is the best-known anonymous overlay network, routing traffic through three relays using Onion encryption so that no single relay sees both source and destination. It was designed in an era when global passive adversaries were considered unrealistic, and its [architecture](https://2019.www.torproject.org/about/overview.html.en) reflects that: packets flow through without delays and there is no cover traffic, which means an adversary watching both ends of a circuit can try and correlate timing to deanonymise users. Nym's mixnet addresses this by adding random delays at each Mix Node to break timing correlations, cover traffic so observers can't tell when real communication is occurring, per-packet routing rather than Tor's per-session circuits (so there's no long-lived path to observe), and a blockchain-based topology instead of Tor's centralised directory authority. The trade-off is latency: Tor is faster because it doesn't add mixing delays, so it may be a better fit for general browsing where timing protection isn't needed. Nym's mixnet is designed for threat models where the adversary can perform traffic analysis. ## Nym vs I2P [I2P](https://geti2p.net/) replaces Tor's centralised directory authority with a [distributed hash table](https://geti2p.net/en/docs/how/network-database), which improves decentralisation but introduces its own attack surface: DHT-based routing is vulnerable to eclipse attacks and Sybil attacks on the routing table. Like Tor, I2P provides no timing protection, so packets flow without delays or cover traffic. Nym uses a blockchain-based topology registry rather than a DHT, which avoids the known attack vectors around DHT-based routing (e.g. eclipse attacks, Sybil attacks on the routing table). The mixing and cover traffic on top of that address the timing analysis gap that I2P shares with Tor. ## Nym vs end-to-end encryption End-to-end encryption systems like [Signal](https://signal.org/docs/) encrypt messages on your device so that only the recipient can decrypt them, and the server never sees the content. But E2EE does nothing for metadata: the server still sees who you communicate with, when, how often, and how much, which on its own is enough to map relationships and infer sensitive activity. Nym and E2EE are complementary: E2EE protects message content, Nym protects the metadata around it (who, when, how much). Using Signal over the Nym mixnet, for instance, would protect both message content and the communication metadata around it. For a practical breakdown of when to use dVPN vs Mixnet mode, see [Choosing a Mode](/network/overview/choosing-a-mode). ## Further reading - [What is WireGuard?](https://nym.com/blog/what-is-wireguard-vpn) - [VPN Tunnels Explained](https://nym.com/blog/vpn-tunnels) - [Tor Project: How Tor Works](https://2019.www.torproject.org/about/overview.html.en) - [Tor Protocol Specification](https://spec.torproject.org/tor-spec/) - [I2P: How It Works](https://geti2p.net/en/docs/how/tech-intro) --- title: dVPN Mode description: How Nym's decentralised VPN mode routes traffic through two independent gateways, splitting trust so no single operator sees both your identity and destination. url: https://nym.com/docs/network/dvpn-mode --- # dVPN Mode dVPN mode is a 2-hop decentralised VPN available through [NymVPN](https://nymvpn.com). Traffic is routed through two independent gateways rather than a single VPN provider's server, so no single operator ever sees both who you are and what you're doing. ## How it works ``` User --> Entry Gateway --> Exit Gateway --> Internet ``` Your device wraps each packet in two layers of encryption, one per gateway. The Entry Gateway strips the outer layer and forwards a packet it cannot read; the Exit Gateway strips the inner layer and sends the plaintext request to the destination. Responses follow the reverse path. The Entry Gateway therefore knows your IP address but not the destination, while the Exit Gateway knows the destination but not the sender. ## Privacy guarantees dVPN mode hides your IP from destination servers and splits trust across two operators. It does not add timing obfuscation or cover traffic. Packets are forwarded immediately, so an adversary watching both gateways could still correlate timing to link your requests. If you need protection against traffic analysis, see [Mixnet Mode](/network/mixnet-mode). ## Performance Added latency is comparable to traditional VPNs, and WireGuard keeps cryptographic overhead low, so browsing, streaming, and downloads are not noticeably affected. ## Technical details - [dVPN Protocol](/network/dvpn-mode/protocol): protocol stack and encryption details - [Censorship Resistance](/network/dvpn-mode/censorship-resistance): AmneziaWG and DPI evasion ## Further reading - [Introducing AmneziaWG for NymVPN](https://nym.com/blog/introducing-amneziawg-for-nymvpn): censorship resistance - [What Is a Double VPN?](https://nym.com/blog/double-vpn): multi-hop privacy explained - [Building a Decentralized WireGuard VPN](https://nym.com/blog/building-decentralized-wireguard-vpn): architecture decisions - [What is NymVPN?](https://nym.com/blog/what-is-nymvpn): general overview --- title: dVPN Protocol Stack and Encryption description: Technical details of Nym dVPN mode's protocol layers: nested WireGuard tunnels, split-knowledge architecture, and packet format trade-offs. url: https://nym.com/docs/network/dvpn-mode/protocol --- # dVPN Protocol This page covers the technical details of dVPN mode's protocol stack and encryption. ## Protocol layers dVPN mode uses two nested WireGuard tunnels. The client establishes an inner tunnel to the Exit Gateway and an outer tunnel to the Entry Gateway, where the inner tunnel is created first and the outer tunnel encapsulates it. ``` +-----------------------------------------+ | Application Data | +-----------------------------------------+ | Inner WireGuard tunnel (Client → Exit) | +-----------------------------------------+ | Outer WireGuard tunnel (Client → Entry)| +-----------------------------------------+ | UDP/IP | +-----------------------------------------+ ``` The Entry Gateway decrypts only the outer tunnel and forwards the inner tunnel, still fully encrypted, to the Exit Gateway. The Exit Gateway decrypts the inner tunnel and forwards traffic to its destination. Because the Entry Gateway never holds keys for the inner tunnel, it is cryptographically excluded from the payload. ## Encryption Both tunnels use standard WireGuard cryptography: Curve25519 for key exchange, ChaCha20-Poly1305 for authenticated encryption, and BLAKE2s for hashing. Each tunnel derives independent session keys, providing 256-bit security with modern, well-audited primitives. ## Packet format dVPN mode uses standard WireGuard packet framing: packets are not padded to a uniform size. Packet sizes may vary and could in principle leak information about content types (video streams have different size patterns than text messages). This is a deliberate trade-off: uniform padding would add overhead and reduce throughput, which conflicts with dVPN mode's goal of low-latency, high-throughput connectivity. For uniform packet sizes, use [mixnet mode](/network/mixnet-mode), which wraps all traffic in fixed-size Sphinx packets. ## Connection lifecycle When connecting, the client first selects Entry and Exit Gateways based on latency, location preference, or random selection. It then presents a zk-nym credential to the Entry Gateway for anonymous authentication. The credential proves payment without revealing identity; it is re-randomized for each connection and cannot be linked to previous usage. Once authenticated, the client establishes both WireGuard tunnels: first the inner tunnel keyed with the Exit Gateway, then the outer tunnel keyed with the Entry Gateway. Traffic flows through both hops until the session ends. ## Security properties The protocol provides forward secrecy: new session keys are derived for each connection, so compromising long-term keys does not expose past sessions. WireGuard's key rotation provides additional forward secrecy within sessions. The nested-tunnel architecture enforces split knowledge. The Entry Gateway knows your IP but cannot decrypt the inner tunnel, so it sees neither your destinations nor your payload. The Exit Gateway decrypts the inner tunnel and sees your destinations but never learns your IP. Neither gateway can correlate the two. Replay protection comes from WireGuard's counter-based mechanism and from zk-nym serial numbers that prevent credential reuse. ## Relationship to mixnet mode dVPN mode shares infrastructure with mixnet mode. Both use the same Entry and Exit Gateways and the same credential system. The difference is in how traffic is handled: mixnet mode routes through three additional Mix Node layers with delays and cover traffic using fixed-size [Sphinx packets](/network/cryptography/sphinx), while dVPN mode routes directly between gateways using WireGuard. The two modes are distinguishable at the protocol level due to their different packet formats and traffic patterns. In anonymous (5-hop) mode, NymVPN routes traffic through the full mixnet to the Exit Gateway's [IP Packet Router](/network/infrastructure/exit-services#ip-packet-router), which tunnels raw IP packets to the internet. See [Exit Gateway Services](/network/infrastructure/exit-services) for how the IPR and Network Requester work. This shared infrastructure means improvements to Gateways and credentials benefit both modes. --- title: Censorship Resistance in dVPN Mode description: How AmneziaWG obfuscation, QUIC transport mode, and Stealth API Connect help Nym dVPN users evade deep packet inspection and protocol blocking. url: https://nym.com/docs/network/dvpn-mode/censorship-resistance --- # Censorship Resistance dVPN mode incorporates several techniques to help users connect in restrictive network environments where VPN protocols are actively detected and blocked. ## The problem: protocol fingerprinting Deep Packet Inspection (DPI) systems deployed by ISPs and governments can identify VPN protocols by their handshake patterns, packet sizes, and timing characteristics. Standard WireGuard, for instance, has a recognisable handshake initiation pattern that DPI rules can match against. Once identified, connections can be throttled or blocked entirely. This is not a theoretical concern: countries including China, Russia, Iran, and others actively deploy DPI to restrict VPN usage. ## AmneziaWG dVPN mode uses [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/), a fork of WireGuard that adds obfuscation techniques to make the protocol harder to fingerprint. AmneziaWG modifies the WireGuard handshake by introducing decoy packets before the handshake initiation. These decoy packets disrupt DPI rules that rely on matching the standard WireGuard handshake sequence. The actual WireGuard protocol behaviour is preserved; the modifications sit around the handshake rather than replacing it, so all of WireGuard's security properties (Curve25519 key exchange, ChaCha20-Poly1305 encryption, forward secrecy) remain intact. ## Limitations AmneziaWG raises the bar for censors relying on simple protocol fingerprinting, but it doesn't help against deeper analysis: statistical fingerprinting of packet timing and sizes, IP-based blocking of known Gateway addresses, or active probing where the censor sends packets to suspected VPN servers to confirm their identity. ## QUIC transport mode QUIC transport mode wraps the WireGuard/AmneziaWG connection inside a [QUIC](https://datatracker.ietf.org/doc/html/rfc9000) layer, so the traffic looks like standard HTTPS/HTTP3 to DPI systems rather than a VPN tunnel. Since QUIC is now used by a significant portion of regular web traffic (over 30% of Cloudflare's traffic in 2023 was HTTP/3 over QUIC), blocking it outright would break large parts of the web for everyone, making it an unattractive target for censors. QUIC transport applies to the Entry Gateway connection only (the first hop). Not all Gateways support it yet; enabling QUIC in the NymVPN app will filter the Gateway list to those that do. Because the QUIC wrapper adds overhead, it can reduce speeds slightly, so it's worth leaving disabled unless you're in a censored environment or having connectivity issues. ## Stealth API Connect Even if a user can establish a VPN tunnel, censors can also block access to the API that the NymVPN app needs to discover Gateways and fetch network state in the first place. Stealth API Connect addresses this by routing the app's API requests through a mechanism that is harder to identify and block, so the app can bootstrap its connection to the Nym network even in environments where the Nym API endpoints are actively censored. ## Limitations These techniques are layered: AmneziaWG obfuscates the handshake, QUIC disguises the tunnel as regular web traffic, and Stealth API Connect protects the initial API discovery. Together they cover several common censorship methods, but none of them are guarantees. Censorship resistance is an ongoing arms race, and new techniques will be documented here as they ship. ## Further reading - [Introducing AmneziaWG for NymVPN](https://nym.com/blog/introducing-amneziawg-for-nymvpn) - [AmneziaWG documentation](https://docs.amnezia.org/documentation/amnezia-wg/) - [What is QUIC? Censorship-Resistant Internet Connections](https://nym.com/blog/what-is-quic) - [What is QUIC transport mode in NymVPN?](https://support.nym.com/hc/en-us/articles/39648047741457-What-is-QUIC-transport-mode-in-NymVPN) - [What is Stealth API Connect in NymVPN?](https://support.nym.com/hc/en-us/articles/39652289741329-What-is-Stealth-API-connect-in-NymVPN) - [NymVPN's roadmap for censorship resistance](https://nym.com/blog/NymVPN-Roadmap-for-censorship-resistance-2025) --- title: Mixnet Mode description: How Nym's Mixnet mode works: 5-hop routing through Mix Nodes with random delays, packet reordering, and cover traffic for unlinkability and unobservability. url: https://nym.com/docs/network/mixnet-mode --- # Mixnet Mode Mixnet mode routes traffic through 5 hops: an Entry Gateway, three layers of Mix Nodes, and an Exit Gateway. Each mixing layer adds random delays, reorders packets, and injects cover traffic. Available through [NymVPN](https://nymvpn.com) and the [Nym SDKs](/developers). ## How it works ``` User --> Entry --> Mix L1 --> Mix L2 --> Mix L3 --> Exit --> Internet | | | delay delay delay ``` Each Mix Node strips one layer of [Sphinx](/network/cryptography/sphinx) encryption to learn the next hop, holds the packet for a random delay, then forwards it. No node ever sees both the origin and the final destination. The client also continuously sends [cover traffic](/network/mixnet-mode/cover-traffic) - dummy packets cryptographically indistinguishable from real ones - so an observer sees a constant stream of identical packets regardless of whether any real communication is taking place. ## Privacy properties - **Unlinkability**: the random delays and reordering at each Mix Node destroy the timing signal an observer would need to correlate incoming and outgoing packets, or to connect successive packets from the same user. See [Packet Mixing](/network/mixnet-mode/mixing). - **Unobservability**: because cover traffic is constant, an observer cannot determine when a user is active or what fraction of the traffic is real. See [Cover Traffic](/network/mixnet-mode/cover-traffic). - **Resistance to traffic analysis**: uniform Sphinx packet sizes prevent content-type fingerprinting, and per-packet routing eliminates the long-lived circuits that make other anonymity networks susceptible to end-to-end correlation. See [Traffic Flow](/network/mixnet-mode/traffic-flow). ## Performance The three mixing layers add additional latency. This is acceptable for messaging, file transfers, and most API calls, but unsuitable for real-time applications like video calling. For those, [dVPN mode](/network/dvpn-mode) is more appropriate. ## Further reading The following pages cover mixnet internals in detail: - [Loopix Design](/network/mixnet-mode/loopix) explains the academic foundation of Nym's Mixnet design - [Traffic Flow](/network/mixnet-mode/traffic-flow) shows the packet journey with diagrams - [Cover Traffic](/network/mixnet-mode/cover-traffic) explains how dummy packets provide unobservability - [Packet Mixing](/network/mixnet-mode/mixing) covers timing delays and their importance - [Anonymous Replies](/network/mixnet-mode/anonymous-replies) describes SURBs for bidirectional communication --- title: Loopix Design description: The academic Loopix mixnet design behind Nym: stratified topology, continuous-time mixing with exponential delays, and cover traffic loops for unlinkability and unobservability. url: https://nym.com/docs/network/mixnet-mode/loopix --- # Loopix Design The Nym mixnet is based on the [Loopix](https://arxiv.org/pdf/1703.00536) design, with modifications for decentralised operation and economic incentives. ## The insight Traditional mixnets focus on hiding "who messages whom," but this alone is insufficient, as adversaries observing message volume and timing over time can still infer private information. If you always message the same friend at the same time, patterns emerge. If you go silent when traveling, that's information too. Loopix was designed to provide both **unlinkability** (hiding who talks to whom) and **unobservability** (hiding when and how much communication occurs). The name comes from its use of "loop" cover traffic that circulates through the network. ## Stratified topology The network uses a layered architecture. Traffic flows through Entry Gateways, three Mix Node layers, and Exit Gateways. Each node connects only to adjacent layers. Path selection is independent per-message, unlike Tor's per-session circuits. This structure prevents observations about which paths are used together and limits the damage any single compromised node can cause. ## Continuous-time mixing Unlike batch mixnets that collect messages and release them periodically, Loopix uses continuous-time mixing, where each message is delayed independently according to an exponential distribution and then forwarded as soon as its delay expires. This approach offers optimal anonymity for a given mean latency. The exponential distribution has a key property: if two messages arrive at different times, they have equal probability of leaving in either order. An adversary watching input and output timing gains no information about which input became which output. Continuous mixing also means lower latency overall since messages don't wait for batches to fill. ## Cover traffic loops Connected clients and nodes continuously generate dummy packets that travel in loops through the network back to the sender. These packets are indistinguishable from real traffic: same size, same encryption, same timing distribution. Loop traffic ensures minimum anonymity even when few users are active, hides when real communication starts and stops, and enables detection of active attacks (if loop packets fail to return, a network fault or active interference is likely). ## Nym's modifications The Nym implementation extends Loopix in several ways: replacing the trusted directory server with the Nyx blockchain for decentralised topology management, incentivising node operation with NYM token rewards rather than relying on volunteers, and adding zk-nyms for privacy-preserving payment, which the original academic design did not address. ## Security guarantees The combination of continuous-time mixing and cover traffic provides provable guarantees. The anonymity set (the set of users who could have sent a given message) grows unboundedly over time. Even messages with short delays have large anonymity sets because of the exponential distribution. An adversary observing the entire network cannot determine who is communicating with whom, cannot tell when real communication is occurring, and gains no advantage from statistical analysis because the traffic patterns are designed to be indistinguishable from random. For the full formal analysis, see the [Loopix paper](https://arxiv.org/pdf/1703.00536) and the [Nym Whitepaper](https://nym.com/nym-whitepaper.pdf). --- title: Traffic Flow url: https://nym.com/docs/network/mixnet-mode/traffic-flow --- # Traffic Flow This page walks through how packets travel through the mixnet, from sending client to destination. This describes the 5-hop mixnet flow. For the 2-hop dVPN mode, see [dVPN Protocol](/network/dvpn-mode/protocol). ## Overview The Nym mixnet uses source routing: the sender chooses the complete route before sending. This means the sender constructs a Sphinx packet with layered encryption, where each layer contains routing information for one hop. ## Client to Entry Gateway On connection, the Nym client registers with a particular Entry Gateway. This Gateway becomes part of the client's Nym address and is where incoming messages are delivered. The client continuously sends packets to the Entry Gateway over a WebSocket connection. This stream includes both real messages and cover traffic at a constant rate. When the application has data to send, the client encrypts it as Sphinx packets and slots them into the stream. When there is no data, cover packets flow instead. ```mermaid sequenceDiagram box Local Machine participant App as Application participant Client as Nym Client end participant Gateway as Entry Gateway Gateway->>Client: Key Exchange loop Continuous Traffic Client->>Gateway: Cover packet Client->>Gateway: Cover packet App-->>Client: Data to send Client->>Client: Encrypt as Sphinx Client->>Gateway: Real packet Client->>Gateway: Cover packet end ``` ## Through the Mix Nodes The Entry Gateway forwards packets into the three Mix Node layers. At each hop, the node decrypts its layer of the Sphinx packet to learn the next destination, verifies the HMAC to ensure integrity, applies a random delay, and forwards to the next hop. The delay is critical. Without it, timing would correlate inputs to outputs. With exponential random delays, packets are reordered and the timing relationship is destroyed. ```mermaid --- config: theme: neo-dark --- sequenceDiagram participant Entry as Entry Gateway participant M1 as Mix Layer 1 participant M2 as Mix Layer 2 participant M3 as Mix Layer 3 participant Exit as Exit Gateway Entry->>M1: Sphinx Packet M1->>M1: Decrypt layer M1->>M1: Verify HMAC M1->>M1: Random delay M1->>M2: Sphinx Packet M2->>M2: Decrypt layer M2->>M2: Verify HMAC M2->>M2: Random delay M2->>M3: Sphinx Packet M3->>M3: Decrypt layer M3->>M3: Verify HMAC M3->>M3: Random delay M3->>Exit: Sphinx Packet ``` ## Exit Gateway to Destination The Exit Gateway handles the final hop. For traffic destined for external services, it decrypts the packet and forwards to the destination, then packages responses back into Sphinx packets for the return journey. For traffic destined for another Nym client, the Exit Gateway delivers to that client's registered Gateway, which holds the message until the recipient comes online. ## The complete picture Putting it together, a packet travels through five hops with encryption removed and delays applied at each Mix Node layer: ```mermaid --- config: theme: neo-dark --- sequenceDiagram box Sender participant App1 as Application participant C1 as Nym Client end box Mixnet participant Entry as Entry GW participant M1 as Mix L1 participant M2 as Mix L2 participant M3 as Mix L3 participant Exit as Exit GW end box Receiver participant C2 as Nym Client participant App2 as Application end App1->>C1: Send data C1->>C1: Create Sphinx packet C1->>Entry: Encrypted packet Entry->>M1: Forward M1->>M1: Decrypt, delay M1->>M2: Forward M2->>M2: Decrypt, delay M2->>M3: Forward M3->>M3: Decrypt, delay M3->>Exit: Forward Exit->>C2: Deliver C2->>C2: Decrypt final layer C2->>App2: Received data ``` ## External services When sending to an external service rather than another Nym client, the Exit Gateway acts as a proxy. It extracts the destination from the decrypted packet, makes the request on your behalf, and routes responses back through the network. The destination service sees the Exit Gateway's IP, not yours. ## Peer-to-peer For applications where all parties run Nym clients, traffic stays entirely within the mixnet. Both sides enjoy full privacy protection, and [SURBs](/network/mixnet-mode/anonymous-replies) enable anonymous bidirectional communication without either party learning the other's address. --- title: Cover Traffic description: How constant dummy packet streams hide real communication patterns in the Nym mixnet, achieving unobservability even against global network observers. url: https://nym.com/docs/network/mixnet-mode/cover-traffic --- # Cover Traffic Cover traffic consists of dummy packets that hide when real communication is occurring, providing unobservability: an adversary cannot determine whether a user is actively communicating. ## The problem Even with perfect encryption and mixing, traffic analysis can reveal information. An adversary can see how much data you're sending, when you're sending it, and detect patterns over time. Regular silence followed by bursts of activity reveals your schedule, and consistent traffic volumes to certain destinations reveal ongoing relationships. ## The solution Cover traffic maintains a constant rate of packet transmission. When you have real data to send, it replaces a cover packet in the stream. When you have nothing to send, cover packets flow anyway. To an observer, the traffic looks identical either way. ``` Without cover traffic: | ||| | Time ---------+---------+++---------+------> Idle Activity Idle (visible) With cover traffic: |||||||||||||||||||||||||||||||||||||| Time --------------------------------------> Constant rate (activity hidden) ``` The cover packets are real Sphinx packets with valid encryption, just with empty payloads. They travel through the network exactly like real packets, get mixed at each hop, and are discarded at their destination. No node along the way can tell whether a packet contains real data or is cover traffic. ## Loop traffic Cover packets follow complete routes through the network back to the sender. These "loops" serve multiple purposes: they provide traffic for mixing with others' cover traffic and they can detect active attacks. If loop packets stop returning, a network fault or active interference is likely. Mix nodes also generate their own cover traffic, ensuring minimum traffic levels even when few users are active. This provides baseline anonymity guarantees regardless of network load. ## How it's generated Traffic follows a Poisson process with a configurable rate parameter. Inter-packet times are exponentially distributed: random, but with a known average rate. This distribution provides maximum entropy (uncertainty) for a given mean rate, which translates to optimal privacy properties. ## Trade-offs More cover traffic provides better unobservability but uses more bandwidth and, when zk-nyms are enabled, more credential value. Less cover traffic reduces costs but may allow some inference about activity patterns. The default parameters balance privacy and resource usage. Applications with heightened privacy requirements can increase the cover traffic rate; applications where unobservability is less critical can reduce it. ## What cover traffic defeats Cover traffic prevents volume analysis (how much you communicate), timing analysis (when you communicate), and behavioural profiling (your communication patterns over time). Combined with packet mixing, this means that even an adversary watching the entire network cannot learn about your communication behaviour with currently known methods. --- title: Packet Mixing and Random Delays description: How Mix Nodes use exponential random delays to reorder packets and break timing correlations, preventing traffic analysis by network observers. url: https://nym.com/docs/network/mixnet-mode/mixing --- # Packet Mixing Packet mixing breaks timing correlations by adding random delays at each Mix Node. It's the core mechanism that prevents traffic analysis. ## The problem Without mixing, an observer watching a node could correlate inputs and outputs. If packets leave on a FIFO (First In First Out) basis, even with encryption hiding contents, the timing relationship reveals which input became which output. ## The solution Each Mix Node adds a random delay before forwarding. Packets don't flow through in order; they're held for variable times and released in a different sequence than they arrived. An observer sees packets going in and packets coming out, but cannot match them. ``` Input sequence: A B C D E | | | | | v v v v v [ Mixing ] | | | | | v v v v v Output sequence: C A E B D ``` The delays follow an exponential distribution. This choice is mathematically optimal: if two packets arrive at times t₀ and t₁, they have equal probability of leaving in either order, regardless of when they arrived. The adversary gains no information from timing observations. ## Why exponential delays The exponential distribution is memoryless: the probability of a packet leaving in the next moment does not depend on how long it has already waited, so an adversary cannot narrow down possibilities by noting how long packets have been in the node. Any other delay distribution leaks information; fixed delays would let adversaries match arrivals to departures by timing, and uniform distributions would create windows where matches become more likely. ## Continuous vs batch mixing Older mixnet designs collected packets into batches and shuffled them before release. This has problems: latency is unpredictable since you wait for batches to fill, bandwidth is inefficient due to bursty traffic, and the anonymity set is limited to the batch size. Continuous-time mixing processes each packet independently. Latency is predictable (the mean delay is configurable), bandwidth is used efficiently, and the anonymity set is unbounded: it includes all packets that have ever passed through, weighted by time. ## The aggregate effect With three Mix Node layers, each applying random delays, the overall effect is thorough reordering. Packets entering the mixnet in sequence exit in a completely different order. The timing relationship between sending and receiving is destroyed. These delays account for the additional latency of mixnet mode relative to dVPN mode. ## Combined with cover traffic Mixing and cover traffic are complementary. Cover traffic ensures there are always packets to mix, even during low activity, while mixing ensures that real and cover packets become interleaved and indistinguishable. Together they provide both unlinkability and unobservability. --- title: Anonymous Replies with SURBs description: How Single Use Reply Blocks (SURBs) enable anonymous bidirectional communication in the Nym mixnet without revealing the sender's address. url: https://nym.com/docs/network/mixnet-mode/anonymous-replies --- # Anonymous Replies SURBs (Single Use Reply Blocks) enable anonymous bidirectional communication. A receiver can reply to a sender without learning the sender's identity or address. ## The problem In a typical mixnet scenario, Alice sends a message to Bob and wants a reply, but if Bob sends directly to Alice's Nym address, he learns it. This defeats the purpose of anonymous communication; Bob now knows Alice's identity for future contact, and due to how Nym's [addressing scheme](/network/reference/addressing.md) works, this means that Bob knows which Gateway node Alice's client is using. ## How SURBs work Alice creates SURBs (encrypted routing headers) and includes them with her message to Bob. Each SURB contains a complete route back to Alice, encrypted so that Bob cannot read it. Bob attaches his reply to a SURB and sends the resulting packet into the mixnet. It travels through the encoded route and arrives at Alice, but Bob never learns where it went. A SURB contains the address of the first hop (Alice's Entry Gateway), encrypted routing headers for the path back to Alice, and a key to encrypt the reply payload. The routing headers are layered like a Sphinx packet; each hop can only see the next destination. ## Single use Each SURB can only be used once. This prevents replay attacks and ensures forward secrecy. For conversations requiring multiple exchanges, Alice sends multiple SURBs with her initial message. SURB validity is tied to key rotation. Node keys rotate on an odd/even schedule with a default validity of 24 epochs (roughly 25 hours at the current 1-hour epoch length). After that window, the routing keys a SURB was built with are no longer accepted. Clients automatically purge stale SURBs and request fresh ones. Reply keys also expire after 24 hours independently of rotation cycles. ## SURB replenishment If Bob's reply is larger than the available SURBs can carry, he uses one SURB to request more. Alice receives the request, generates additional SURBs, and sends them to Bob. This adds round-trip latency but lets conversations continue regardless of reply size. ```mermaid --- config: theme: neo-dark --- sequenceDiagram participant Alice participant Mixnet participant Bob Alice->>Mixnet: Message + 5 SURBs Mixnet->>Bob: Message + 5 SURBs Bob->>Bob: Reply needs 10 SURBs Bob->>Mixnet: "Need more SURBs" (uses 1 SURB) Mixnet->>Alice: SURB request Alice->>Mixnet: 10 more SURBs Mixnet->>Bob: Additional SURBs Bob->>Mixnet: Reply (uses SURBs) Mixnet->>Alice: Reply received ``` ## Sender tags For sessions with multiple messages, Alice includes a randomly generated sender tag with her SURBs. This helps Bob organise SURBs from multiple conversations without revealing anything about Alice's identity; the tag is random and unlinkable to her address. ## Security considerations There's a known attack where a malicious receiver hoards SURBs and sends them all back simultaneously, attempting to correlate traffic patterns at the sender's Gateway. This attack requires active participation (not just passive observation), and provides limited information even if successful. It's not a passive surveillance technique; the attacker must be specifically targeting you and willing to spend resources. ## Comparison to Tor onion addresses Tor's onion addresses allow indefinite replies but require the recipient to run a hidden service. SURBs are single-use but require no service; they're generated on-demand per message. SURBs also benefit from the mixnet's timing protection, which onion addresses don't have. --- title: Nym Network Cryptography description: Overview of the cryptographic systems powering Nym: transport encryption, Sphinx packet format, per-hop encryption, and zk-nym anonymous credentials. url: https://nym.com/docs/network/cryptography --- # Cryptography The Nym Network relies on several cryptographic systems working together. This section covers the algorithms, packet formats, and credential systems that provide privacy guarantees. ## What's covered [Sphinx Packets](/network/cryptography/sphinx) explains the packet format used for layered encryption and anonymous routing. Each Sphinx packet contains routing information encrypted in layers, where each hop can only decrypt its own layer. [zk-nyms](/network/cryptography/zk-nym) covers the anonymous credential system that separates payment from usage. This is how you can pay for network access without that payment being linkable to your activity. --- title: Sphinx Packet Format description: How Sphinx packets provide layered encryption for anonymous mixnet routing, with fixed-size payloads, per-hop key derivation, and integrity verification via HMACs. url: https://nym.com/docs/network/cryptography/sphinx --- # Sphinx Sphinx is the cryptographic packet format used for all mixnet traffic. It provides layered encryption where each hop can only decrypt its own routing information, so no single node knows both the source and destination of a packet. ## How Sphinx works When a client sends a message through the mixnet, it constructs a Sphinx packet with multiple encryption layers, one for each hop in the route. The outermost layer is encrypted for the first hop (Entry Gateway), the next layer for the second hop (Mix Node Layer 1), and so on until the innermost layer contains the actual payload encrypted for the recipient. At each hop, the node uses its private key to decrypt its layer, revealing the address of the next hop and a new Sphinx packet to forward. The node cannot see any other routing information or the payload contents. ## Packet structure All Sphinx packets have a fixed payload size of 2048 bytes. This uniformity is critical: if packets varied in size, nodes could infer their position in the route or correlate packets by size. The packet contains a header with encrypted routing information for each hop, HMACs to verify integrity at each layer, and the encrypted payload. The header uses an "onion" structure where processing at each hop reveals only the next hop's information while maintaining constant size through padding. ## Integrity verification Each layer includes an HMAC (Hash-based Message Authentication Code) that the receiving node verifies before processing, which prevents malicious nodes from modifying packet contents en route. If the HMAC doesn't match, the packet is dropped. The payload uses Lioness wide-block encryption, which means any modification to any part of the payload invalidates the entire payload. This prevents bit-flipping attacks where an adversary might try to modify specific bytes. ## Key derivation For each hop, the client performs an ECDH key exchange using the node's public key and an ephemeral key embedded in the packet header. This shared secret is then used with HKDF to derive the symmetric keys for that layer's encryption and HMAC. The ephemeral key is "blinded" at each hop so that successive nodes cannot correlate packets by the key value. Each node sees a different ephemeral key even though they're mathematically related. ## Message fragmentation Messages larger than a single Sphinx payload are split into fragments. Each fragment travels independently through the network, potentially taking different routes. The recipient reassembles the fragments into the original message. ## External implementation Nym uses the [`sphinx-packet`](https://github.com/nymtech/sphinx) crate for core Sphinx operations. This crate handles packet construction, header encryption, layer processing, and the mathematical operations for key blinding. ## References - [Sphinx paper](https://cypherpunks.ca/~iang/pubs/Sphinx_Oakland09.pdf): Original specification and security proofs - [Elle Mouton's Sphinx explainer](https://ellemouton.com/posts/sphinx/): Detailed walkthrough of packet construction - [Nym Whitepaper §4](https://nym.com/nym-whitepaper.pdf): Sphinx in the context of Nym --- title: What are zk-nyms? url: https://nym.com/docs/network/cryptography/zk-nym --- # What are zk-nyms? The zk-nym scheme enables the creation and use of unlinkable, rerandomisable anonymous access credentials that are 'spent' with Gateways in order to anonymously prove that someone has paid for Mixnet access. This implementation incorporates elements of both the [Coconut Credential](https://arxiv.org/pdf/1802.07344) and [Offline Ecash](https://arxiv.org/pdf/2303.08221) schemes. As outlined in the [overview](./zk-nym/zk-nym-overview) on the next page, zk-nyms allow users to pay for Mixnet access in a way that is **unlinkable to their payment account**, even with pseudonymous cryptocurrencies or fiat. This solves one of the fundamental privacy problems with most VPNs and dVPNs in production today: the linkability of a user's session with their payment information, which can in most cases be used to deanonymise them, either at the behest of an authority or by the service operators themselves. > The current zk-nym scheme is non-generic in that it is only used for gating Mixnet access. A generic scheme based on zk-nyms is being actively researched, to support more generic and customisable anonymous credentials for other applications and services. ## Motivations Most of the time, when we build system security, we think of _who_ questions: - Has Alice identified herself (authentication)? - Is Alice allowed to take a specific action (authorisation)? However, _who_ is not necessarily a question we want to be asking when designing a system with anonymous access control. This scheme allows us to instead consider questions of _rights_, namely: - Does the entity taking this action have a right to do _X_? This allows a different kind of security. Most networked services do not need to know _who_ is making a request, only whether the requester has the _right to use_ the system. The zk-nym scheme allows for this move to take place. Credentials are generated cooperatively by decentralised, trustless systems, and once the credentials are generated, they can be _re-randomized_; entirely new credentials, which no one has ever seen before, can be presented to the ingress point of the Nym Network, and validated without being linkable back to the signatures produced by the Quorum of credential signers used to generate them, or any credentials previously used by an entity wanting access. These properties allow zk-nyms to act as something like cryptographic bearer tokens generated by decentralised systems. The tokens can be mutated so that they are not traceable, but still verified with the original permissions intact. > TL;DR: Users present cryptographic claims encoded inside the credentials to get secure access to resources despite the systems verifying credential usage not being able to know who they are. ### Re-randomisation vs pseudonymity We stand on the shoulders of giants. Ten years ago, Bitcoin showed the way forward by allowing people to control resource access without recourse to _who_ questions. Rather, in Bitcoin and succeeding blockchains, a private key proves a _right to use_. But as we can now see, private keys in blockchain systems act only as a minor barrier to finding out _who_ is accessing resources. A Bitcoin or Ethereum private key is a long-lived pseudonym that is easily traceable through successive transactions. **zk-nyms allows us to build truly private systems rather than pseudonymous ones.** ## Features Specifically, zk-nym is an implementation of a blinded, re-randomizable, selective disclosure threshold credential signature scheme. Let's say you have a `message` with the content `This credential controls X` in hand. In addition to the normal `sign(message, secretKey)` and `verify(message, publicKey)` functions present in other signature schemes like RSA, the zk-nym credential scheme adds the following: 1. _[Blind signatures](https://en.wikipedia.org/wiki/Blind_signature)_ - disguises message content so that the signer can't see what they're signing. This defends users against signers: the entity that signed can't identify the user who created a given credential, since they've never seen the message they're signing before it's been _blinded_ (turned into seemingly random binary data). The scheme uses zero-knowledge proofs so that the signer can sign confidently without seeing the unblinded content of the message. 2. _Re-randomizable signatures_ - take a signature, and generate a brand new signature that is valid for the same underlying message `This credential controls X`. The new bitstring in the re-randomized signature is equivalent to the original signature but not linkable to it. So a user can generate multiple zk-nyms from a single credential source, unlinkable to any previous "shown" zk-nym. But the underlying content of the re-randomized credential is the same (including for things like double-spend protection). This once again protects the user against the signer, because the signer can't trace the signed message that they gave back to the user when it is presented. It also protects the user against the relying party that accepts the signed credential. The user can generate multiple re-randomized credentials repeatedly, and although the underlying message is the same in all cases, there's no way of tracking them by watching the user present the same credential multiple times. 3. _Selective disclosure of attributes_ - allows someone with the public key to verify some, but not all, parts of a message. So you could for instance selectively reveal parts of a signed message to some people, but not to others. This property of the scheme is to be explored more in future work, with potential applications including voting systems, anonymous currency, and privacy-friendly KYC systems. 4. _[Threshold issuance](https://en.wikipedia.org/wiki/Threshold_cryptosystem)_ - allows signature generation to be split up across multiple nodes and decentralised, so that either all signers need to sign (_n of n_ where _n_ is the number of signers) or only a threshold number of signers need to sign a message (_t of n_ where _t_ is the threshold value). Taken together, these properties provide privacy for applications when generating and using signatures for cryptographic claims. The closest analogy in conventional systems is a decentralised, privacy-preserving [JWT](https://jwt.io/). --- title: Generating and using zk-nym anonymous credentials url: https://nym.com/docs/network/cryptography/zk-nym/zk-nym-overview --- # Generating and using zk-nym anonymous credentials zk-nyms are already used in production by [NymVPN](https://nymvpn.com) to unlink subscription payments from network activity. The entire credential lifecycle described on this page (key generation, issuance, spending) happens inside the NymVPN application without user involvement. SDK integrations currently connect to the Mixnet without requiring credentials. Generation of zk-nyms involves the following actors / pieces of infrastructure: - **Requester needing a zk-nym** for example a single user using the NymVPN app, or a company purchasing zk-nyms to distribute to their app users, in the instance of an app integrating a Mixnet client via one of the SDKs. The Requester is represented by a Bech32 address on the Nyx blockchain. - [NymAPI](/network/infrastructure/nyx#nym-api) instances working together on signature generation and spent credential validation, referred to as the **NymAPI Quorum**. Members of the Quorum are a subset of the Nyx chain Validator set (other tasks they perform include a multisig used for triggering reward payouts to the Network Infrastructure Node Operators and maintaining the global Bloom Filter for double-spend protection). - **OrderAPI**: an API creating crypto/fiat to `NYM` swaps and then depositing the NYM tokens in a smart contract managed by the NymAPI Quorum for payment verification. Implementation details of the API will be released in the coming months. Generation happens in 3 distinct stages: - Key Generation & payment - Issue credential - Generate unlinkable zk-nyms for Nym Network access From the Requester's perspective this happens without user involvement, producing an unlinkable, rerandomisable anonymous proof-of-payment credential (a zk-nym) that grants Mixnet access without linking usage to payment information. A single credential can be split into multiple smaller zk-nyms, so a Requester purchases bandwidth in bulk and spends it incrementally across different ingress Gateways as needed. ## Key Generation & Payment - First, a Cosmos [Bech32 address](https://docs.cosmos.network/sdk/latest/guides/reference/bech32#performance-address-caching) is created for the Requester. This is used to identify themselves when interacting with the OrderAPI via signed authentication tokens. **This is the only identity the OrderAPI sees, and it cannot link this to the zk-nyms that will be generated.** This identity never leaves the Requester's device and there is no email or personal details needed for signup. If a Requester is 'topping up' their subscription, the creation of the address is skipped as it already exists. - The Requester also generates an ed25519 keypair: this is used to identify and authenticate them in the case of using zk-nyms across several devices as an individual user. However, **this is never used in the clear**: these keys are used as private attribute values within generated credentials which are verified via zero-knowledge and not publicly exposed. - The Requester can then interact with payment backends to pay for their zk-nyms with crypto, fiat options, or natively with NYM tokens. - Payment options will trigger the OrderAPI. This will: - Create a swap for `` to `NYM` tokens. - Deposit these tokens with the NymAPI Quorum via a CosmWasm smart contract deployed on the Nyx blockchain. - The Requester sends a request to each member of the Quorum requesting a zk-nym credential. This request is signed with their private key and includes the transaction hash of the NYM deposit into the deposit contract, performed either by themselves or the OrderAPI. ## Issue zk-nym At this point, NYM tokens have been deposited into the smart contract controlled by the Quorum's multisig and a zk-nym has been requested. Next, each member of the Quorum who responds to the Requester's request for a zk-nym checks the validity and returns a partial blinded signature - a 'partial signed credential' ('PSC') - signed with part of the master key (since this is a threshold cryptosystem, not all members of the Quorum must respond to create a zk-nym, only enough to pass the threshold). The process looks like this: - Members of the Quorum perform several checks to verify the request is valid: - They verify the signature sent as part of the request is valid and that the request was made in the last 48 hours. - They verify that the amount requested matches the amount deposited in the transaction, the hash of which was signed by the Requester's ed25519 key and sent as part of the request. - Members then create a PSC from their fragment of the master key generated and split amongst them at the beginning of the Quorum in the initial DKG ceremony. - The member also creates a `key:value` entry in their local cache with the transaction hash as the key, and the PSC + encrypted signature as the value. This is used later for zk-nym validation and is cleaned after a predefined timeout. - These PSCs are given back to the Requester after setting up a secure channel via DH key exchange, with each replying Quorum member also sending their public key for verification that the returned PSC was signed by them. Once the Requester has received over the threshold number of PSCs they can assemble them into a 'ticketbook' of 'tickets' - spendable credentials - signed by the master key. The Requester never learns this master key (it is a private attribute) but the credential can be verified by the Quorum as being valid by checking for a proof that the credential's private attribute - the value of the master key - is valid. ![](/images/network/deposit-generate.png) ## Spend zk-nym to Access Mixnet - Once the ticketbook has been aggregated from the PSCs returned from > threshold of Quorum members, smaller 'ticket' credentials can be generated from it, accounting for smaller chunks of bandwidth which can be 'spent' with ingress Gateways. This occurs entirely offline, on the device of the zk-nym Requester. See pages on the scheme's [unlinkability](unlinkability) and [rerandomisation and incremental spending](./rerandomise) features for further information on this. - This ticket is later presented to the Quorum by the Gateway that collected it, which is used to calculate reward percentages given to Nym Network infrastructure operators by the Quorum, with payouts triggered by their multisig wallet. Both ingress Gateways and the Quorum use spent tickets when engaging in [double spending protection](./double-spend-prot). ![](/images/network/use-zknym.png) --- title: Rerandomisation & Incremental Spend url: https://nym.com/docs/network/cryptography/zk-nym/rerandomise --- # Rerandomisation & Incremental Spend Each ticket will not be valid for the entire amount of data that the ticketbook aggregated from the PSCs is; if the aggregated ticketbook is worth (e.g.) 10GB of Mixnet data, each ticket will be worth far less (e.g. 100MB). This amount will be globally uniform in order to avoid situations where differently sized tickets allow for patterns to emerge. The `nym-cli` examples below are for illustration only and do not reflect how credentials are accessed in production. The specific figures (ticket counts, bandwidth amounts) are illustrative; production values may differ, though individual ticket sizes are uniform across the network. ## Why a 'ticketbook', not individual 'tickets', and why not spend them all at once? This is to account for the need for a client to change their ingress Gateway, either because the Gateway itself has gone down or is not offering the required bandwidth, or because a user might want to split their traffic across multiple Gateways for extra privacy. Clients are therefore not tied to a particular Gateway they have spent their entire subscription with. If an ingress Gateway goes down, or the client wants to use a different one, remaining tickets can be spent with any other Gateway. Going back to the `nym-cli` tool to illustrate this; we can generate multiple unlinkable tickets from a single ticketbook aggregated from PSCs: ```sh ❯ ./nym-cli ecash generate-ticket --credential-storage storage.db --provider 6qidVK21zpHD298jdDa1RRpbRozP29ENVyqcSbm6hQrG --full TICKETBOOK DATA: 4Ys9pzUf9MPxX4s5RASyrRoY9fPk1a1kFuPBP2jm2L5PyUy535yPEfjHAfpUTC1Lf2d155TmjukvcDycQYfBSDfhEUJM4J3qPNfG3B5aQEEkefESZp3CM5AEnAu1AEyhpepbYw6BuXokiNcmaYtq3yJQbA4KicKP8FowoRzKHmXpJoUqY8wYQughGfdtXgr3rVaZmK21X51P1NL2UW1aCE512WWfy6P1LJHByWywT3qVw28Z83 generating payment information for 50 tickets. this might take a while!... AVAILABLE TICKETS +-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------+ | index | binary data | spend status | +============================================================================================================================================================================================+ | 0 | 4kgKyJLq1zQuk9r9AbEFHPqD8mDuxsLSjgo9XW4Lf7EqGSbgfNsWSEcTbRPEMFLzpstbX5azsA3opFh851h4g5qCG2qE3Luwqua4GG2ebJhk91rvEc5JPctbVQxL62fkfQ6svdcNp…1057bytes remaining | NOT SPENT | |-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------| | 1 | 4kefQqViRZd5YezMHH1FTcgUGPK2E2ivfmwgf59exvsnR8tsb5aJtGVwpA7wAJT6icPeo8jtDwDZ3WMPJxL3VRLiakAQr79zh7ixM89gowg3ChHEy6ewmHcT7T6RFkZFsMCMj1CNd…1057bytes remaining | NOT SPENT | |-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------| | 2 | 4kxaKdBxyFzJ8gxSZCh1v3wBfN7JvnCJuoJ4MWqkkMHtt2XgRKbDmHCv5ZxtA57Qk8LC3NDMBmqjADvY34mAPdT3tLBL4uxse9ASa227Ji96dwgxvfbpvLXSSr5o4vuPRV9K7UfpJ…1057bytes remaining | NOT SPENT | |-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------| | 3 | 4kdYwUJwXyxZBLQXextd4GsU2MATjzArVq5Ec459fTXyrm6q3vxurWULzBMpV5UjcmjJtnw1zFqt7f8Ydu5gyxwAVXP3Nwpn83ouguv2n4YrUewZCvFAqQYXgahhhaQGp6RxK2Arh…1057bytes remaining | NOT SPENT | |-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------| ... | 46 | 4kg8bfQ7kGgq5TkkqXagpAEu95gmGT4i7NKbaxJtp2gRgWRrQZM1rxaDAzAxfghoM6PFNbYgKsnLD4MF8HtXW3p92CnPBjswzJ1EbtsMGpgDER3CYFt2ivAhMAVXFziF5UjVJXhpa…1057bytes remaining | NOT SPENT | |-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------| | 47 | 4kipbH5Fqt5E9hFMynm9vzFh5FkxKRdHrSEiiJWDwmg3mASctR61sXoFD5u5ZMBwGdvz9sWsRfrpR4MX2NNfRhC85aUxqtkAv3hXZiCLtE1pUC54Cq7YXHyv2XTNKpvuFZs2GmwYg…1057bytes remaining | NOT SPENT | |-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------| | 48 | 4kxYZ26HXvxVhh4quHXeCUyQokydeF5wkwUi8fMx6P3uoMvuiPaNP1SJTbYnaQEFFtF6U4dGop6QckUYvbtwQFoGJTJesHFHTDtHbshj5Dg8DwbyaHuAR86zGwYMUPved4XKUTMLa…1057bytes remaining | NOT SPENT | |-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------| | 49 | 4kb6zmPebRxjKLVicctq2whvANjWJMoohiPBMr21cT4xj78nvXmJEK8EB4PpqQVFo6ddU9uzuer5ggQZNZgETX2VXBzymBYNzXBuXjLJi1WRdAiASqWz5Hv5im1TJh4XBE4mxKo8Q…1057bytes remaining | NOT SPENT | +-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------+ ``` --- title: Unlinkability url: https://nym.com/docs/network/cryptography/zk-nym/unlinkability --- # Unlinkability Each time a credential is requested by an ingress Gateway to prove that a client has purchased data to send through the Mixnet, the Requester's device produces a ticket. This is a rerandomised value that can be verified as legitimate (in that it was created by a valid root ticketbook) but is **not linked to any other tickets**, either previously generated or to be generated in the future. This also allows a single ticketbook to be split across multiple ingress Gateways or connections and [incrementally spent](./rerandomise) over time. The functionality included in the following code block examples were added to the [nym-cli tool](/developers/tools/nym-cli) for illustrative purposes only: this is not necessarily how credentials will be accessed in the future. The numbers used in this high level overview are for illustration only. The figures used in production may vary. Individual zkNym sizes are uniform across the Network. ```sh ❯ ./nym-cli ecash generate-ticket --credential-storage storage.db --provider 6qidVK21zpHD298jdDa1RRpbRozP29ENVyqcSbm6hQrG --ticket-index=3 TICKETBOOK DATA: 4Ys9pzUf9MPxX4s5RASyrRoY9fPk1a1kFuPBP2jm2L5PyUy535yPEfjHAfpUTC1Lf2d155TmjukvcDycQYfBSDfhEUJM4J3qPNfG3B5aQEEkefESZp3CM5AEnAu1AEyhpepbYw6BuXokiNcmaYtq3yJQbA4KicKP8FowoRzKHmXpJoUqY8wYQughGfdtXgr3rVaZmK21X51P1NL2UW1aCE512WWfy6P1LJHByWywT3qVw28Z83 attempting to generate payment for ticket 3... PAYMENT FOR TICKET 3: VfZAuVRRHekQYMvFevNAZmPPuwMAfEhTBY8TXatBysbrNXAg8euEGPpJvdbhNfQSznBb9nRSeBUSVoNTToSA6Uj5dXmJ7oE2rCB439DarLMWHWYfQNhw6yhWJhcg6bt7ebBYTs3vVeQgSB5kYuifzJF4QQmK6uJyTNPvpV1J6V8M32PBkGT3JpVB3GUGZiksETf7TaF9wAhMo2QAMxw5ZvaQVve5ea7Mane6cfb2Gx69SRff5zDfEQvKqKnyyZje4SGZgWUeHWVLhRjg4KMTJ3JcsHxEqj2k5qeGeyBbgzcuEtCpYvaytsz7nuZGJsT4Z87gB5Zq4NGuDmekuN977eRJvua2dASNWeHiAzVyvnS7ARN5cdUjjYKYiWgHaYrHGsv26WTDeiu4U3sdJMrLHGFY5ihX7f8sTZqD6Wx5AWjQNbEtKaVHymDogfLcwGCC42gQ2yhKfPUaWJ8H4yMB65YBDXGjATaUzcDmJcZKx8g31j2uTVNSFUesd5CRNEEcTNW7cSFFCishCD3T4eV9SuyZyEXAZ48pazPzc1BysBNHEXQNUEtEAZTKmpghC2pihhfDub6LnMJPo9DDdhCULCbcWbGAPc1vPekPaWvk7wrUTGwp5xoNUhQLW3MeJzMvrMSsqLdursCKB4h4Tk272WCStCPQwAKMYoxjWvMzxoUTTWCkhLKHruMtsehRnai4vhu13jbui6ji1F389gfazm4ctth2s4Yw3H3SaPtRETBfZNvZ7n5UV1MD6Q3qin92gT65iqXEi4zRN3woYcK6ZehiSvgUksdEFAUSxNMgNXKtHEYDS6kA37tn5JdBa2Ex2jLudFfhg6JBM226ZKyj65o6feYPgbJAR3jMCmQRHe6DSFb4aH895EowNMjfGUhwhmnbYB1djp7iFXxPP7575NAerhxEQ1WFnxTfoX7pu1Vc9YZb5priCAVbATCaDkECJsdedM45Vx96Jc6E5NWqD98RhMsPimVJkSfYJmRxH9qugica6WonFFb2YLvXYyhoBA1VHBcRqZJ5KHitS5AegYSoYprUfubMzcYo2hGVEQkGKAsFq6jZgCsbJoGLXt3No317vcowB5f3hqT9FjASHAzW2j8uJ9RRzX7XtrPhArwx4EyPgYzrvgG7xcenoSgQt8poa7aYky56eZTKHVUZgUEt6St32MjcivMvmNdWiAHHDc2ZxzTJHgeuCckX7n19vQ3XNLuXv9oGKNNCi8kHnT4tUnnGXNAWXWuyBgZKWUL8u3y41iW6dLYK3Pw5zfpKZTrq3q3bTLJRN5LnnUuFVnWsC3SNqa6VAAvhTGR9PzxLk8C6HeLP2AsYPpqeQwbaL3Ks6tvPdob3tQPWRBGL4uiKtNZ23tRYZGZLYFWZK7psRSZg5AETejKxztVzAuYovpVUiDq71o331tjqWWV1SzWT13Rd1uwz6nHtsjgao2863YaizKARcYr1j9MKtNfDs483yho6i7tbCRR9M4CPLqdiKEaRyVC1FP4F3sejA6nZTuAA35JWUzX6BBj7wgdypMLdMmmtcCZm3bRrF3GvJJs67U8JWRc6dnoGUDaD7rUu ``` Generating another ticket, to spend either when topping up after the previous one's data allowance has been used or with another Gateway. The `ticket-index` is the same: this is generated from the same aggregated credential as the one above. ```sh ❯ ./nym-cli ecash generate-ticket --credential-storage storage.db --provider 6qidVK21zpHD298jdDa1RRpbRozP29ENVyqcSbm6hQrG --ticket-index=3 TICKETBOOK DATA: 4Ys9pzUf9MPxX4s5RASyrRoY9fPk1a1kFuPBP2jm2L5PyUy535yPEfjHAfpUTC1Lf2d155TmjukvcDycQYfBSDfhEUJM4J3qPNfG3B5aQEEkefESZp3CM5AEnAu1AEyhpepbYw6BuXokiNcmaYtq3yJQbA4KicKP8FowoRzKHmXpJoUqY8wYQughGfdtXgr3rVaZmK21X51P1NL2UW1aCE512WWfy6P1LJHByWywT3qVw28Z83 attempting to generate payment for ticket 3... PAYMENT FOR TICKET 3: Vev3SmwWtH5vbnejX5Zzc1EcxXAgveqHpKNN8arxXaWLhFcEpdcZ6n7qr3NrQUNURWsK2AsUiX8aSiGSjMPEY3iDE3aDYnjYERVow8RKUmQiYSKvz7v9cEJxt97JAHBfu9WYNHXTnLFSJwWuFtBdzY5dzPdzGckFenGCysa1ZBHGADHChDVXKoPHXxpn5qyJxmi48coUQDptR64QgkCeQ8RRZ396Lxw2NKFSjqavCMMDVm3g1rW7cYyPanBhkoAUzPU9KXX1rtmhD6F9gV89mGZ8fm7ByDuKuYU28seLQ7GkVKkhNeRW9XxbjSiyscTnMUzJ24R5VbSdr141BaquUHezdUTzmA2EjAtcyyiVrCMV13cc96CRbMXENP2soUzckFnh1qPnrfKCvX4JYkztq7UgPT2mZEnSTDW4C6Z2NVCNBPNLqUSYrU4id8Jzcp1mBxqJjdYcQ7P5fWJbT5Q9NAq44PCgfXpsUkNoj35QVQvKXKLb5oNGqnua5YC1WBPcENcpS7ZPWpk2hwe8VK4gNgnwQtWH2RPmWbvBREAV97vS1vKNHJyry9sD2PiMJGSmBnb1bKsGxR9UQN3YvRsdGHzyJHzAMTzxbFJBqMPmxjSHJR4UdwzhB81Ludu1RAffTvecWFxmWH5bNymCQjw3wey7Uequcxgyy8KAWYDzvHGwCZQbHQXghsYREiqquZWaa8hX3iTNBFUtEk8PRVT78MoFNdeBWNjsLr8zyZ5EGnf4kqmw3a91g5p5vywf6e3LgMu19VHjPSNtKMNXiatkPEVjsCuCppmV4sB7FsdKKWcMUSWLsdmrDBg9PStHr7NaJRzLL5E91gvysmB36Nob9cHeHSZj3wM4NVVjFfZeRqQf4bi7ahfXjeeBetgDpqx7JcbU6tTN4JpcGUpp7fp4MhTq7MeVQMLweGUVLqewKgAGzCvEmrK6dzLd3U1P9vkAAVZ3cCAKUywnHGxoxDeEfexP1g1EqJLtKNZVKPf7hSMWqGhoQ36K7y5GnyZ5YhQ7jcDME9orm5w4StoxoDdCPcjbakKG7UaTHuhd7tU1mUffXcEvVerkXoQK9SEaKvGks21RBhW86aHUzJWVbkiDzdaqjJWbmzLV8FKvNxNyzucoH2rq8LiHRMZfV1H3SkVSa4j2Ktw7ZGoQfdj8DgekxXSR2nHPfhybzKYXTBqFo2ACisxkjR4rXr9Xo6eYywQhQ1MP6aYgYCAXFGHPoFf7kx7Jns5sWvHRBdaMF65zeFF2m5NDuMWETtLgFfsyNgR84vfSqTfzj2gsUykRei7q9N4LKmiDwBALTAEcTvZpLtXBjc8JaB9PUeBw7DoSiSK376sGrQ9F6ZGTngXACNz1TbvYhtau4bDa6KC2Qn7wmoyrphpn7TtM1jdwGBxLcaEEWZKQHvWVfTyL2itjqnrcAZkxYdCj56oQYwpWfKQk3zJEUA6SYHqyJjaLNVK6u25j7969EWjdpTsJ8qSsZgXi3T7dQqiwintZbUUUKRq7egN1SGVnA6Wup91uKrYUWEWMqVu4g8ipmRsLD9iXHHr3yA21Cka7pqk1FxR9BFTAnkk1 ``` These are both generated by the same underlying ticketbook and used in a way that they cannot be tied to each other. An ingress Gateway might (for instance) get 100 connection requests from 100 Nym clients, each validated with a ticket. It has no way of knowing whether these are all from the same single subscription, or 100 different ones. --- title: Double Spend Protection url: https://nym.com/docs/network/cryptography/zk-nym/double-spend-prot --- # Double Spend Protection Double spend protection in the context of zk-nym is a balancing act between speed, reliability, and UX. There are two possible modes for protecting against attempted double spending of zk-nyms: - Online: The online approach mandates that ingress Gateways instantly deposit zk-nyms received from clients to the NymAPI Quorum for verification. Once verified by the Quorum, the ingress Gateway is paid proportional to the amount of bandwidth 'spent' with the zk-nym, and proceeds to grant the client access to the network. - Offline: In contrast, the offline approach involves the periodic submission of collected zk-nyms by ingress Gateways to the Quorum, instead of an instant check. Subsequently, the Quorum nodes perform checks to detect any instances of double-spending and identify the public key associated with such occurrences, whereas the ingress Gateways only do a simple check to check that _that particular_ zk-nym had not been spent with itself before. > The zk-nym system takes the **offline** approach. ## Offline Approach: Pros & Cons The advantages of the offline approach are manifold: - Immediate access to the Nym network upon zk-nym submission, eliminating any delays in service provisioning until payments are deposited and verified as would occur in the online approach. - Reduces load on ingress Gateways and Quorum members compared to the online approach. Moving the compute-heavy work to the Quorum means Gateway nodes can run on less capable machines, so more operators can run them (and cover their costs), increasing the overall number and spread of Gateways around the globe. - Moreover, the offline approach can circumvent the potential issue of overwhelming the blockchain with the serial numbers of spent coins. However, the offline approach introduces certain limitations. - Ingress Gateways accept zk-nyms without preemptively checking for instances of double spending thus making them susceptible to unknowingly accepting double-spent credentials. - Any potential repercussions against double spenders can only be implemented once the user requests a new credential for their zk-nym Generator (aka they have to 'top up' and buy more bandwidth allowance), assuming they haven't altered their identifier (the Bech32 address). An exploitable scenario arises from these limitations: - A malicious user purchases bandwidth and aggregates a valid zk-nym credential in the standard way, worth $10 of crypto/fiat. The malicious user then sells the credential to 100 users for $1 each, allowing each user to generate zk-nym tickets of 100MB from this **valid** credential. Under the offline approach, entry nodes skip double-spending checks; so long as the clients all used different ingress Gateways, all 100 users could access the network without obtaining a subscription. As bandwidth consumption is tracked locally between client and ingress node, and each zk-nym ticket is rerandomised, there is no way that ingress Gateways would know that the zk-credential used by the client has been shared with other parties. This loophole calls for measures to counter such abuses without creating either speed bottlenecks (as in the Online model) or harming the anonymity of the system. We can mitigate this problem without doing either. ## Solution to Offline Double Spending To prevent fraudulent use of tickets within the Nym network, a two-tiered solution combines (1) immediate detection of double-spending attempts at individual ingress Gateways and (2) subsequent identification and blacklisting of offending clients at the Quorum level. ### Entry Node Implementation: Real-Time Ticket Validation Each spent zk-nym ticket contains as an attribute a unique serial number, which is revealed in plaintext to the respective ingress Gateway. Each Gateway has a copy of a [Bloom Filter](https://www.geeksforgeeks.org/bloom-filters-introduction-and-python-implementation/) - on receiving a ticket, it will check against its copy of a local database to check whether this serial number has already been seen. If so, it rejects the ticket as being double-spent and the client's connection request is rejected. If not, it will add the serial number to its local DB. > Since each time a zk-nym credential is rerandomised its serial number is changed, the serial number being shared in no way identifies a client or user. Each Gateway will periodically share their serial numbers with the Quorum and refresh their copy of the Bloom Filters from the Quorum, in order to refresh the global list shared by all ingress Gateways and the Quorum. See the step below for more on this. > Crucially, ingress Gateways do not perform extensive computations to identify the original ticket owner, and do not broadcast information about the double-spending attempt to other ingress Gateways. The entry node is also not involved in any global blacklisting of clients. The sole purpose of this check is to quickly identify double-spending attempts and add the seen ticket's serial number to the local DB cache. ### Nym-API Implementation: Blacklisting and Penalties for Double-Spenders All Gateways periodically forward the collected tickets to the Quorum, enabling them to pinpoint and blacklist any clients who double spend. Upon receiving the tickets, the Quorum appends all the incoming serial numbers to the global list of spend zk-nym serial numbers and proceed with the identification process for any malicious users engaging in double-spending. This identification phase involves looking for instances of double spending, identifying the id of the double-spending client, and blacklisting this client by its id. Subsequently, when this client requests a new credential, their plaintext public identifier is included in the request. The Quorum then checks if this identifier is blacklisted. If it is, a new credential is not issued. Furthermore, since the PSCs are only attainable after depositing NYM as payment, the Quorum has the authority to withhold the deposited NYMs as a punitive measure for any detected instances of double-spending. --- title: Nym Network Infrastructure description: Overview of the Nym Network's decentralised infrastructure: independently operated nodes coordinated by the Nyx blockchain for routing, key management, and credential issuance. url: https://nym.com/docs/network/infrastructure --- # Infrastructure The Nym Network runs on decentralised infrastructure: a set of independently operated nodes coordinated by the Nyx blockchain, where no single party controls routing, key management, or credential issuance. ## In this section - [Nyx Blockchain](/network/infrastructure/nyx): the Cosmos SDK chain that maintains the node registry, manages token economics, and hosts the smart contracts for credentials and rewards - [Nym Nodes](/network/infrastructure/nym-nodes): the unified `nym-node` binary that operates as Entry Gateways, Mix Nodes, or Exit Gateways depending on network demand --- title: Nyx Blockchain description: How the Nyx Cosmos SDK blockchain coordinates the Nym Network by maintaining node topology, managing NYM token economics, and hosting smart contracts for credentials and rewards. url: https://nym.com/docs/network/infrastructure/nyx --- # Nyx Blockchain Nyx is a Cosmos SDK blockchain that coordinates the Nym Network. It maintains the topology of active nodes, manages token economics, and hosts the smart contracts that power the credential system. To interact with the chain, see [Interacting with Nyx](/developers/chain). To run a Validator, see the [Operator Documentation](/operators/nodes/validator-setup). ## Role in the network The blockchain serves several functions, including maintaining the **topology registry**: the list of active nodes and their public keys. This eliminates the need for a centralized directory server and prevents attacks that plague peer-to-peer directory systems. It manages **token economics**, where the NYM token is a native token of the chain, used for staking, rewards, and credential payments. Validators secure the chain via proof-of-stake consensus. And it hosts **smart contracts** for mixnet coordination and the zk-nym credential system. ## Validators Nyx Validators run the `nyxd` binary to maintain the blockchain. They process transactions, execute smart contracts, and participate in consensus. A subset of validators also run Nym API instances for credential issuance. ## Nym API For setup instructions, see the [Nym API Operator Guide](/operators/nodes/validator-setup/nym-api). The Nym API is operated by a subset of validators forming the "Quorum." This group performs network monitoring: sending test packets through the mixnet and calculating reliability scores for nodes. More critically, it handles credential issuance, generating the partial blind signatures that form zk-nyms. The Quorum uses threshold cryptography. No single member can issue credentials alone. The system remains functional even if some members are offline. This distributes trust across multiple independent parties. ## Smart contracts The Nyx chain is CosmWasm-enabled. The **Mixnet Contract** stores bonded node information, provides network topology for client routing, and tracks delegations and rewards. The **Vesting Contract** manages NYM token vesting schedules. The **zk-nym Contract** tracks deposits for credential generation and manages the blacklist for double-spend attempts. Contract addresses for different networks can be queried via the [Nym API](/apis/nym-api). ## Querying the chain The [Nym API](/apis/nym-api) provides HTTP endpoints for querying network topology, node status, rewards, and credential information. For direct contract interaction, see the [Developer Documentation](/developers/chain). --- title: Nym Nodes url: https://nym.com/docs/network/infrastructure/nym-nodes --- # Nym Nodes All traffic-routing infrastructure runs on the `nym-node` binary. This unified binary operates in different modes (Entry Gateway, Mix Node, or Exit Gateway), simplifying deployment and enabling future dynamic role assignment. To run a node, see the [Operator Documentation](/operators/introduction). ## Node modes **Entry Gateways** are the user's first point of contact with the network. They accept WebSocket connections from clients, verify zk-nym credentials to confirm payment, and store messages for clients that go offline. Entry Gateways know the client's IP address but cannot see message contents or final destinations. **Mix Nodes** form the three mixing layers that provide core privacy. They receive Sphinx packets, remove one encryption layer, verify integrity, apply a random delay, and forward to the next hop. Mix Nodes cannot determine their position in the route and cannot link incoming packets to outgoing packets. **Exit Gateways** handle traffic leaving the mixnet. They run two proxy services: the [Network Requester](/network/infrastructure/exit-services#network-requester) (a SOCKS proxy for application-layer requests) and the [IP Packet Router](/network/infrastructure/exit-services#ip-packet-router) (a raw IP tunnel used by NymVPN and smolmix). Exit Gateways can see destination addresses but cannot identify the original sender. See [Exit Gateway Services](/network/infrastructure/exit-services) for details. ## Unified binary These components were originally separate binaries but have been consolidated into a single `nym-node` binary where the role is specified at runtime. This simplifies operation and makes configuration consistent across roles. In the future, nodes will automatically switch modes based on network conditions. Operators won't need to manually set whether a node is a Gateway or Mix Node; the network will assign modes dynamically each epoch. ## Nym clients For client setup, see the [Developer Documentation](/developers/clients/socks5). Clients are the user-side software that connects to the network. They discover network topology from the blockchain, register with an Entry Gateway, construct Sphinx packets with layered encryption, generate cover traffic, and handle acknowledgements and retransmission. Client types include native Rust clients, WASM clients for browsers, the SOCKS5 proxy client, and the NymVPN client. The NymVPN client supports both dVPN and mixnet modes. ## Running infrastructure The current deployment includes {stats.nodes} active nodes across {stats.locations} countries, operated by independent parties worldwide. This includes {stats.mixnodes} Mix Nodes and {stats.exit_gateways} Exit Gateways. Running a node requires meeting minimum hardware specifications, bonding NYM tokens as collateral, and maintaining high uptime for rewards. --- title: Exit Gateway Services: Network Requester & IP Packet Router description: The two proxy services running on Nym Exit Gateways: the Network Requester (SOCKS proxy) and the IP Packet Router (raw IP tunneling). How they work, what they see, and who uses them. url: https://nym.com/docs/network/infrastructure/exit-services --- # Exit Gateway Services Exit Gateways are where traffic leaves the Nym network and reaches the wider internet. Each Exit Gateway runs two distinct proxy services that handle different kinds of outbound traffic: - **Network Requester (NR)**, an application-layer SOCKS proxy - **IP Packet Router (IPR)**, a raw IP tunnel with address allocation Both services run on every Exit Gateway. Which one handles your traffic depends on how you connect. ## Network Requester The Network Requester is a SOCKS4/4a/5 proxy. Clients send SOCKS-formatted requests through the mixnet, and the NR makes the corresponding connection on their behalf: resolving hostnames, opening TCP connections, and relaying data. ```text Client → Entry Gateway → Mixnodes1..3 → Exit Gateway (NR) → SOCKS connect → destination ← relay response ← ``` Because it operates at the application layer, the NR: - Resolves DNS on behalf of the client (the client sends hostnames, not IPs) - Opens individual TCP connections per SOCKS request - Can enforce allow/deny lists on destination hosts and ports - Sees the destination hostname and port, but not the contents if TLS is used **Used by:** the [SDK's SOCKS client](/developers/rust/mixnet), [standalone SOCKS5 client](/developers/clients/socks5), and [mixFetch](/developers/mix-fetch) (which wraps SOCKS requests in a browser-friendly `fetch` API). ## IP Packet Router The IP Packet Router operates at the IP layer. Instead of proxying individual connections, it allocates a virtual IP address to the client and routes raw IP packets between the client and the internet, functioning as a tunnel endpoint. ```text Client → Entry Gateway → Mixnodes1..3 → Exit Gateway (IPR) → raw IP packets → destination ← raw IP packets ← ``` On connection, the IPR: 1. Allocates an IPv4/IPv6 address pair to the client 2. Accepts raw IP packets (TCP, UDP, or any IP protocol) from the client via the mixnet 3. Sends them to the internet from the gateway's own IP address 4. Routes response packets back through the mixnet to the client Because it operates at the IP layer, the IPR: - Does not resolve DNS; the client handles its own DNS (either via clearnet or by sending DNS queries as UDP packets through the tunnel) - Handles any IP protocol: TCP, UDP, ICMP, etc. - Sees raw IP packets, including destination IPs and ports - Does not see contents if the client uses TLS or another encryption layer In both services, traffic between the Exit Gateway and the destination travels over the public internet, exactly as it would from any other server. The mixnet protects sender anonymity (the destination sees the gateway's IP, not yours), but does not encrypt the payload past the gateway. Use TLS or another application-layer cipher to protect payload confidentiality, just as you would on a direct connection. **Used by:** [NymVPN anonymous mode](/network/dvpn-mode/protocol) (5-hop mixnet routing to the IPR), and [`smolmix`](/developers/smolmix) (programmatic `TcpStream`/`UdpSocket` access to the IPR via the Rust SDK). ## Comparison | | Network Requester | IP Packet Router | |---|---|---| | **Layer** | Application (SOCKS) | IP (raw packets) | | **Protocols** | TCP only | TCP, UDP, any IP protocol | | **DNS** | Resolved by the NR | Client resolves its own | | **Client gets** | Proxied connections | An allocated IP address | | **Connection model** | Per-request | Persistent tunnel | | **Used by** | SDK SOCKS client, mixFetch | NymVPN (anonymous mode), smolmix | ## Trust model Both services share the same fundamental trust property: **the Exit Gateway can see destinations but not senders.** The mixnet's layered encryption ensures that the Exit Gateway cannot determine who sent a given packet; it only knows where it's going. Specifically, the Exit Gateway: - **Can see:** destination IP/hostname, destination port, unencrypted payload content, traffic volume and timing at the exit hop - **Cannot see:** the sender's IP address, the sender's Nym address, which Entry Gateway the traffic entered through - **Cannot determine:** the linkage between different requests from the same sender (unless the payload itself contains identifying information) The sender's identity is protected by the mixnet's 5-hop routing, Sphinx encryption, cover traffic, and packet mixing. The Exit Gateway is the last hop: it decrypts the final Sphinx layer and sees the destination, but the chain of Mix Nodes between Entry and Exit has destroyed any timing or ordering correlation. --- title: Nym Network Reference description: Technical specifications and protocol details for the Nym Network: addressing format, epoch timing, and the hop-by-hop acknowledgement system. url: https://nym.com/docs/network/reference --- # Reference Technical specifications and protocol details that apply across the Nym Network regardless of mode. ## In this section - [Addressing](/network/reference/addressing): the `identity.encryption@gateway` address format and how routing works - [Epochs](/network/reference/epochs): time divisions in the network, reward distribution, and topology reshuffling - [Acknowledgements](/network/reference/acks): the hop-by-hop packet delivery confirmation system --- title: Nym Network Addressing description: How Nym addresses work: the identity.encryption@gateway format, key components, routing mechanics, and privacy considerations for client addressing. url: https://nym.com/docs/network/reference/addressing --- # Addressing All clients and nodes in the Nym Network have an address that uniquely identifies them for routing. ## Address format A Nym address has three parts separated by dots and an @ symbol: ``` .@ ``` The **identity key** identifies the client for routing purposes and is derived from the client's Ed25519 keypair and base58-encoded for readability. The **encryption key** is the public key used to encrypt the final layer of Sphinx packets destined for this client. Only the client holding the corresponding private key can decrypt messages addressed to them. The **gateway key** identifies which Gateway holds messages for this client. When you connect, your client registers with a specific Entry Gateway, and that Gateway's identity becomes part of your address. ## Example ``` DguTcdkWWtDyUFLvQxRdcA8qZhardhE1ZXy1YCC7Zfmq.Dxreouj5RhQqMb3ZaAxgXFdGkmfbDKwk457FdeHGKmQQ@4kjgWmFU1tcGAZYRZR57yFuVAexjLbJ5M7jvo3X5Hkcf ``` ## How routing works When sending to a Nym address, the sender extracts the Gateway key and constructs a Sphinx packet with that Gateway as the final hop. The Gateway receives the packet, identifies the recipient by their identity key, and delivers the message (or stores it if the recipient is offline). ## Privacy considerations The address reveals which Gateway you use and your public keys. It doesn't reveal your IP address or private keys. Multiple clients can use the same Gateway, so the Gateway key alone doesn't identify you. For persistent identity across sessions, store your keypairs and re-register with the same Gateway. For ephemeral identity, generate new keys each session. --- title: Epochs in the Nym Network description: How epochs organise time in the Nym Network: reward distribution, topology reshuffling, SURB validity windows, and future automatic role assignment. url: https://nym.com/docs/network/reference/epochs --- # Epochs Time in the Nym Network is organised into epochs: discrete periods during which certain network operations occur. The current epoch length is one hour. ## What happens at epoch boundaries **Reward distribution** calculates performance metrics for each node and distributes NYM token rewards based on routing reliability and uptime, so that nodes successfully forwarding packets earn more than those with poor performance. **Topology rerandomization** shuffles the arrangement of nodes in each layer. This prevents long-term route prediction attacks and limits the damage from any compromised nodes. Nodes may also enter or leave the active set based on uptime monitoring and stake changes. ## Future changes In upcoming releases, epochs will trigger automatic role assignment. Nodes will switch between Mix Node and Gateway roles based on network demand, without operators needing to manually configure roles. ## SURB validity SURBs are tied to key rotation cycles. Node keys rotate on an odd/even schedule with a default validity of 24 epochs. A SURB remains usable for `(validity_epochs + 1) * epoch_duration`, roughly 25 hours at the current 1-hour epoch. After that, the routing keys it was built with are no longer accepted by the network. Clients automatically purge stale SURBs and request fresh ones. ## Querying epoch information Current epoch data is available through Nyx blockchain queries and Nym API endpoints. --- title: Packet Acknowledgements description: How the Nym Network uses hop-by-hop acknowledgements and retransmission to ensure reliable packet delivery despite network congestion or node failures. url: https://nym.com/docs/network/reference/acks --- # Acknowledgements The Nym Network uses acknowledgements to ensure reliable packet delivery. When a node receives a packet, it sends an ack back to the sender. If no ack arrives within a timeout, the packet is retransmitted. ## How it works The sender transmits a packet and waits for acknowledgement. The receiver processes the packet and sends an ack. If the sender receives the ack, the packet is marked as delivered. If not, the sender retransmits. This happens automatically at each hop. If a client sends 100 packets to a Gateway and only receives 95 acks, it retransmits the 5 missing packets. The same mechanism operates between all nodes in the route. ## Why it matters Network conditions can cause packet loss: congestion, temporary failures, connectivity issues. Without acks and retransmission, lost packets would mean lost messages. The acknowledgement system ensures reliable delivery despite imperfect network conditions. ## Scope Acknowledgements operate hop-by-hop between adjacent nodes. They confirm that packets reached the next hop, not that they reached the final destination. End-to-end delivery confirmation for anonymous communication is handled separately through [SURBs](/network/mixnet-mode/anonymous-replies). ## Implementation This is handled entirely by the Nym binaries. Developers and operators don't need to implement or configure acknowledgements; the system handles packet loss without any application involvement. --- title: Licensing url: https://nym.com/docs/network/licensing --- # Licensing As a general approach, licensing follows this pattern: * [Nym Documentation](https://nym.com/docs) by [Nym Technologies](https://nym.com) is licensed under [CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) ![CC](/images/cc-icons/cc.svg) ![BY](/images/cc-icons/by.svg) ![NC](/images/cc-icons/nc.svg) ![SA](/images/cc-icons/sa.svg) * Nym applications and binaries are [GPL-3.0-only](https://www.gnu.org/licenses/) * Used libraries and different components are [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.html) or [MIT](https://mit-license.org/) For accurate information, please check individual files. --- title: Code of Conduct url: https://nym.com/docs/network/coc --- # Code of Conduct We are committed to providing a friendly, safe and welcoming environment for all, regardless of level of experience, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other similar characteristic. Please avoid using overtly sexual aliases or other nicknames that might detract from a friendly, safe and welcoming environment for all. Please be kind and courteous. There’s no need to be mean or rude. Respect that people have differences of opinion and that every design or implementation choice carries a trade-off and numerous costs. There is seldom a right answer. Please keep unstructured critique to a minimum. If you have solid ideas you want to experiment with, make a fork and see how it works. We will exclude you from interaction if you insult, demean or harass anyone. That is not welcome behaviour. We interpret the term “harassment” as including the definition in the Citizen Code of Conduct; if you have any lack of clarity about what might be included in that concept, please read their definition. In particular, we don’t tolerate behaviour that excludes people in socially marginalized groups. Private harassment is also unacceptable. No matter who you are, if you feel you have been or are being harassed or made uncomfortable by a community member, please contact one of the channel ops or any of the Rust moderation team immediately. Whether you’re a regular contributor or a newcomer, we care about making this community a safe place for you and we’ve got your back. Likewise any spamming, trolling, flaming, baiting or other attention-stealing behaviour is not welcome. --- title: Overview description: Choose a Nym integration path by runtime and approach, then find the crate or package: nym-sdk, smolmix, mix-tunnel, mix-fetch, mix-dns, mix-websocket, and the TypeScript SDK. url: https://nym.com/docs/developers --- # Overview Every Nym integration sends its traffic through the mixnet via a Nym client. Which crate or package you use comes down to two questions: 1. **Runtime**: where does your code run? 2. **Approach**: do you control both sides of the connection (**end-to-end**), or are you reaching a third-party service through the mixnet (**proxy**)? The table below maps those two answers to a package. ## Choosing a package | Runtime | End-to-end (both sides run Nym) | Proxy (exit to clearnet) | |---|---|---| | **Native Rust** (desktop / CLI / server) | [`nym-sdk`](/developers/rust): Mixnet, Stream, Client Pool | [`smolmix`](/developers/smolmix): `TcpStream` / `UdpSocket` · [`nym-sdk` SOCKS](/developers/rust) | | **Browser / WebView** (JS + WASM) | [TypeScript SDK](/developers/typescript): `@nymproject/sdk` raw messaging | [`mix-fetch`](/developers/mix-fetch) HTTP/S · [`mix-dns`](/developers/mix-dns) DNS · [`mix-websocket`](/developers/mix-websocket) WS/WSS | **Mobile is a host, not a runtime.** The same phone can run either row. Compile the Rust SDK to a native library (`uniffi` plus [`cargo-swift`](https://github.com/antoniusnaumann/cargo-swift) for an iOS XCFramework, or [`cargo-ndk`](https://github.com/bbqsrc/cargo-ndk) for Android `jniLibs/`), or load the WASM packages inside a WebView (Capacitor, Cordova, Ionic, WKWebView, Android WebView). The SDK ships [FFI bindings](/developers/rust/ffi) for Go and C/C++ only; for Swift or Kotlin you generate your own from the [`sdk/ffi/shared`](https://github.com/nymtech/nym/tree/develop/sdk/ffi) uniffi crate. The WebView path needs no Nym-specific native code. On Android the native path has a [TLS bootstrap gotcha](/developers/rust/mixnet/troubleshooting#android-mixnet-bootstrap-fails-with-a-certificate-revoked--ocsp-error). ## End-to-end or proxy The runtime axis is about where your code runs: a native process has raw sockets and a filesystem, so it runs the full Rust client; a browser or WebView has neither (only WebSockets and `fetch`, under mixed-content rules), so it runs a WASM client inside a Web Worker. The approach axis is about who runs Nym at the other end. **End-to-end**: both sides run a Nym client. Traffic stays Sphinx-encrypted the whole way ([what this protects](/developers/concepts/exit-security#proxy-mode-or-end-to-end)). Use it for peer-to-peer setups or anywhere you control both endpoints. ![](/images/developers/nym-arch-client-to-client.png) **Proxy**: only your side runs Nym. Traffic exits the mixnet at an Exit Gateway and continues to the destination over the public internet. The mixnet anonymises the sender; protecting the payload (TLS, Noise) is your application's job, exactly as on a direct connection. Use it for third-party services such as blockchain RPCs or external APIs. ![](/images/developers/nym-arch-ip-routing.png) Past the Exit Gateway, traffic travels the public internet like any other connection. The mixnet anonymises the sender but does not encrypt the payload beyond the gateway. Use TLS or another application-layer cipher. See [Exit security](/developers/concepts/exit-security) for what the exit can and cannot observe. In a browser or WebView, your app talks to that WASM client through JS bindings rather than direct calls. The mixnet behaviour is identical in both modes, only the integration shape differs. See [mix-* architecture](/developers/mix-architecture) for the full picture. ![](/images/developers/nym-browser-arch.png) ## Packages ### Rust | Crate | Use it for | |---|---| | [`nym-sdk`](/developers/rust) | End-to-end mixnet messaging, `AsyncRead`/`AsyncWrite` byte streams, client pooling. Start with the [Tour](/developers/rust/tour). | | [`smolmix`](/developers/smolmix) | `TcpStream` and `UdpSocket` over the mixnet via a userspace IP stack. Compatible with `tokio-rustls`, `hyper`, `tokio-tungstenite`, and the rest of the async Rust ecosystem. | ### TypeScript The four mix-* packages share one tunnel ([`mix-tunnel`](/developers/mix-tunnel)) and one WASM instance; install only what you need. See [mix-* architecture](/developers/mix-architecture) for how they're wired. | Package | Use it for | |---|---| | [`mix-tunnel`](/developers/mix-tunnel) | The shared tunnel the three feature packages build on. Most apps don't import it directly. | | [`mix-fetch`](/developers/mix-fetch) | Drop-in `fetch()` for HTTP and HTTPS through the mixnet. | | [`mix-dns`](/developers/mix-dns) | Hostname-to-IP resolution through the mixnet. UDP DNS via the IPR. | | [`mix-websocket`](/developers/mix-websocket) | WebSocket-like class for WS and WSS through the mixnet. | | [TypeScript SDK](/developers/typescript) | `@nymproject/sdk`: end-to-end raw messaging when you control both ends. Smart contracts via `@nymproject/contract-clients`. | ### Standalone and other | Resource | Use it for | |---|---| | [SOCKS5 / WebSocket clients](/developers/clients) | Language-agnostic binaries for piping traffic through the mixnet without an SDK. | | [Chain interaction](/developers/chain) | Query Nyx state, submit transactions, call Nym smart contracts. | | [APIs](/apis/introduction) | Auto-generated reference for Nym infrastructure HTTP endpoints. | --- title: Overview description: Choose a Nym integration path by runtime and approach, then find the crate or package: nym-sdk, smolmix, mix-tunnel, mix-fetch, mix-dns, mix-websocket, and the TypeScript SDK. url: https://nym.com/docs/developers --- # Overview Every Nym integration sends its traffic through the mixnet via a Nym client. Which crate or package you use comes down to two questions: 1. **Runtime**: where does your code run? 2. **Approach**: do you control both sides of the connection (**end-to-end**), or are you reaching a third-party service through the mixnet (**proxy**)? The table below maps those two answers to a package. ## Choosing a package | Runtime | End-to-end (both sides run Nym) | Proxy (exit to clearnet) | |---|---|---| | **Native Rust** (desktop / CLI / server) | [`nym-sdk`](/developers/rust): Mixnet, Stream, Client Pool | [`smolmix`](/developers/smolmix): `TcpStream` / `UdpSocket` · [`nym-sdk` SOCKS](/developers/rust) | | **Browser / WebView** (JS + WASM) | [TypeScript SDK](/developers/typescript): `@nymproject/sdk` raw messaging | [`mix-fetch`](/developers/mix-fetch) HTTP/S · [`mix-dns`](/developers/mix-dns) DNS · [`mix-websocket`](/developers/mix-websocket) WS/WSS | **Mobile is a host, not a runtime.** The same phone can run either row. Compile the Rust SDK to a native library (`uniffi` plus [`cargo-swift`](https://github.com/antoniusnaumann/cargo-swift) for an iOS XCFramework, or [`cargo-ndk`](https://github.com/bbqsrc/cargo-ndk) for Android `jniLibs/`), or load the WASM packages inside a WebView (Capacitor, Cordova, Ionic, WKWebView, Android WebView). The SDK ships [FFI bindings](/developers/rust/ffi) for Go and C/C++ only; for Swift or Kotlin you generate your own from the [`sdk/ffi/shared`](https://github.com/nymtech/nym/tree/develop/sdk/ffi) uniffi crate. The WebView path needs no Nym-specific native code. On Android the native path has a [TLS bootstrap gotcha](/developers/rust/mixnet/troubleshooting#android-mixnet-bootstrap-fails-with-a-certificate-revoked--ocsp-error). ## End-to-end or proxy The runtime axis is about where your code runs: a native process has raw sockets and a filesystem, so it runs the full Rust client; a browser or WebView has neither (only WebSockets and `fetch`, under mixed-content rules), so it runs a WASM client inside a Web Worker. The approach axis is about who runs Nym at the other end. **End-to-end**: both sides run a Nym client. Traffic stays Sphinx-encrypted the whole way ([what this protects](/developers/concepts/exit-security#proxy-mode-or-end-to-end)). Use it for peer-to-peer setups or anywhere you control both endpoints. ![](/images/developers/nym-arch-client-to-client.png) **Proxy**: only your side runs Nym. Traffic exits the mixnet at an Exit Gateway and continues to the destination over the public internet. The mixnet anonymises the sender; protecting the payload (TLS, Noise) is your application's job, exactly as on a direct connection. Use it for third-party services such as blockchain RPCs or external APIs. ![](/images/developers/nym-arch-ip-routing.png) Past the Exit Gateway, traffic travels the public internet like any other connection. The mixnet anonymises the sender but does not encrypt the payload beyond the gateway. Use TLS or another application-layer cipher. See [Exit security](/developers/concepts/exit-security) for what the exit can and cannot observe. In a browser or WebView, your app talks to that WASM client through JS bindings rather than direct calls. The mixnet behaviour is identical in both modes, only the integration shape differs. See [mix-* architecture](/developers/mix-architecture) for the full picture. ![](/images/developers/nym-browser-arch.png) ## Packages ### Rust | Crate | Use it for | |---|---| | [`nym-sdk`](/developers/rust) | End-to-end mixnet messaging, `AsyncRead`/`AsyncWrite` byte streams, client pooling. Start with the [Tour](/developers/rust/tour). | | [`smolmix`](/developers/smolmix) | `TcpStream` and `UdpSocket` over the mixnet via a userspace IP stack. Compatible with `tokio-rustls`, `hyper`, `tokio-tungstenite`, and the rest of the async Rust ecosystem. | ### TypeScript The four mix-* packages share one tunnel ([`mix-tunnel`](/developers/mix-tunnel)) and one WASM instance; install only what you need. See [mix-* architecture](/developers/mix-architecture) for how they're wired. | Package | Use it for | |---|---| | [`mix-tunnel`](/developers/mix-tunnel) | The shared tunnel the three feature packages build on. Most apps don't import it directly. | | [`mix-fetch`](/developers/mix-fetch) | Drop-in `fetch()` for HTTP and HTTPS through the mixnet. | | [`mix-dns`](/developers/mix-dns) | Hostname-to-IP resolution through the mixnet. UDP DNS via the IPR. | | [`mix-websocket`](/developers/mix-websocket) | WebSocket-like class for WS and WSS through the mixnet. | | [TypeScript SDK](/developers/typescript) | `@nymproject/sdk`: end-to-end raw messaging when you control both ends. Smart contracts via `@nymproject/contract-clients`. | ### Standalone and other | Resource | Use it for | |---|---| | [SOCKS5 / WebSocket clients](/developers/clients) | Language-agnostic binaries for piping traffic through the mixnet without an SDK. | | [Chain interaction](/developers/chain) | Query Nyx state, submit transactions, call Nym smart contracts. | | [APIs](/apis/introduction) | Auto-generated reference for Nym infrastructure HTTP endpoints. | --- title: Exit Security: What the Mixnet Protects and What It Doesn't description: The canonical security model for traffic that leaves the Nym mixnet at an IPR exit gateway. Applies to smolmix, mix-tunnel, mix-fetch, mix-dns, and mix-websocket alike. url: https://nym.com/docs/developers/concepts/exit-security --- # Exit security Every tool that reaches an external service through the Nym mixnet shares the same security model, whether it's the Rust [`smolmix`](/developers/smolmix) crate or the mix-* packages built on [`mix-tunnel`](/developers/mix-tunnel) ([`mix-fetch`](/developers/mix-fetch), [`mix-dns`](/developers/mix-dns), [`mix-websocket`](/developers/mix-websocket)). They all exit the mixnet at an [IPR (Internet Packet Router)](/network/infrastructure/exit-services#ip-packet-router) gateway, so they inherit the same properties and the same single caveat. This page is the canonical statement of that model; the package pages link here rather than restating it. ## The one-sentence version The mixnet hides **who** you are from the destination and **where** you're going from the network, but the exit gateway sees your **destination** and any payload you didn't encrypt yourself. ## Proxy mode or end-to-end? This page is about **proxy mode**: your traffic leaves the mixnet at an IPR exit and continues to a third-party server over clearnet, where the security trade-offs apply. If both ends run a Nym client (**end-to-end**), traffic never exits the mixnet. It stays Sphinx-encrypted from your client to the other client, there is no IPR, and the [encrypt-your-own-payload](#encrypt-your-own-payload) concern below does not arise: the mixnet is the encrypted channel. What still applies to end-to-end traffic is everything that is not exit-specific: the [trust boundaries](#trust-boundaries) (unlinkability is statistical, not absolute) and [what the mixnet does not protect](#what-the-mixnet-does-not-protect) (application identity, fingerprinting, traffic analysis). For the end-to-end wiring itself, see [`nym-sdk`](/developers/rust) and the [TypeScript SDK](/developers/typescript). ## What each hop sees ```text you │ Sphinx ▼ entry gateway │ Sphinx ▼ 3 mix layers │ Sphinx ▼ IPR exit gateway │ plain IP (Sphinx removed here) ▼ destination ``` | Segment | Mixnet encryption | What's visible | |---|---|---| | Your machine → mixnet entry | Sphinx (layered) | Entry gateway sees your IP but not the destination | | Inside the mixnet (entry gateway + 3 mix layers) | Sphinx (layered) | Each node only knows its previous and next hop | | Exit gateway (IPR) | Sphinx removed, raw IP packet exposed | IPR sees destination IP + port. Payload depends on your application layer (see below). | | IPR → remote host | None (Sphinx is mixnet-only; your own TLS, if any, still applies) | Remote host sees the IPR's IP, not yours | The Sphinx encryption is the **mixnet transport layer**: it protects packets as they traverse the mix nodes. At the exit gateway the Sphinx layers are stripped and the original IP packet is forwarded to the destination. This is analogous to how a Tor exit node or VPN endpoint unwraps its tunnel. ## Encrypt your own payload Because the IPR removes the Sphinx layers, whatever is inside that IP packet is visible to the exit unless you encrypted it yourself. - **Application-layer encryption closes the gap.** TLS, the Noise Protocol, or any authenticated encryption keeps the payload as ciphertext to the IPR. It still sees the destination IP and port, but not the content. Over TLS the IPR only ever handles ciphertext bound for that destination; the bytes inside stay opaque to it. - **Unencrypted payloads are fully visible.** Plain HTTP, unencrypted WebSocket (`ws://`), and plain UDP DNS are readable in full at the exit. The mixnet still hides your identity, so the exit reads the content without being able to attribute it to you. ## Trust boundaries - You trust the mixnet to provide unlinkability between sender and receiver. Sphinx provides this cryptographically at the per-packet level: a node cannot read addressing beyond its own hop. Unlinkability of your *traffic pattern* over time is weaker, and statistical rather than absolute. It comes from mixing and cover traffic, and degrades with low network traffic, with cover traffic or Poisson timing disabled, and against an adversary that can observe a large fraction of the network. - You trust the IPR exit gateway in the same way you trust a VPN exit or Tor exit node: it can inspect your raw IP packets. The difference is that the IPR doesn't know who is sending the traffic (the mixnet hides your identity). Treat the IPR exactly as you would a VPN exit or a Tor exit node: it can inspect your raw IP packets. The difference Nym adds is that the IPR doesn't know who's sending the traffic. Protect the payload with TLS or equivalent, and pin a trusted exit (`preferredIpr`) if the exit operator matters to you. ## What the mixnet does not protect The mixnet operates at the **network layer**: it hides your IP and unlinks sender from receiver. It does nothing at the **application layer**, so anything you reveal in the content of your traffic is yours to manage: - **Application identity.** If you log in, send a cookie, or include an API token, the destination knows who you are regardless of the network path. The mixnet anonymises the pipe, not what you put through it. - **Fingerprinting.** A stable request pattern, a distinctive TLS or HTTP fingerprint, or a recognisable account correlates your traffic across sessions. `mix-fetch`'s [default headers](/developers/mix-fetch/guides#default-request-headers) reduce trivial fingerprinting but do not make you indistinguishable from a real browser. Separately, the network-layer guarantee itself is not absolute: - **Statistical traffic analysis.** Unlinkability is probabilistic, not a guarantee. It is strong by default but weakens with low network traffic, with cover traffic and Poisson timing turned off, and against an adversary observing a large fraction of the network. If you need anonymity at the application layer too, design for it explicitly: fresh identities, no cross-session correlators, and no logged-in accounts you also use over clearnet. ## Comparison with other privacy tools | | Nym (mixnet) | Tor | VPN | |---|---|---|---| | **Exit node sees traffic?** | Yes (encrypt it) | Yes (encrypt it) | Yes (encrypt it) | | **Exit node knows sender?** | No (mixnet hides identity) | No (onion routing) | Yes (VPN provider knows) | | **Timing analysis resistance** | Strong with defaults (mixing, cover traffic) | Weak (low-latency) | None | | **UDP support** | Yes | No (TCP only) | Yes | The timing-analysis rating assumes the defaults. Cover traffic and Poisson timing can be turned off to trade that resistance for latency and bandwidth, moving Nym's row toward Tor's. The named switches for this (`disableCoverTraffic` / `disablePoissonTraffic`) are specific to the browser/wasm packages ([`mix-tunnel`](/developers/mix-tunnel/guides#configuration) and the feature packages built on it); the native `smolmix` crate does not expose them by those names. The UDP row reflects a design difference, not a ranking: Tor is TCP-only by design, while the Nym IPR routes raw IP. ## Read more The package pages add the parts specific to their transport (where TLS terminates, what the resolver sees, WSS vs `ws://`): - [Exit Gateway Services](/network/infrastructure/exit-services#ip-packet-router): how the IPR allocates addresses and routes raw IP packets, and how it differs from the SOCKS-based Network Requester. - The per-package "Security model" section on [mix-fetch](/developers/mix-fetch/concepts#security-model), [mix-dns](/developers/mix-dns/guides#security-model), and [mix-websocket](/developers/mix-websocket/concepts#security-model) for the transport-specific exposure. --- title: Nym Client Message Queue and Cover Traffic description: How the Nym client queues messages, sends cover traffic via Poisson processes, and manages Sphinx packet streams to prevent timing attacks. url: https://nym.com/docs/developers/concepts/message-queue --- # Message Queue Useful for understanding how the Nym Client works internally, but only of practical interest if you are using the [`Mixnet`](/developers/rust/mixnet) module of the Rust SDK and interacting with the client at a low level. The [`Stream`](/developers/rust/stream) module (`AsyncRead + AsyncWrite` channels) abstracts most of this away. ## Sphinx Packet Streams Clients, once connected to the Mixnet, **are always sending traffic into the Mixnet**; as well as the packets that you as a developer are sending from your application logic, they send [cover traffic](/network/mixnet-mode/cover-traffic) at a constant rate defined by a Poisson process. This is part of the network's mitigation of timing attacks. There are two constant streams of sphinx packets leaving the client at the rate defined by the Poisson process. - one that is solely cover traffic - one that sends a mixture of cover and 'real' traffic ```mermaid --- config: theme: neo-dark layout: elk title: Cover Traffic Stream --- sequenceDiagram box Local Machine participant App Logic participant Nym Client end participant Entry Gateway loop Cover Traffic Stream Nym Client->>Nym Client: Delay Nym Client->>Entry Gateway: Cover traffic end ``` ```mermaid --- config: theme: neo-dark layout: elk title: Mixed Stream --- sequenceDiagram box Local Machine participant App Logic participant Nym Client end participant Entry Gateway loop Cover + Real Traffic Stream Nym Client->>Nym Client: Check internal queue + delay Nym Client->>Entry Gateway: Cover traffic alt Packets with App Payload App Logic-->>Nym Client: Send(bytes): add to internal queue Nym Client->>Nym Client: Check internal queue: bytes to send Nym Client->>Nym Client: Encrypt & packetise bytes Nym Client->>Entry Gateway: Real Packets Nym Client->>Nym Client: Check internal queue: bytes to send Nym Client->>Nym Client: Encrypt & packetise bytes Nym Client->>Entry Gateway: Real Packets Nym Client->>Nym Client: Check internal queue: queue empty end Nym Client->>Nym Client: Delay Nym Client->>Entry Gateway: Cover traffic end ``` > Since Sphinx packets are indistinguishable to an external observer, the only difference between 'real' and cover traffic is whether the payload is empty or not. This can be only known to the eventual receiver of the packet. ## What does `send()` do then? When passing a message to a client (however you do it, either piping messages from an app to a standalone client or via one of the `send` functions exposed by the SDKs), you are **putting that message into the queue** to be source-encrypted and sent later, so that traffic leaving the client stays uniform to an external observer and creates no burst or timing change that could aid traffic analysis. ## Note on Client Shutdown Accidentally dropping a client before your message has been sent is possible and should be avoided (see the [troubleshooting guide](/developers/rust/mixnet/troubleshooting) for more on this). To avoid it: - keep your client process alive, even if you are not expecting a reply to your message - (with the SDKs) disconnect your client properly so that the message queue is flushed of Sphinx packets with real payloads. --- title: smolmix: TCP/UDP Over the Nym Mixnet description: A userspace IP tunnel that provides standard TcpStream and UdpSocket types over the Nym mixnet. Compatible with the async tokio Rust ecosystem. url: https://nym.com/docs/developers/smolmix --- # smolmix `smolmix` is a TCP/UDP tunnel over the Nym mixnet. It uses a userspace network stack [`smoltcp`](https://docs.rs/smoltcp/latest/smoltcp/) to provide `TcpStream` and `UdpSocket` types that work with the async [`tokio`](https://docs.rs/tokio) Rust ecosystem e.g. [`tokio-rustls`](https://docs.rs/tokio-rustls), [`hyper`](https://docs.rs/hyper), [`tokio-tungstenite`](https://docs.rs/tokio-tungstenite), etc. The `TcpStream` type implements tokio's `AsyncRead`/`AsyncWrite` traits and `UdpSocket` provides `send_to`/`recv_from` for datagrams. ```text ┌──────────────────────────────────────────────────────────────┐ │ Your application (TLS, HTTP, WebSocket, DNS, etc.) │ │ └─ smolmix::TcpStream / UdpSocket │ │ └─ smoltcp (userspace TCP/IP) │ │ └─ Nym mixnet → IPR exit gateway → internet │ └──────────────────────────────────────────────────────────────┘ ``` Traffic exits the mixnet at an [IPR (Internet Packet Router)](/network/infrastructure/exit-services#ip-packet-router) exit gateway. The exit IP is the gateway's, not yours. ## Runtime and platform support ### Other runtimes `smolmix` currently requires `tokio`. The internal pipeline is tokio-based: the bridge task that shuttles packets to the mixnet, the Nym SDK's `MixnetClient`, and the [`tokio-smoltcp`](https://docs.rs/tokio-smoltcp) reactor that drives the userspace TCP/IP stack all run on the tokio runtime. This means `smolmix` is not directly compatible with alternative async runtimes like [`smol`](https://docs.rs/smol) or [`async-std`](https://docs.rs/async-std). If you need to use `smolmix` from another runtime, the [`async-compat`](https://docs.rs/async-compat) crate can bridge the gap. ### The WebAssembly build (browsers and WebViews) The WebAssembly build of `smolmix` powers the mix-* packages [`mix-tunnel`](/developers/mix-tunnel), [`mix-fetch`](/developers/mix-fetch), [`mix-dns`](/developers/mix-dns), and [`mix-websocket`](/developers/mix-websocket). The wasm is inlined into `mix-tunnel`, so apps install one of those packages rather than building the crate themselves. It runs in any JavaScript runtime that provides `WebSocket`, Web Workers, and WebAssembly. That means desktop browsers and mobile WebViews alike, so hybrid app shells (Capacitor, Ionic, Cordova, `react-native-webview`) are in scope, not just web pages. It does not run in a bare Node.js or React Native Hermes/JSC runtime, which lack Web Workers (and, for Hermes, WebAssembly). For native mobile or server targets, compile the Rust crate itself rather than using the wasm packages. ## Installation Add `smolmix` to your `Cargo.toml`: ```toml [dependencies] smolmix = "1.21.1" nym-bin-common = { version = "1.21.1", features = ["basic_tracing"] } tokio = { version = "1", features = ["rt-multi-thread", "macros"] } ``` `tokio` is a transitive dependency of smolmix, but you need to enable `rt-multi-thread` (the default runtime spun up by `#[tokio::main]`) and `macros` (for the `#[tokio::main]` attribute itself). `nym-bin-common` is optional but recommended: it sets up [`tracing`](https://docs.rs/tracing) logging so you can see mixnet connection progress. **Minimum Rust version:** {RUST_MSRV}+ `smolmix`'s API may still change between minor releases. Pin a version rather than tracking the latest, and read the changelog before bumping. ### From Git For unreleased changes, import directly from the repository: ```toml smolmix = { git = "https://github.com/nymtech/nym", branch = "develop" } ``` ## When to use smolmix | | smolmix | Stream module | mixFetch | SOCKS client | |---|---|---|---|---| | **Layer** | Transport (TCP/UDP) | Message (multiplexed streams) | HTTP | TCP (SOCKS proxy) | | **Controls both sides?** | No (proxy mode) | Yes (E2E) | No (proxy mode) | No (proxy mode) | | **API** | `TcpStream`, `UdpSocket` | `AsyncRead + AsyncWrite` | `fetch()` replacement | SOCKS4/5 protocol | | **Composability** | Full: TLS, HTTP, WebSocket, DNS, etc. stack on top | Byte streams only | HTTP(S) only | Application-dependent | | **Best for** | Reaching external services from Rust with standard networking | Peer-to-peer / E2E protocols between Nym clients | Browser HTTP requests | Legacy apps with SOCKS support | ## Security model Traffic is Sphinx-encrypted inside the mixnet. Past the Exit Gateway, it travels over the public internet to the destination, the same as any other server-initiated connection. Protect the payload at the application layer with TLS ([`rustls`](https://docs.rs/rustls)), Noise Protocol ([`snow`](https://docs.rs/snow)), or equivalent, as you would on a direct connection. The mixnet hides your identity from the destination and your destination from the network, but the IPR exit gateway sees the destination IP and port, and sees your payload only if you didn't encrypt it. If you connect with TLS (as in the [TCP example](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/tcp.rs)), the IPR only sees ciphertext addressed to the destination, not its contents. Plaintext HTTP is fully readable at the exit. The full model (what each hop sees, trust boundaries, and a comparison with Tor and VPNs) is on [Exit security](/developers/concepts/exit-security). ## Examples Runnable examples in [`smolmix/core/examples/`](https://github.com/nymtech/nym/tree/develop/smolmix/core/examples). Each is self-contained; read the `//!` doc comments at the top of each file for a walkthrough. ```sh cargo run -p smolmix --example ``` All examples accept `--ipr
` to target a specific exit node (pass a `Recipient` address to `Tunnel::builder().ipr_address()`). | Example | Source | What it demonstrates | |---|---|---| | UDP | [`udp.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/udp.rs) | DNS lookup via [`hickory-proto`](https://docs.rs/hickory-proto), sending a raw UDP query to `1.1.1.1:53` through the mixnet. Runs a clearnet [`hickory-resolver`](https://docs.rs/hickory-resolver) lookup alongside it to compare resolved IPs and latency | | TCP | [`tcp.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/tcp.rs) | HTTPS request via [`hyper`](https://docs.rs/hyper) + [`tokio-rustls`](https://docs.rs/tokio-rustls). Fetches Cloudflare's `/cdn-cgi/trace` to show that the exit IP differs from clearnet | | WebSocket | [`websocket.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/websocket.rs) | WebSocket echo against `ws.postman-echo.com` via [`tokio-tungstenite`](https://docs.rs/tokio-tungstenite) + [`tokio-rustls`](https://docs.rs/tokio-rustls). Runs the same stack over clearnet first, so the only thing that changes between runs is the underlying TCP stream | | TCP download | [`tcp_download.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/tcp_download.rs) | DNS-over-mixnet + multi-request HTTP/1.1 download over a single keep-alive connection, the full real-world pattern | ## Architecture The internal stack (smoltcp reactor, device adapter, bridge task, packet flow) is documented in [`ARCHITECTURE.md`](https://github.com/nymtech/nym/blob/develop/smolmix/core/src/ARCHITECTURE.md). This is also the crate-level documentation on docs.rs. ## API reference Full API documentation is available on [docs.rs/smolmix](https://docs.rs/smolmix). --- title: nym-sdk: Rust SDK for the Nym Mixnet description: Rust SDK reference for building privacy applications on the Nym mixnet. Covers the Mixnet client, Stream multiplexing, Client Pool, FFI bindings, and code examples. url: https://nym.com/docs/developers/rust --- # nym-sdk `nym-sdk` is the Rust SDK for the Nym mixnet. All modules share a common `MixnetClient` that manages gateway connections, Sphinx encryption, and cover traffic. ```text ┌──────────────────────────────────────────────────────────────┐ │ Your Rust app (alice) │ │ └─ MixnetClient (Sphinx layering, cover traffic) │ │ └─ WebSocket to entry gateway │ │ └─ Nym mixnet (entry → 3 mix layers → exit) │ │ └─ MixnetClient (bob) │ │ └─ Your Rust app (bob) │ └──────────────────────────────────────────────────────────────┘ ``` Both sides run a `MixnetClient`. Sphinx encryption protects every hop end-to-end; neither gateway nor any mix node can link sender to receiver. Full API reference: [**docs.rs/nym-sdk**](https://docs.rs/nym-sdk/latest/nym_sdk/) For an overview of what the SDK can do, see the **[Tour](./rust/tour)**. For setup instructions, see [Installation](./rust/importing). ## Modules | Module | What it does | Status | |---|---|---| | [**Stream**](./rust/stream) | Multiplexed `AsyncRead + AsyncWrite` byte streams over the Mixnet, the closest analogue to TCP sockets. | Recommended | | [**Mixnet**](./rust/mixnet) | Raw message payloads, independently routed, no connections or ordering. Full control over the communication model. | Stable | | [**Client Pool**](./rust/client-pool) | Keeps ready-to-use `MixnetClient` instances warm for bursty workloads. | Stable | | [**TcpProxy**](./rust/tcpproxy) | TCP socket proxying with session management and message ordering. | Deprecated | | [**FFI**](./rust/ffi) | Go and C/C++ bindings. | Stable | **TcpProxy is deprecated.** Use the [Stream module](./rust/stream) for new projects. ## Proxy-mode crates For proxy-mode integrations (reaching third-party services through an Exit Gateway), see also: - [**`smolmix`**](/developers/smolmix): `TcpStream` and `UdpSocket` over the Mixnet via a userspace IP stack. Compatible with `tokio-rustls`, `hyper`, `tokio-tungstenite`, and the rest of the async Rust ecosystem. - [**SOCKS Client**](./rust/mixnet): SOCKS4/4a/5 proxy via the Exit Gateway's Network Requester. Works with any SOCKS-capable application without code changes. --- title: Tour of the Rust SDK url: https://nym.com/docs/developers/rust/tour --- # Tour of the Rust SDK A quick walkthrough of the most important things you can do with `nym-sdk`. Each section shows working code and links to the module that covers it in depth. The Mixnet is not like regular internet networking. There are no persistent connections, no guaranteed message ordering, and no TCP underneath. At its core, the Mixnet is a message-based anonymity network: you send individual payloads that are Sphinx-encrypted, mixed through multiple nodes, and independently reconstructed at the destination. The raw [message API](./mixnet) therefore works differently from what most developers expect. The [Stream module](./stream) bridges this gap by providing `AsyncRead + AsyncWrite` byte streams on top of the Mixnet. If you are coming from socket-based networking, start with streams. ## Send a raw message payload The message API gives you direct access to the Mixnet's native communication model: individually addressed payloads with no connections and no ordering guarantees. This is useful when you want full control, but it's not how most networking code works: ```rust use nym_sdk::mixnet::{self, MixnetMessageSender}; #[tokio::main] async fn main() { let mut client = mixnet::MixnetClient::connect_new().await.unwrap(); let addr = *client.nym_address(); println!("Connected: {addr}"); // Send a message to ourselves client .send_plain_message(addr, "hello mixnet!") .await .unwrap(); // Receive it (filter empty SURB management messages) if let Some(msgs) = client.wait_for_messages().await { for msg in msgs.iter().filter(|m| !m.message.is_empty()) { println!("Got: {}", String::from_utf8_lossy(&msg.message)); } } // Always disconnect for clean shutdown client.disconnect().await; } ``` The message is Sphinx-encrypted, mixed across 5 nodes, and reconstructed on arrival. The whole round trip takes a few seconds. Next: [Mixnet module](./mixnet) | [Tutorial: Send Your First Private Message](./mixnet/tutorial) ## Reply anonymously with SURBs Every received message carries a `sender_tag`, an opaque token that lets you reply **without knowing the sender's Nym address**. Replies travel back through pre-built Single Use Reply Blocks (SURBs): ```rust // After receiving a message... let tag = received_msg.sender_tag.expect("message includes sender tag"); client.send_reply(tag, "anonymous reply!").await.unwrap(); ``` The replying side never learns where the reply is going, enabling anonymous communication without mutual identity disclosure. ## Open a bidirectional stream If you're used to working with TCP sockets, this is where you'll feel at home. The [Stream module](./stream) provides persistent, bidirectional byte channels that implement tokio's `AsyncRead + AsyncWrite`, so any code that works with sockets works with `MixnetStream`: ```rust use nym_sdk::mixnet; use tokio::io::{AsyncReadExt, AsyncWriteExt}; #[tokio::main] async fn main() { let mut sender = mixnet::MixnetClient::connect_new().await.unwrap(); let mut receiver = mixnet::MixnetClient::connect_new().await.unwrap(); let recv_addr = *receiver.nym_address(); // Receiver creates a listener (activates stream mode) let mut listener = receiver.listener().unwrap(); // Sender opens a stream to the receiver let mut out = sender.open_stream(recv_addr, None).await.unwrap(); // Receiver accepts it let mut inc = listener.accept().await.unwrap(); // Standard tokio I/O: write, flush, read out.write_all(b"hello stream").await.unwrap(); out.flush().await.unwrap(); let mut buf = vec![0u8; 1024]; let n = inc.read(&mut buf).await.unwrap(); println!("{}", String::from_utf8_lossy(&buf[..n])); drop(out); drop(inc); sender.disconnect().await; receiver.disconnect().await; } ``` Activating stream mode (by calling `listener()` or `open_stream()`) disables message-based methods like `send_plain_message()` and `wait_for_messages()`. A single client operates in one mode at a time. Next: [Stream module](./stream) | [Tutorial: Build a Private Echo Server](./stream/tutorial) ## Use a client pool for bursty traffic Creating a `MixnetClient` takes several seconds (gateway handshake, key generation, topology fetch). The [Client Pool](./client-pool) pre-creates clients in the background so they're ready when you need them: ```rust use nym_sdk::client_pool::ClientPool; #[tokio::main] async fn main() { let pool = ClientPool::new(3); // maintain 3 clients in reserve let bg = pool.clone(); tokio::spawn(async move { bg.start().await }); // Wait for pool to fill, then grab a ready client tokio::time::sleep(std::time::Duration::from_secs(15)).await; if let Some(client) = pool.get_mixnet_client().await { println!("Got client: {}", client.nym_address()); client.disconnect().await; } pool.disconnect_pool().await; } ``` Clients are consumed, not returned; the pool creates replacements automatically. Next: [Client Pool module](./client-pool) | [Tutorial: Handle Bursty Traffic](./client-pool/tutorial) ## Persist your identity By default, `connect_new()` creates ephemeral keys that are discarded on disconnect. To keep the same Nym address across restarts, use the builder with on-disk storage: ```rust use nym_sdk::mixnet::{MixnetClientBuilder, StoragePaths}; use std::path::PathBuf; let storage = StoragePaths::new_from_dir( &PathBuf::from("/tmp/my-nym-client") ).unwrap(); let client = MixnetClientBuilder::new_with_default_storage(storage) .await .unwrap() .build() .unwrap() .connect_to_mixnet() .await .unwrap(); // This address is the same every time you run with the same path println!("Persistent address: {}", client.nym_address()); ``` ## Where to go next - [Installation](./importing): add `nym-sdk` to your project - [Mixnet Tutorial](./mixnet/tutorial): send, receive, and reply with SURBs - [Stream Tutorial](./stream/tutorial): build a private echo server - [Client Pool Tutorial](./client-pool/tutorial): handle bursty traffic - [API Reference on docs.rs](https://docs.rs/nym-sdk/latest/nym_sdk/): type details, method signatures, architecture docs --- title: Install the Nym Rust SDK description: Add nym-sdk to your Rust project from Git or crates.io. Covers version requirements, minimum Rust version, and current feature gate status. url: https://nym.com/docs/developers/rust/importing --- # Installation ```toml [dependencies] nym-sdk = "1.21.1" ``` **Minimum Rust version:** {RUST_MSRV}+ ### From Git You can also import directly from Git if you want unreleased changes: ```toml # development branch (latest changes, may be unstable) nym-sdk = { git = "https://github.com/nymtech/nym", branch = "develop" } # latest stable release nym-sdk = { git = "https://github.com/nymtech/nym", branch = "master" } ``` **No feature gates yet.** Importing `nym-sdk` pulls in everything (mixnet, tcp_proxy, client_pool, etc.) and their full dependency trees. Cargo feature flags are planned. --- title: Nym Rust SDK: Mixnet Messaging Module description: Use the Nym Rust SDK Mixnet module to send messages through the mixnet. Covers builder patterns, custom topologies, SOCKS proxy, and anonymous replies. url: https://nym.com/docs/developers/rust/mixnet --- # Mixnet Module The `mixnet` module provides [`MixnetClient`](https://docs.rs/nym-sdk/latest/nym_sdk/mixnet/struct.MixnetClient.html) for connecting to the Nym Mixnet, sending messages through Sphinx packet encryption and 5-hop routing, and receiving reconstructed messages on the other side. Messages are individually routed through the Mixnet with no guaranteed ordering or persistent connections. If you want familiar socket-like I/O (`read`/`write`), use the [Stream module](./stream) instead. See the [Tour](./tour) for how the two approaches compare. ## Two operating modes The client operates in one of two mutually exclusive modes: **Message mode** (default): send and receive raw message payloads: ```rust use nym_sdk::mixnet::{self, MixnetMessageSender}; let mut client = mixnet::MixnetClient::connect_new().await.unwrap(); // Send a message client.send_plain_message(*client.nym_address(), "hello").await.unwrap(); // Receive messages if let Some(msgs) = client.wait_for_messages().await { for msg in msgs { println!("Got: {}", String::from_utf8_lossy(&msg.message)); } } client.disconnect().await; ``` **Stream mode:** persistent `AsyncRead + AsyncWrite` channels. See the [Stream module](./stream) for details. Stream mode is activated by calling `open_stream()` or `listener()`. Once active, message-mode methods return `Error::StreamModeActive`. This is a one-way transition. ## API reference - [API reference on docs.rs](https://docs.rs/nym-sdk/latest/nym_sdk/mixnet/): full architecture documentation, all types, builder methods, traits, and configuration options - [Examples on GitHub](https://github.com/nymtech/nym/tree/develop/sdk/rust/nym-sdk/examples): runnable examples covering simple send/receive, builder patterns, custom topologies, SOCKS proxy, anonymous replies, and more Run any example with: ```sh cargo run --example ``` ## Next steps - [Tutorial: Send your first private message](./mixnet/tutorial): step-by-step guide covering sending, receiving, SURBs, and persistent identity - [Troubleshooting](./mixnet/troubleshooting): common issues with logging, empty messages, and client lifecycle - [Stream module](./stream): if you need persistent bidirectional byte channels --- title: Mixnet Tutorial: Send Your First Private Message description: Step-by-step Rust tutorial to connect to the Nym mixnet, send and receive messages, reply anonymously with SURBs, and persist client identity. url: https://nym.com/docs/developers/rust/mixnet/tutorial --- # Tutorial: Send Your First Private Message A program that sends a Sphinx-encrypted message to itself through the Nym Mixnet, receives it, and replies anonymously using SURBs. Later sections cover persistent identity and concurrent send/receive. Requires Rust {RUST_MSRV}+ and an internet connection (clients connect to the live Mixnet). ## Step 1: Set up the project ```sh cargo init nym-mixnet-demo cd nym-mixnet-demo ``` Add dependencies to `Cargo.toml`: ```toml [dependencies] nym-sdk = "1.21.1" nym-bin-common = { version = "1.21.1", features = ["basic_tracing"] } tokio = { version = "1", features = ["full"] } ``` ## Step 2: Connect and send Replace the contents of `src/main.rs`: ```rust use nym_sdk::mixnet::{self, MixnetMessageSender}; #[tokio::main] async fn main() { nym_bin_common::logging::setup_tracing_logger(); // connect_new() creates an ephemeral client; keys are generated in // memory and discarded on disconnect. let mut client = mixnet::MixnetClient::connect_new().await.unwrap(); let our_address = client.nym_address(); println!("Connected: {our_address}"); // The message is Sphinx-encrypted and mixed across 5 nodes. // send_plain_message only blocks until the message is queued; // encryption and mixing happen in background tasks. client .send_plain_message(*our_address, "hello from the mixnet!") .await .unwrap(); println!("Sent, waiting for arrival..."); ``` `setup_tracing_logger()` shows what the SDK is doing under the hood: gateway connections, topology fetches, Sphinx packet encryption. If the output is too verbose, comment out the line or filter with `RUST_LOG=warn cargo run`. ## Step 3: Receive ```rust // wait_for_messages() returns the next batch of incoming messages. // Filter empty messages: these are SURB replenishment requests. let message = loop { if let Some(msgs) = client.wait_for_messages().await { if let Some(msg) = msgs.into_iter().find(|m| !m.message.is_empty()) { break msg; } } }; println!("Received: {}", String::from_utf8_lossy(&message.message)); ``` ## Step 4: Reply anonymously Every message includes a `sender_tag`, an opaque `AnonymousSenderTag` that lets you reply **without knowing the sender's address**. The SDK bundles SURBs (Single Use Reply Blocks) with every outgoing message by default: ```rust let sender_tag = message.sender_tag.expect("should have sender tag"); // send_reply uses the SURB; the sender's address is never revealed. client.send_reply(sender_tag, "hello back, anonymously!").await.unwrap(); let reply = loop { if let Some(msgs) = client.wait_for_messages().await { if let Some(msg) = msgs.into_iter().find(|m| !m.message.is_empty()) { break msg; } } }; println!("Reply: {}", String::from_utf8_lossy(&reply.message)); client.disconnect().await; } ``` ## Step 5: Run it ```sh RUST_LOG=info cargo run ``` ``` Connected: 8gk4Y...@2xU4d... Sent, waiting for arrival... Received: hello from the mixnet! Reply: hello back, anonymously! ``` ## Going further: persist your identity The ephemeral client above generates a new address on every run. To keep the same address across restarts, use `MixnetClientBuilder` with on-disk storage: ```rust use nym_sdk::mixnet::{self, MixnetMessageSender, StoragePaths}; #[tokio::main] async fn main() { // Keys are generated on first run, then loaded from disk on subsequent runs. let paths = StoragePaths::new_from_dir("./my-client-data").unwrap(); let mut client = mixnet::MixnetClientBuilder::new_with_default_storage(paths) .await .unwrap() .build() .unwrap() .connect_to_mixnet() .await .unwrap(); let our_address = client.nym_address(); println!("Address: {our_address}"); // Same API as before: send, receive, SURB reply. client .send_plain_message(*our_address, "hello from persistent client!") .await .unwrap(); println!("Sent, waiting for arrival..."); let message = loop { if let Some(msgs) = client.wait_for_messages().await { if let Some(msg) = msgs.into_iter().find(|m| !m.message.is_empty()) { break msg; } } }; println!("Received: {}", String::from_utf8_lossy(&message.message)); let sender_tag = message.sender_tag.expect("should have sender tag"); client.send_reply(sender_tag, "anonymous reply!").await.unwrap(); let reply = loop { if let Some(msgs) = client.wait_for_messages().await { if let Some(msg) = msgs.into_iter().find(|m| !m.message.is_empty()) { break msg; } } }; println!("Reply: {}", String::from_utf8_lossy(&reply.message)); // Always disconnect for clean shutdown; background tasks need to be // stopped and state files flushed. client.disconnect().await; } ``` Run it twice; the address stays the same. ## Going further: send and receive from different tasks Add `futures` to your `Cargo.toml`: ```toml futures = "0.3" ``` Use `split_sender()` to get a clone-able send handle for use in separate tasks: ```rust use futures::StreamExt; use nym_sdk::mixnet::{self, MixnetMessageSender}; #[tokio::main] async fn main() { let mut client = mixnet::MixnetClient::connect_new().await.unwrap(); let addr = *client.nym_address(); // split_sender() returns a clone-able MixnetClientSender. let sender = client.split_sender(); // Spawn a receiver: the original client implements futures::Stream. let rx = tokio::spawn(async move { if let Some(msg) = client.next().await { println!("Received: {}", String::from_utf8_lossy(&msg.message)); } client.disconnect().await; }); // Spawn a sender on a different task. let tx = tokio::spawn(async move { sender.send_plain_message(addr, "hello from another task!").await.unwrap(); }); tx.await.unwrap(); rx.await.unwrap(); } ``` ## What's happening underneath `connect_new()` generates an ephemeral identity (ed25519 + x25519 keypair), fetches the current network topology, selects a gateway, and opens a persistent WebSocket connection. `send_plain_message()` wraps the payload in Sphinx packets, layered encryption where each of the 5 Mix Nodes can only decrypt one layer and learn the next hop, never the full route. `wait_for_messages()` drains a local queue fed by the gateway; messages arrive out of order by design, to defeat timing analysis. SURBs (Single Use Reply Blocks) are pre-computed return routes bundled with each outgoing message. The recipient uses them to reply without learning the sender's address. Each is single-use; the SDK replenishes them automatically. `split_sender()` clones the send channel while the original client retains the receive side. Both halves can run on separate tokio tasks without synchronization. ## Complete code ### Ephemeral client New address on every run, good for quick experiments: ```rust use nym_sdk::mixnet::{self, MixnetMessageSender}; #[tokio::main] async fn main() { nym_bin_common::logging::setup_tracing_logger(); let mut client = mixnet::MixnetClient::connect_new().await.unwrap(); let our_address = client.nym_address(); println!("Connected: {our_address}"); client .send_plain_message(*our_address, "hello from the mixnet!") .await .unwrap(); println!("Sent, waiting for arrival..."); let message = loop { if let Some(msgs) = client.wait_for_messages().await { if let Some(msg) = msgs.into_iter().find(|m| !m.message.is_empty()) { break msg; } } }; println!("Received: {}", String::from_utf8_lossy(&message.message)); let sender_tag = message.sender_tag.expect("should have sender tag"); client.send_reply(sender_tag, "hello back, anonymously!").await.unwrap(); let reply = loop { if let Some(msgs) = client.wait_for_messages().await { if let Some(msg) = msgs.into_iter().find(|m| !m.message.is_empty()) { break msg; } } }; println!("Reply: {}", String::from_utf8_lossy(&reply.message)); client.disconnect().await; } ``` ### Persistent identity Same address across restarts. Use this for real applications: ```rust use nym_sdk::mixnet::{self, MixnetMessageSender, StoragePaths}; #[tokio::main] async fn main() { nym_bin_common::logging::setup_tracing_logger(); let paths = StoragePaths::new_from_dir("./my-client-data").unwrap(); let mut client = mixnet::MixnetClientBuilder::new_with_default_storage(paths) .await .unwrap() .build() .unwrap() .connect_to_mixnet() .await .unwrap(); let our_address = client.nym_address(); println!("Address: {our_address}"); client .send_plain_message(*our_address, "hello from persistent client!") .await .unwrap(); println!("Sent, waiting for arrival..."); let message = loop { if let Some(msgs) = client.wait_for_messages().await { if let Some(msg) = msgs.into_iter().find(|m| !m.message.is_empty()) { break msg; } } }; println!("Received: {}", String::from_utf8_lossy(&message.message)); let sender_tag = message.sender_tag.expect("should have sender tag"); client.send_reply(sender_tag, "anonymous reply!").await.unwrap(); let reply = loop { if let Some(msgs) = client.wait_for_messages().await { if let Some(msg) = msgs.into_iter().find(|m| !m.message.is_empty()) { break msg; } } }; println!("Reply: {}", String::from_utf8_lossy(&reply.message)); client.disconnect().await; } ``` --- title: Mixnet Module Examples description: Runnable Rust examples for the Nym mixnet module: sending messages, SURB anonymous replies, MixnetClientBuilder, persistent storage, and parallel send/receive. url: https://nym.com/docs/developers/rust/mixnet/examples --- # Examples Runnable examples in [`sdk/rust/nym-sdk/examples/`](https://github.com/nymtech/nym/tree/develop/sdk/rust/nym-sdk/examples). Each file is self-contained with step-by-step comments. ```bash cargo run --example ``` | Example | Source | What it demonstrates | |---|---|---| | Simple | [`simple.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/simple.rs) | Send a message to yourself and print it | | SURB Reply | [`surb_reply.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/surb_reply.rs) | Anonymous replies using `AnonymousSenderTag` and `send_reply()` | | Builder | [`builder.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/builder.rs) | Using `MixnetClientBuilder` with ephemeral keys | | Builder with Storage | [`builder_with_storage.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/builder_with_storage.rs) | Persisting keys to disk with `StoragePaths` | | Parallel Send/Receive | [`parallel_sending_and_receiving.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/parallel_sending_and_receiving.rs) | Using `split_sender()` for concurrent tasks | | Sandbox Testnet | [`sandbox.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/sandbox.rs) | Connecting to the Sandbox testnet instead of mainnet | | Bandwidth Credential | [`bandwidth.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/bandwidth.rs) | Acquiring a bandwidth credential for paid mixnet access | | Custom Topology | [`custom_topology_provider.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/custom_topology_provider.rs) | Implementing the `TopologyProvider` trait to filter or customise node selection | | Overwrite Topology | [`manually_overwrite_topology.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs) | Manually constructing a topology with hardcoded nodes | | Control Requests | [`control_requests.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/control_requests.rs) | Sending service provider control requests (health, version, binary info) | | Custom Storage | [`manually_handle_storage.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/manually_handle_storage.rs) | Implementing custom storage backends for keys, gateways, and credentials | --- title: Mixnet Module Troubleshooting description: Solutions for common Nym Rust SDK issues: client disconnect errors, empty SURB messages, verbose logging, and database lock problems. url: https://nym.com/docs/developers/rust/mixnet/troubleshooting --- # Troubleshooting Common issues and how to resolve them. ## Always disconnect your client You should always **manually disconnect your client** with `client.disconnect().await`. The client writes to a local DB and manages SURB storage, so it needs to shut down gracefully. Failing to do this can lead to the errors described below. ## Waiting for non-empty messages When listening for a response, you may receive empty messages. These are SURB replenishment requests: the remote side asking for more reply SURBs. Filter them out: ```rust let mut message = None; while let Some(new_message) = client.wait_for_messages().await { if !new_message.is_empty() { message = new_message.into_iter().next(); break; } } ``` Prefer `client.next().await` (from the `futures::StreamExt` trait, not the Nym Stream module) over `client.wait_for_messages().await`; it returns one message at a time which is easier to work with. You'll need `use futures::StreamExt;` in scope. ## Verbose `task client is being dropped` logging ### On client shutdown (expected) When calling `client.disconnect().await`, the client logs that its background tasks are shutting down. This is normal and expected. Control log verbosity with `RUST_LOG`: ```sh RUST_LOG=warn cargo run --example simple ``` ### Not on client shutdown (unexpected) If you see these messages unexpectedly, you may be killing the client process too early. See the next section. ## Accidentally killing your client process too early If you see errors like `Polling shutdown failed: channel closed` or panics about `action control task has died`, your client is being dropped before it finishes sending. `send_plain_message()` is async, but **it only blocks until the message is placed in the client's internal queue**, not until it's actually sent into the Mixnet. After queuing, the client still needs to route-encrypt the message and interleave it with cover traffic. Make sure the program stays alive long enough. In practice this means awaiting a response or calling `sleep` before disconnecting: ```rust // Send a message client.send_plain_message(recipient, "hello").await.unwrap(); // Wait for the reply (keeps the client alive) if let Some(received) = client.wait_for_messages().await { for r in received { println!("Received: {}", String::from_utf8_lossy(&r.message)); } } // Always disconnect gracefully client.disconnect().await; ``` ## Lots of `duplicate fragment received` messages `WARN` level logs about duplicate fragments are caused by Mixnet-level packet retransmission: the original and the retransmitted copy both arrive. This is not a bug in your client logic. ## Android: mixnet bootstrap fails with a certificate `Revoked` / OCSP error When you compile the SDK for Android (via `uniffi` + `cargo-ndk`), the client can fail to bootstrap the mixnet with a hard `Revoked` or "Certificate does not specify OCSP responder" error on certain Validator endpoints. The cause is `rustls-platform-verifier` routing certificate validation through Java's `CertPathValidator`, which enforces an OCSP check the endpoint does not satisfy. iOS and the desktop targets are unaffected; they tolerate or skip the check. Configure the client to use preconfigured TLS roots with `webpki_roots::TLS_SERVER_ROOTS` instead of the platform verifier to get around it. --- title: Stream Module: AsyncRead/AsyncWrite Over the Mixnet description: The Nym Stream module provides persistent, bidirectional byte channels over the mixnet with standard Rust AsyncRead and AsyncWrite traits. url: https://nym.com/docs/developers/rust/stream --- # Stream Module The Mixnet is fundamentally message-based: no persistent connections, no guaranteed ordering, no TCP. The default [message API](./mixnet) works at this level, sending individual payloads independently through Mix Nodes. This is effective for privacy but unlike how most networking code is structured. The Stream module bridges the gap by providing persistent, bidirectional byte channels that behave like TCP sockets. Each `MixnetStream` implements [`AsyncRead`](https://docs.rs/tokio/latest/tokio/io/trait.AsyncRead.html) and [`AsyncWrite`](https://docs.rs/tokio/latest/tokio/io/trait.AsyncWrite.html), so `tokio::io::copy`, codecs, `BufReader`/`BufWriter`, and any other async I/O consumer work without modification. All streams are multiplexed over a single `MixnetClient`. A background router task reads a small header on each incoming message and dispatches the payload to the correct stream by ID, so multiple concurrent streams require no additional connections or gateways. ## How it works The two sides of a stream connection follow a client/server pattern: 1. The opener calls `client.open_stream(recipient, surbs)`. This generates a random `StreamId`, registers the stream locally, and sends an `Open` message through the Mixnet. 2. The listener calls `listener.accept()`, which blocks until an `Open` arrives, registers the new stream, and returns a `MixnetStream` ready for reading and writing. 3. Both sides read and write using standard `AsyncRead`/`AsyncWrite`. Bytes are wrapped in a 16-byte LP frame header (stream ID, message type, sequence number), routed through the Mixnet, and demultiplexed on arrival. 4. On drop, the stream deregisters from the local router. No close message is sent over the wire, since a close could race ahead of in-flight data. ```text ┌─────────────────────────────────────────────────────────┐ │ MixnetClient │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ MixnetStream │ │ MixnetStream │ ... │ │ │ (peer A) │ │ (peer B) │ │ │ └──────┬───────┘ └──────┬───────┘ │ │ │writes │writes │ │ ▼ ▼ │ │ ┌─────────────────────────────────┐ │ │ │ ClientInput.input_sender │ │ │ └──────────────┬──────────────────┘ │ │ │ │ │ ▼ │ │ ── mixnet ── │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────┐ │ │ │ reconstructed_receiver │ │ │ └──────────────┬──────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────┐ │ │ │ Router task │ │ │ │ decode header → dispatch by ID │ │ │ └──┬──────────────────────────┬───┘ │ │ │ Open messages │ Data messages │ │ ▼ ▼ │ │ ┌──────────────┐ ┌──────────────────┐ │ │ │MixnetListener│ │ StreamMap lookup │ │ │ │ .accept() │ │ → per-stream tx │ │ │ └──────────────┘ └──────────────────┘ │ └─────────────────────────────────────────────────────────┘ ``` ## Complete example A minimal example with two clients on the same machine: one opens a stream to the other, sends a message, and reads a reply. ```rust use nym_sdk::mixnet; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use std::time::Duration; const TIMEOUT: Duration = Duration::from_secs(60); #[tokio::main] async fn main() { // Connect two ephemeral clients let mut sender = mixnet::MixnetClient::connect_new().await.unwrap(); let mut receiver = mixnet::MixnetClient::connect_new().await.unwrap(); let receiver_addr = *receiver.nym_address(); // The receiver creates a listener (activates stream mode) let mut listener = receiver.listener().unwrap(); // The sender opens a stream to the receiver's Nym address let mut outbound = sender.open_stream(receiver_addr, None).await.unwrap(); // The receiver accepts the incoming stream let mut inbound = tokio::time::timeout(TIMEOUT, listener.accept()) .await .expect("timed out") .expect("listener closed"); // Send data and read it back, just like a TCP socket outbound.write_all(b"hello from sender").await.unwrap(); outbound.flush().await.unwrap(); let mut buf = vec![0u8; 1024]; let n = tokio::time::timeout(TIMEOUT, inbound.read(&mut buf)) .await .expect("timed out") .expect("read failed"); println!("Receiver got: {}", String::from_utf8_lossy(&buf[..n])); // Reply back through the same stream inbound.write_all(b"hello from receiver").await.unwrap(); inbound.flush().await.unwrap(); let n = tokio::time::timeout(TIMEOUT, outbound.read(&mut buf)) .await .expect("timed out") .expect("read failed"); println!("Sender got: {}", String::from_utf8_lossy(&buf[..n])); // Streams deregister on drop, then disconnect clients drop(outbound); drop(inbound); sender.disconnect().await; receiver.disconnect().await; } ``` The receiver replies via **reply SURBs** (Single Use Reply Blocks) and never learns the sender's Nym address. ## When to use streams vs messages | | Messages | Streams | TcpProxy | |---|---|---|---| | **Pattern** | Raw message payloads | Persistent bidirectional channels | TCP socket proxying | | **API** | `send_plain_message()` / `wait_for_messages()` | `AsyncRead` + `AsyncWrite` | Localhost TCP socket | | **Multiplexing** | N/A | Multiple streams per client | One client per TCP connection | | **Ordering** | No guarantees | Sequence-based reordering | Session-based ordering | | **Best for** | Simple notifications, one-shot requests | Interactive protocols, streaming data, any code expecting async I/O | Wrapping existing TCP applications | | **Status** | Stable | New | Deprecated | Streams and messages are mutually exclusive. Once you call `open_stream()` or `listener()`, the message-based API (`send_plain_message`, `wait_for_messages`) is permanently disabled on that client. This is a one-way transition: no switching back without disconnecting and reconnecting. See the [`stream_mode_guard.rs` example](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/stream_mode_guard.rs) for details. ## Next steps - [Tutorial: Build a private echo server](./stream/tutorial): server and client communicating over streams - [Architecture](./stream/architecture): wire protocol, router task, data flow, stream cleanup, and known limitations - [Examples](./stream/examples): annotated walkthroughs of the SDK examples (multi-stream, idle timeout, throughput testing) --- title: Stream Tutorial: Build a Private Echo Server description: Step-by-step Rust tutorial to build an echo server and client communicating through the Nym mixnet using AsyncRead and AsyncWrite streams. url: https://nym.com/docs/developers/rust/stream/tutorial --- # Tutorial: Build a Private Echo Server Two programs: a server that listens for incoming streams and echoes back what it receives, and a client that opens a stream, writes data, and reads the echo. Both communicate through the Nym Mixnet using `AsyncRead` and `AsyncWrite`, like a TCP socket pair. ## What you'll learn - Setting up a `MixnetListener` to accept incoming streams - Opening an outbound stream with `open_stream()` - Reading and writing with standard tokio I/O traits - How streams are multiplexed over a single `MixnetClient` - Clean shutdown and stream lifecycle ## Prerequisites - Rust toolchain ({RUST_MSRV}+) - A working internet connection (clients connect to the live Nym Mixnet) ## Step 1: Set up the project ```sh cargo init nym-echo cd nym-echo rm src/main.rs ``` Add dependencies to `Cargo.toml`: ```toml [dependencies] nym-sdk = "1.21.1" nym-bin-common = { version = "1.21.1", features = ["basic_tracing"] } tokio = { version = "1", features = ["full"] } rand = "0.8" ``` ## Step 2: Build the echo server The server connects a `MixnetClient`, creates a listener, and accepts streams in a loop. Each stream gets its own task that reads data and writes it back. Create `src/bin/server.rs`: ```rust use nym_sdk::mixnet; use tokio::io::{AsyncReadExt, AsyncWriteExt}; #[tokio::main] async fn main() { nym_bin_common::logging::setup_tracing_logger(); // Connect to the Mixnet let mut client = mixnet::MixnetClient::connect_new().await.unwrap(); println!("Echo server listening at: {}", client.nym_address()); // Create a listener; this activates stream mode. // From this point, message-based methods are disabled. let mut listener = client.listener().unwrap(); // Accept streams in a loop loop { let mut stream = match listener.accept().await { Some(s) => s, None => { println!("Listener closed"); break; } }; let stream_id = stream.id(); println!("Accepted stream {stream_id}"); // Spawn a task to handle each stream concurrently tokio::spawn(async move { let mut buf = vec![0u8; 32_000]; loop { let n = match stream.read(&mut buf).await { Ok(0) => break, // EOF, stream closed Ok(n) => n, Err(e) => { eprintln!("Stream {stream_id} read error: {e}"); break; } }; let data = &buf[..n]; println!("Stream {stream_id} received {n} bytes"); // Echo it back if let Err(e) = stream.write_all(data).await { eprintln!("Stream {stream_id} write error: {e}"); break; } stream.flush().await.unwrap(); } println!("Stream {stream_id} closed"); }); } } ``` `listener()` can only be called once per client. It takes exclusive ownership of the inbound message channel; a second call returns `Error::ListenerAlreadyTaken`. ## Step 3: Build the client The client connects, opens a stream to the server, sends a few messages, reads back the echoes, and disconnects. Create `src/bin/client.rs`: ```rust use nym_sdk::mixnet::{self, Recipient}; use std::time::Duration; use tokio::io::{AsyncReadExt, AsyncWriteExt}; const TIMEOUT: Duration = Duration::from_secs(60); #[tokio::main] async fn main() { nym_bin_common::logging::setup_tracing_logger(); // Read the server's Nym address from the command line let server_addr: Recipient = std::env::args() .nth(1) .expect("Usage: client ") .parse() .expect("Invalid Nym address"); // Connect to the Mixnet let mut client = mixnet::MixnetClient::connect_new().await.unwrap(); println!("Client address: {}", client.nym_address()); // Open a stream to the server. // The second argument (None) uses the default number of reply SURBs. let mut stream = client.open_stream(server_addr, None).await.unwrap(); println!("Stream opened: {}", stream.id()); // Give the Open message time to traverse the mixnet and reach the server. // open_stream() returns immediately after sending; it doesn't wait for // the server to accept. Writing too soon risks the data arriving before // the Open, which the server would drop. tokio::time::sleep(Duration::from_secs(5)).await; // Send three payloads of different sizes and verify the echo. // Random bytes show that streams are binary-safe, not just text. let sizes = [320, 25_000, 1280]; for (i, &size) in sizes.iter().enumerate() { let payload: Vec = (0..size).map(|_| rand::random::()).collect(); println!("Sending message {} ({size} bytes)", i + 1); stream.write_all(&payload).await.unwrap(); stream.flush().await.unwrap(); // Read the echo let mut buf = vec![0u8; 32_000]; let n = tokio::time::timeout(TIMEOUT, stream.read(&mut buf)) .await .expect("timed out waiting for echo") .expect("read failed"); assert_eq!(&buf[..n], &payload[..], "echo mismatch on message {}", i + 1); println!("Received echo: {n} bytes ok"); } // Drop the stream to deregister it from the router drop(stream); // Disconnect the client client.disconnect().await; println!("Done!"); } ``` ## Step 4: Run it In one terminal, start the server: ```sh RUST_LOG=info cargo run --bin server ``` It prints its Nym address: ``` Echo server listening at: 8gk4Y...@2xU4d... ``` In a second terminal, start the client with the server's address: ```sh RUST_LOG=info cargo run --bin client -- 8gk4Y...@2xU4d... ``` You'll see the messages traverse the Mixnet and echo back: ``` Client address: F3qR7...@9nK2m... Stream opened: 12345678 Sending message 1 (320 bytes) Received echo: 320 bytes ok Sending message 2 (25000 bytes) Received echo: 25000 bytes ok Sending message 3 (1280 bytes) Received echo: 1280 bytes ok Done! ``` On the server side: ``` Accepted stream 12345678 Stream 12345678 received 320 bytes Stream 12345678 received 25000 bytes Stream 12345678 received 1280 bytes Stream 12345678 closed ``` ## How it works internally 1. The server's `listener()` activates **stream mode**, which spawns a **router task** that decodes incoming Mixnet messages and dispatches them by stream ID. 2. The client's `open_stream()` generates a random 8-byte `StreamId`, sends an `Open` message through the Mixnet, and registers the stream in a local routing table. 3. When the server's router receives the `Open` message, it delivers it to `listener.accept()`, which creates the inbound `MixnetStream`. 4. Each `write_all()` prepends a 16-byte LP frame header (`[LpFrameKind: 2B][StreamId: 8B][MsgType: 1B][SequenceNum: 4B][Reserved: 1B]`) and sends the data through the Mixnet as a Sphinx packet. 5. On arrival, the router reads the `LpFrameKind` to identify it as stream traffic, decodes the header, finds the matching stream by ID, and delivers the raw payload to `read()`. 6. The inbound stream replies via reply SURBs, the same anonymous reply mechanism as the message API. The server never learns the client's Nym address. 7. When a stream is dropped, it deregisters from the local router. No close message is sent over the wire, since a close could race ahead of in-flight data. See the [Architecture](./architecture) page for the full technical details. ## What you've learned - `client.listener()` activates stream mode and returns a `MixnetListener` - `listener.accept()` blocks until a remote peer opens a stream - `client.open_stream(recipient, surbs)` opens an outbound stream to a Nym address - `MixnetStream` implements `AsyncRead + AsyncWrite`, so standard tokio I/O works unchanged - Multiple streams are multiplexed over a single client - Streams deregister on `drop`; no close handshake is needed - The server replies via SURBs and never learns the client's address ## Complete code ### Server (`src/bin/server.rs`) ```rust use nym_sdk::mixnet; use tokio::io::{AsyncReadExt, AsyncWriteExt}; #[tokio::main] async fn main() { nym_bin_common::logging::setup_tracing_logger(); let mut client = mixnet::MixnetClient::connect_new().await.unwrap(); println!("Echo server listening at: {}", client.nym_address()); let mut listener = client.listener().unwrap(); loop { let mut stream = match listener.accept().await { Some(s) => s, None => { println!("Listener closed"); break; } }; let stream_id = stream.id(); println!("Accepted stream {stream_id}"); tokio::spawn(async move { let mut buf = vec![0u8; 32_000]; loop { let n = match stream.read(&mut buf).await { Ok(0) => break, Ok(n) => n, Err(e) => { eprintln!("Stream {stream_id} read error: {e}"); break; } }; let data = &buf[..n]; println!("Stream {stream_id} received {n} bytes"); if let Err(e) = stream.write_all(data).await { eprintln!("Stream {stream_id} write error: {e}"); break; } stream.flush().await.unwrap(); } println!("Stream {stream_id} closed"); }); } } ``` ### Client (`src/bin/client.rs`) ```rust use nym_sdk::mixnet::{self, Recipient}; use std::time::Duration; use tokio::io::{AsyncReadExt, AsyncWriteExt}; const TIMEOUT: Duration = Duration::from_secs(60); #[tokio::main] async fn main() { nym_bin_common::logging::setup_tracing_logger(); let server_addr: Recipient = std::env::args() .nth(1) .expect("Usage: client ") .parse() .expect("Invalid Nym address"); let mut client = mixnet::MixnetClient::connect_new().await.unwrap(); println!("Client address: {}", client.nym_address()); let mut stream = client.open_stream(server_addr, None).await.unwrap(); println!("Stream opened: {}", stream.id()); // Wait for the Open message to reach the server through the mixnet tokio::time::sleep(Duration::from_secs(5)).await; let sizes = [320, 25_000, 1280]; for (i, &size) in sizes.iter().enumerate() { let payload: Vec = (0..size).map(|_| rand::random::()).collect(); println!("Sending message {} ({size} bytes)", i + 1); stream.write_all(&payload).await.unwrap(); stream.flush().await.unwrap(); let mut buf = vec![0u8; 32_000]; let n = tokio::time::timeout(TIMEOUT, stream.read(&mut buf)) .await .expect("timed out waiting for echo") .expect("read failed"); assert_eq!(&buf[..n], &payload[..], "echo mismatch on message {}", i + 1); println!("Received echo: {n} bytes ok"); } drop(stream); client.disconnect().await; println!("Done!"); } ``` --- title: Stream Module Architecture description: Internal architecture of the Nym Stream subsystem: wire protocol, multiplexing, router task, and how concurrent byte channels share a single MixnetClient. url: https://nym.com/docs/developers/rust/stream/architecture --- # Stream Architecture {/* Canonical source: sdk/rust/nym-sdk/src/mixnet/stream/ARCHITECTURE.md */} ## Overview The stream subsystem gives each `MixnetClient` the ability to hold many concurrent byte channels (`AsyncRead + AsyncWrite`) to different remote peers, multiplexed over a single client connection. ```mermaid --- config: theme: neo-dark --- flowchart TD subgraph MixnetClient SA["MixnetStream A"] -->|writes| CI["Client input channel"] SB["MixnetStream B"] -->|writes| CI CI --> MX["── Mixnet ──"] MX --> RT["Router task"] RT -->|Open messages| ML["MixnetListener.accept()"] RT -->|Data messages| SM["Stream routing table"] SM --> SA SM --> SB end ``` ## Wire protocol Every stream message has a fixed 16-byte LP frame header prepended to the payload: ``` [LpFrameKind: 2 bytes LE][StreamId: 8 bytes BE][MsgType: 1 byte][SequenceNum: 4 bytes BE][Reserved: 1 byte][payload ...] ``` - **LpFrameKind:** `3` (SphinxStream). Distinguishes stream traffic from other LP frame types (Opaque, Registration, Forward). - **StreamId:** random `u64` generated by the opener, used to multiplex streams. - **MsgType:** `Open` (0) or `Data` (1). - **SequenceNum:** `u32` counter, incremented per write. Used by the receiver's per-stream reorder buffer to deliver data in the correct order. - **Reserved:** must be `0x00`. There is no `Close` message type; see [Known Limitations](#known-limitations) for why. ## Stream mode Stream mode is activated lazily on the first call to `open_stream()` or `listener()`. This is a **one-way transition**: 1. The client's message receiver is handed off to a background router task 2. `stream_mode` flag is set to `true` 3. Message-based methods (`send_plain_message`, `wait_for_messages`) are disabled and return errors There is no switching back without disconnecting and creating a new client. ## Opening and accepting streams **Opening (outbound):** 1. `open_stream(recipient, surbs)` generates a random `StreamId` 2. An `Open` message is sent through the Mixnet to the recipient 3. A `MixnetStream` is returned, ready for writing and reading **Accepting (inbound):** 1. `listener.accept()` waits for an `Open` message from a remote peer 2. A `MixnetStream` is created with the opener's `sender_tag` for anonymous replies 3. The stream is ready for bidirectional I/O ## Cleanup - **On `drop`:** the stream deregisters from the routing table. No close message is sent over the wire. - **Idle timeout:** streams idle for longer than the configured timeout (default: 30 minutes) are automatically cleaned up. Configure with [`MixnetClientBuilder::with_stream_idle_timeout()`](https://docs.rs/nym-sdk/latest/nym_sdk/mixnet/struct.MixnetClientBuilder.html). ## Known limitations The Mixnet does not guarantee message ordering at the transport level, but each stream write includes a `sequence_num` in the LP frame header. The receiver maintains a per-stream reorder buffer (BTreeMap keyed by sequence number) that buffers out-of-order messages and drains them in sequence, so protocols that depend on byte ordering (HTTP, TLS, protobuf) work correctly over streams. - **Buffer cap:** 256 messages per stream. If the buffer fills (e.g. a large gap in sequence numbers), the receiver skips ahead to the lowest buffered sequence. - **Duplicates:** messages with a sequence number below the next expected are dropped. - There is no `Close` message type, since a close could race ahead of in-flight data. ## Internal details For the full implementation details (router task, `StreamMap`, `PollSender` usage, base-client type rationale), see the `ARCHITECTURE.md` file next to the module source code, or the [docs.rs](https://docs.rs/nym-sdk/latest/nym_sdk/) API reference. --- title: Stream Module Examples description: Runnable Rust examples for the Nym Stream module: bidirectional read/write, idle timeouts, mode guards, and throughput benchmarks. url: https://nym.com/docs/developers/rust/stream/examples --- # Examples Runnable examples in [`sdk/rust/nym-sdk/examples/`](https://github.com/nymtech/nym/tree/develop/sdk/rust/nym-sdk/examples). Each file is self-contained with step-by-step comments. ```bash cargo run --example ``` | Example | Source | What it demonstrates | |---|---|---| | Simple Read/Write | [`stream_simple_read_write.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/stream_simple_read_write.rs) | Multiple concurrent streams, bidirectional communication | | Idle Timeout | [`stream_idle_timeout.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/stream_idle_timeout.rs) | Configuring `with_stream_idle_timeout`, observing EOF after cleanup | | Mode Guard | [`stream_mode_guard.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/stream_mode_guard.rs) | Mutual exclusion between stream and message modes | | Throughput | [`stream_throughput.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/stream_throughput.rs) | Sending 1 MB over a single stream, verifying data integrity | --- title: Nym TcpProxy: Route TCP via the Mixnet (Deprecated) description: Route TCP traffic through the Nym mixnet using the TcpProxy Rust module. Deprecated in favour of the Stream module. url: https://nym.com/docs/developers/rust/tcpproxy --- # TcpProxy Module This module is unmaintained. The TcpProxy is no longer actively developed in favour of the [Stream module](/developers/rust/stream), which provides `AsyncRead + AsyncWrite` channels directly over the Mixnet without the localhost TCP socket layer. Existing users should plan to migrate. The module will continue to work but will not receive new features or bug fixes. `NymProxyClient` and `NymProxyServer` proxy TCP traffic through the Mixnet. Both run in a background thread and expose a configurable `localhost` socket that callers read and write to like any other TCP connection. The Stream module replaces this pattern with multiplexed channels on a single client and no localhost socket layer. > Non-Rust/Go developers can use the [standalone binaries](/developers/tools/standalone-tcpproxy) instead. ## Examples | Example | Source | |---|---| | Single connection | [`tcp_proxy_single_connection.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/tcp_proxy_single_connection.rs) | | Multiple connections | [`tcp_proxy_multistream.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/tcp_proxy_multistream.rs) | ```bash cargo run --example tcp_proxy_single_connection cargo run --example tcp_proxy_multistream ``` ## API reference [`docs.rs/nym-sdk/tcp_proxy`](https://docs.rs/nym-sdk/latest/nym_sdk/tcp_proxy/) covers types, methods, and the full client/server walkthrough. ## Architecture `NymProxyClient` uses a [Client Pool](/developers/rust/client-pool) with one client per incoming TCP connection; if the pool runs dry it falls back to creating clients on demand. `NymProxyServer` runs a single Nym client with a persistent identity. Each TCP connection is wrapped in a session ID; messages within a session carry an incrementing message ID, and a final `Close` message signals that no more outbound bytes are coming. Session and message IDs are necessary because the Mixnet guarantees delivery but not ordering, and ordering matters whenever a parser cares about frame boundaries (gRPC over protobuf, HTTP, TLS). ```rust pub struct ProxiedMessage { message: Payload, session_id: Uuid, message_id: u16, } ``` For the full request/response sequence diagram, see the [module source](https://github.com/nymtech/nym/tree/develop/sdk/rust/nym-sdk/src/tcp_proxy) or the [docs.rs](https://docs.rs/nym-sdk/latest/nym_sdk/tcp_proxy/) entry. --- title: Client Pool: Pre-Connected Mixnet Clients description: The Nym ClientPool maintains ready-to-use MixnetClient instances, eliminating connection latency for bursty traffic patterns. url: https://nym.com/docs/developers/rust/client-pool --- # Client Pool The `ClientPool` keeps a configurable number of `MixnetClient` instances pre-connected in a background loop, so callers don't pay the gateway handshake, key generation, and topology fetch cost on the hot path. ## How it works ```mermaid --- config: theme: neo-dark --- flowchart LR BG["Background loop"] -->|creates clients| P["Pool (Vec)"] P -->|"get_mixnet_client()"| APP["Your application"] APP -->|uses and disconnects| D["Done"] BG -->|"pool < reserve? create another"| P ``` 1. Create the pool with a target reserve size: `ClientPool::new(5)`. 2. Start the background loop: `pool.start()`. It immediately begins connecting clients. 3. Pop a client when needed: `pool.get_mixnet_client()` returns `Some(client)` or `None` if the pool is empty. 4. Use the client normally: send messages, open streams. 5. Disconnect the client when done. The background loop notices the pool is below reserve and creates a replacement. Clients are **consumed, not returned**. The pool creates new ones to maintain the reserve. If the pool is empty, you can fall back to `MixnetClient::connect_new()` (slower, but keeps things working). The `NymProxyClient` (TcpProxy) uses a `ClientPool` internally: one client per incoming TCP connection. ## Quick example ```rust use nym_sdk::client_pool::ClientPool; use nym_network_defaults::setup_env; #[tokio::main] async fn main() -> Result<(), Box> { nym_bin_common::logging::setup_tracing_logger(); // Load mainnet network defaults into env vars (required by ClientPool) setup_env(None::); let pool = ClientPool::new(5); // maintain 5 clients in reserve let pool_clone = pool.clone(); tokio::spawn(async move { pool_clone.start().await }); // Get a client when needed if let Some(client) = pool.get_mixnet_client().await { println!("Got client: {}", client.nym_address()); client.disconnect().await; } pool.disconnect_pool().await; Ok(()) } ``` ## Further reading - [Tutorial: Handle bursty traffic](./client-pool/tutorial): step-by-step guide covering pool creation, burst handling, and fallback logic - [API reference on docs.rs](https://docs.rs/nym-sdk/latest/nym_sdk/client_pool/): type details, method signatures, and architecture docs - [Example source on GitHub](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/client_pool.rs): complete working example --- title: Client Pool Tutorial: Handle Bursty Traffic description: Step-by-step Rust tutorial to use Nym ClientPool for handling bursts of concurrent mixnet operations without blocking on client creation. url: https://nym.com/docs/developers/rust/client-pool/tutorial --- # Tutorial: Handle Bursty Traffic with Client Pool A program that uses `ClientPool` to absorb bursts of concurrent Mixnet operations without paying client-creation latency on the hot path. The pool pre-creates clients in the background; tasks pop them under load; the tutorial also walks through what happens when demand outruns supply. ## What you'll learn - Creating and starting a `ClientPool` - Popping clients from the pool for concurrent operations - Falling back to on-demand client creation when the pool is empty - Observing pool replenishment - Graceful shutdown ## Prerequisites - Rust toolchain ({RUST_MSRV}+) - A working internet connection ## Step 1: Set up the project ```sh cargo init nym-pool-demo cd nym-pool-demo ``` Add dependencies to `Cargo.toml`: ```toml [dependencies] nym-sdk = "1.21.1" nym-network-defaults = "1.21.1" nym-bin-common = { version = "1.21.1", features = ["basic_tracing"] } tokio = { version = "1", features = ["full"] } ``` ## Step 2: Create and start the pool The pool is created with a **reserve size**: the number of connected clients it tries to maintain at all times. The `start()` method runs a background loop that creates clients whenever the pool drops below the reserve. Create `src/main.rs`: ```rust use nym_sdk::client_pool::ClientPool; use nym_sdk::mixnet::MixnetMessageSender; use nym_network_defaults::setup_env; use std::time::Duration; #[tokio::main] async fn main() { nym_bin_common::logging::setup_tracing_logger(); // Load mainnet network defaults into env vars (required by ClientPool) setup_env(None::); // Create a pool that maintains 3 clients in reserve let pool = ClientPool::new(3); // Start the pool in a background task. // It immediately begins connecting clients. let pool_bg = pool.clone(); tokio::spawn(async move { pool_bg.start().await.unwrap(); }); println!("Pool started, waiting for clients to connect..."); tokio::time::sleep(Duration::from_secs(15)).await; // Check how many are ready let count = pool.get_client_count().await; println!("Pool has {count} clients ready"); ``` Creating a `MixnetClient` takes several seconds (gateway handshake, key generation, topology fetch). The pool does this work ahead of time so your application doesn't block when it needs a client. ## Step 3: Pop clients and use them When you call `get_mixnet_client()`, the pool removes a client and returns it. The background loop notices the shortfall and starts creating a replacement. ```rust // Simulate a burst of 3 concurrent tasks, each needing a client let mut handles = vec![]; for i in 1..=3 { let pool = pool.clone(); let handle = tokio::spawn(async move { // Pop a client from the pool let mut client = match pool.get_mixnet_client().await { Some(c) => { println!("Task {i}: got client {} from pool", c.nym_address()); c } None => { // Pool is empty; fall back to creating one on the fly. // This is slower but keeps things working. println!("Task {i}: pool empty, creating client on the fly..."); nym_sdk::mixnet::MixnetClient::connect_new().await.unwrap() } }; // Do something with the client. Here, send a message to ourselves. let addr = *client.nym_address(); client .send_plain_message(addr, format!("hello from task {i}")) .await .unwrap(); // Wait for the message to arrive if let Some(msgs) = client.wait_for_messages().await { for msg in msgs { if !msg.message.is_empty() { println!( "Task {i}: received {:?}", String::from_utf8_lossy(&msg.message) ); } } } // Disconnect when done; the pool will create a replacement. client.disconnect().await; println!("Task {i}: done"); }); handles.push(handle); } // Wait for all tasks to finish for h in handles { h.await.unwrap(); } ``` ## Step 4: Observe replenishment After popping all 3 clients, the pool background loop starts creating replacements. Give it time and check: ```rust // Pool should be replenishing println!("\nWaiting for pool to replenish..."); tokio::time::sleep(Duration::from_secs(15)).await; let count = pool.get_client_count().await; println!("Pool has {count} clients ready again"); ``` ## Step 5: Shut down gracefully ```rust // Disconnect all remaining clients and stop the background loop pool.disconnect_pool().await; println!("Pool shut down"); } ``` ## Step 6: Run it ```sh RUST_LOG=info cargo run ``` You'll see output like: ``` Pool started, waiting for clients to connect... Pool has 3 clients ready Task 1: got client 8gk4Y...@2xU4d... from pool Task 2: got client F3qR7...@9nK2m... from pool Task 3: got client A7bN2...@4pL8w... from pool Task 1: received "hello from task 1" Task 2: received "hello from task 2" Task 3: received "hello from task 3" Task 1: done Task 2: done Task 3: done Waiting for pool to replenish... Pool has 3 clients ready again Pool shut down ``` ## When to use the pool The pool is most useful when: - **You have bursty traffic:** many concurrent operations that each need their own client - **Latency matters:** you can't afford the several-second delay of creating a client on each request - **You're building a service:** an API endpoint that creates a client per request would benefit from pre-warmed clients If your application only ever needs one client at a time, just use `MixnetClient::connect_new()` directly. The `NymProxyClient` (TcpProxy module) uses a `ClientPool` internally: one client per incoming TCP connection. ## What you've learned - **`ClientPool::new(n)`** creates a pool targeting `n` reserve clients - **`pool.start()`** runs a background loop that creates clients whenever the pool is below reserve - **`pool.get_mixnet_client()`** pops a client; returns `None` if the pool is empty - **Clients are consumed, not returned.** The pool automatically creates replacements - **`pool.disconnect_pool()`** shuts down all remaining clients and stops the background loop - **Fall back to on-demand creation** when the pool is empty for resilience ## Complete code ```rust use nym_sdk::client_pool::ClientPool; use nym_sdk::mixnet::MixnetMessageSender; use nym_network_defaults::setup_env; use std::time::Duration; #[tokio::main] async fn main() { nym_bin_common::logging::setup_tracing_logger(); setup_env(None::); let pool = ClientPool::new(3); let pool_bg = pool.clone(); tokio::spawn(async move { pool_bg.start().await.unwrap(); }); println!("Pool started, waiting for clients to connect..."); tokio::time::sleep(Duration::from_secs(15)).await; let count = pool.get_client_count().await; println!("Pool has {count} clients ready"); let mut handles = vec![]; for i in 1..=3 { let pool = pool.clone(); let handle = tokio::spawn(async move { let mut client = match pool.get_mixnet_client().await { Some(c) => { println!("Task {i}: got client {} from pool", c.nym_address()); c } None => { println!("Task {i}: pool empty, creating client on the fly..."); nym_sdk::mixnet::MixnetClient::connect_new().await.unwrap() } }; let addr = *client.nym_address(); client .send_plain_message(addr, format!("hello from task {i}")) .await .unwrap(); if let Some(msgs) = client.wait_for_messages().await { for msg in msgs { if !msg.message.is_empty() { println!( "Task {i}: received {:?}", String::from_utf8_lossy(&msg.message) ); } } } client.disconnect().await; println!("Task {i}: done"); }); handles.push(handle); } for h in handles { h.await.unwrap(); } println!("\nWaiting for pool to replenish..."); tokio::time::sleep(Duration::from_secs(15)).await; let count = pool.get_client_count().await; println!("Pool has {count} clients ready again"); pool.disconnect_pool().await; println!("Pool shut down"); } ``` --- title: Client Pool Examples description: Runnable Rust example for the Nym Client Pool: managing multiple MixnetClients with ephemeral fallback. url: https://nym.com/docs/developers/rust/client-pool/examples --- # Examples Runnable examples in [`sdk/rust/nym-sdk/examples/`](https://github.com/nymtech/nym/tree/develop/sdk/rust/nym-sdk/examples). Each file is self-contained with step-by-step comments. ```bash cargo run --example ``` | Example | Source | What it demonstrates | |---|---|---| | Client Pool | [`client_pool.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/client_pool.rs) | Creating a pool of `MixnetClient`s, retrieving clients from the pool, and falling back to ephemeral clients when the pool is empty | --- title: FFI Bindings: Go and C/C++ description: Use the Nym SDK from Go and C/C++ via FFI bindings. Covers mixnet messaging, anonymous replies, and TcpProxy lifecycle from non-Rust languages. url: https://nym.com/docs/developers/rust/ffi --- # FFI Bindings The SDK exposes FFI bindings for Go and C/C++. The source lives in [`sdk/ffi`](https://github.com/nymtech/nym/tree/develop/sdk/ffi): ``` ffi ├── cpp # C/C++ bindings (manual C FFI) ├── go # Go bindings (via uniffi-bindgen-go) └── shared # Shared Rust implementation ``` Core logic lives in `shared/` and is imported into language-specific wrappers. The shared layer handles thread safety and runs client operations on blocking threads on the Rust side of the FFI boundary. ## What's exposed **Mixnet** (Go and C/C++): ephemeral and persistent client creation, sending messages, anonymous replies via SURBs, listening for incoming messages. **TcpProxy** (Go only): client and server creation and lifecycle. The TcpProxy module is deprecated. For new projects, use the [Stream module](./stream) instead. **Client Pool and Stream** have no standalone FFI bindings yet. The TcpProxy bindings use the Client Pool internally. ## Quick example (Go) ```go // Initialise an ephemeral client bindings.InitEphemeral() // Get our Nym address addr, _ := bindings.GetSelfAddress() // Send a message through the Mixnet bindings.SendMessage(addr, "hello from Go") // Listen for incoming messages msg, _ := bindings.ListenForIncoming() fmt.Println("Received:", msg.Message) // Reply anonymously via SURBs bindings.Reply(msg.Sender, "reply from Go") ``` ## Quick example (C++) The C++ bindings use callbacks for return values and a `ReceivedMessage` struct for incoming data: ```cpp extern "C" { struct ReceivedMessage { const uint8_t* message; size_t size; const char* sender_tag; }; void init_logging(); char init_ephemeral(); char get_self_address(void (*callback)(const char*)); char send_message(const char*, const char*); char listen_for_incoming(void (*callback)(ReceivedMessage)); char reply(const char*, const char*); } // Get address via callback char addr[134]; void on_address(const char* s) { strcpy(addr, s); } // Receive message via callback char sender_tag[22]; void on_message(ReceivedMessage msg) { std::cout << "Received: " << msg.message << std::endl; strcpy(sender_tag, msg.sender_tag); } int main() { init_ephemeral(); get_self_address(on_address); send_message(addr, "hello from C++"); listen_for_incoming(on_message); reply(sender_tag, "reply from C++"); } ``` ## Building Each language has a `build.sh` script that compiles the Rust shared library and generates bindings. See the README in each directory for prerequisites. ## Examples and source - [Go mixnet example](https://github.com/nymtech/nym/blob/develop/sdk/ffi/go/example.go): init, send, receive, SURB reply - [Go TcpProxy example](https://github.com/nymtech/nym/blob/develop/sdk/ffi/go/proxy_example.go): proxy client and server with TCP echo - [C++ example](https://github.com/nymtech/nym/blob/develop/sdk/ffi/cpp/src/main.cpp): same flow using Boost threads - [`sdk/ffi` source](https://github.com/nymtech/nym/tree/develop/sdk/ffi): full source and build scripts --- title: Mixnet Playground description: Interactive browser playground for Nym's TypeScript packages: drive fetch, DNS, WebSocket and download traffic through a mixnet tunnel, and send end-to-end messages with the raw messaging SDK. url: https://nym.com/docs/developers/playground --- # Mixnet playground This playground runs Nym's browser TypeScript packages against the live mixnet. It covers both integration models: - **Proxy**, via the [mix-* family](/developers/mix-tunnel): bring the shared tunnel up once, then drive `fetch`, DNS, WebSocket, stress and file-download traffic through it to clearnet destinations. - **End-to-end**, via the [raw messaging SDK](/developers/typescript): Sphinx-encrypted messages between two Nym clients, with nothing exiting to the clearnet. Some sections send the same request over the tunnel and over the clearnet, so you can compare the two. On npm: [`@nymproject/mix-fetch`](https://www.npmjs.com/package/@nymproject/mix-fetch), [`@nymproject/mix-dns`](https://www.npmjs.com/package/@nymproject/mix-dns), [`@nymproject/mix-tunnel`](https://www.npmjs.com/package/@nymproject/mix-tunnel), [`@nymproject/mix-websocket`](https://www.npmjs.com/package/@nymproject/mix-websocket), and [`@nymproject/sdk`](https://www.npmjs.com/package/@nymproject/sdk). ## HTTPS / DNS / WebSockets Everything here runs client-side over the live Nym mixnet. The first `setupMixTunnel` is slow (a few seconds): it loads the WebAssembly client, registers a fresh client identity with a gateway, and discovers an IPR exit. Later calls reuse the tunnel. ## Raw mixnet messaging The sections above share one smolmix tunnel and exit to the clearnet through an IPR. The [Messaging SDK](/developers/typescript) (`@nymproject/sdk`) is the other model: end-to-end mixnet messages between two Nym clients, where you control both ends and nothing exits to the clearnet. It runs a separate wasm client, so it loads on demand: ## Source and examples - [Playground source](https://github.com/nymtech/nym/tree/develop/documentation/docs/components/playground): the React component behind this page (`MixPlayground.tsx` and `lib.ts`). - [SDK examples](https://github.com/nymtech/nym/tree/develop/sdk/typescript/examples): standalone runnable apps, including a browser example per package ([mix-fetch](https://github.com/nymtech/nym/tree/develop/sdk/typescript/examples/mix-fetch/browser), [mix-dns](https://github.com/nymtech/nym/tree/develop/sdk/typescript/examples/mix-dns/browser), [mix-websocket](https://github.com/nymtech/nym/tree/develop/sdk/typescript/examples/mix-websocket/browser)). ## Per-package docs For the API of each package, see [mix-tunnel](/developers/mix-tunnel), [mix-fetch](/developers/mix-fetch), [mix-dns](/developers/mix-dns), and [mix-websocket](/developers/mix-websocket). --- title: Demo: ENS resolution over the Nym mixnet description: Resolve an ENS name to its address and IPFS contenthash, then fetch the site, with every JSON-RPC and gateway request routed through the mixnet via mix-fetch. Shows the ethers-to-mixFetch adapter. url: https://nym.com/docs/developers/demos/ens --- export const EnsDemo = dynamic( () => import('../../../components/demos/ens/EnsDemo').then((m) => m.EnsDemo), { ssr: false }, ) # ENS over the mixnet A normal ENS lookup (name to address to IPFS website) built with [ethers.js](https://docs.ethers.org/v6/), except every network request goes through the Nym mixnet instead of leaving over your normal connection. The Ethereum RPC node and the IPFS gateway see the [IPR](/network/infrastructure/exit-services#ip-packet-router) exit's IP, not yours, and your ISP cannot see which names or sites you reach. The trade-off is latency: every packet takes a multi-relay path, so requests are slower than a direct route. ## How it works The whole integration is one adapter. ethers v6 exposes `FetchRequest.getUrlFunc` as a settable property, so you replace its HTTP transport with a function that calls [`mixFetch`](/developers/mix-fetch). To ethers it looks like an ordinary fetch; to the mixnet, ethers looks like any other caller. ```ts function buildProvider(rpcUrl: string): JsonRpcProvider { const base = new FetchRequest(rpcUrl); // Route every JSON-RPC call through mixFetch, renaming the response // fields ethers expects (status -> statusCode, statusText -> statusMessage). base.getUrlFunc = async (req) => { const res = await mixFetch(req.url, { method: req.method, headers: req.headers, // ethers' FetchRequest.body is Uint8Array | null; cast for RequestInit. body: (req.body ?? undefined) as BodyInit | undefined, }); return { statusCode: res.status, statusMessage: res.statusText, headers: Object.fromEntries(res.headers), body: new Uint8Array(await res.arrayBuffer()), }; }; // staticNetwork skips the eth_chainId probe: one mixnet round trip saved. return new JsonRpcProvider(base, 'mainnet', { staticNetwork: true }); } ``` One caveat: browser `fetch` decompresses gzip transparently and `mixFetch` does not, so the demo adds a `DecompressionStream` step after each response (Cloudflare gzips RPC replies). The full version with decompression and per-call logging is in [`components/demos/ens/lib.ts`](https://github.com/nymtech/nym/tree/develop/documentation/docs/components/demos/ens). On npm: [`@nymproject/mix-fetch`](https://www.npmjs.com/package/@nymproject/mix-fetch) and [`ethers`](https://www.npmjs.com/package/ethers). The lookup itself is three steps, each an Ethereum call or HTTPS GET over the same tunnel: 1. **Resolve address.** `provider.resolveName(name)` under the hood reads the ENS Registry's `resolver(node)` to find the resolver contract, then calls `resolver.addr(node)` for the Ethereum address (ethers may add an EIP-165 `supportsInterface` probe in between). `node` is the namehash (recursive keccak256 over the labels). 2. **Get contenthash.** One more `eth_call`: `resolver.contenthash(node)`. ethers decodes the EIP-1577 multicodec bytes to a URI; this demo handles `ipfs://`. 3. **Fetch from IPFS.** A plain HTTPS GET to a gateway with the CID as a subdomain or path label. CIDv0 (`Qm...`) is re-encoded as CIDv1 (`bafy...`) for subdomain gateways, since DNS is case-insensitive. ## Try it Connect to bring the tunnel up (a default IPR exit is pinned; tick **Use random IPR** for auto-discovery), click **Verify IP routing** to confirm traffic exits through Nym, then run the three steps. ## What to expect - **The first request is the slow one.** Connecting builds the mixnet client and handshakes with the IPR; no TCP or TLS yet. The first request to a host then runs a TCP and TLS handshake carried as IP packets over the mixnet (several sequential round trips). smolmix keeps that connection warm and reuses it, so later requests to the same host are much quicker. A long pause is handshakes in flight, not a hang. - **You will not see the tunnelled requests in DevTools.** The RPC and IPFS requests never touch the browser's `fetch`. They leave the worker as encrypted packets over a single WebSocket to the entry gateway, which is the one connection the Network tab shows. The exception is **Verify IP routing**, which deliberately makes one direct clearnet call to ipinfo.io for comparison. - **Rate limiting.** Public IPFS gateways and Ethereum RPCs rate-limit shared IP addresses. If requests start failing with 403, 429, or connection errors, the exit IP is likely flagged: tick **Use random IPR** and reload for a fresh exit. ## Glossary --- title: Demo: Shielding testnet ETH into Railgun over the Nym mixnet description: Shield testnet ETH into a Railgun private note with every Ethereum RPC call routed through the Nym mixnet. Shows the global ethers-to-mixFetch routing that covers a whole SDK. url: https://nym.com/docs/developers/demos/railgun --- export const RailgunDemo = dynamic( () => import('../../../components/demos/railgun/RailgunDemo').then((m) => m.RailgunDemo), { ssr: false }, ) # Shielding testnet ETH into Railgun over the mixnet **Nym** hides the network layer: every Ethereum RPC call goes through the mixnet via [`mixFetch`](/developers/mix-fetch), so the RPC node and your ISP cannot link you to the query, and **Railgun** hides the application layer, as shielded notes break the on-chain link between sender, receiver, and amount. This demo covers just the **shield** step on Sepolia: depositing testnet ETH into a private note. It does not do private transfers or unshielding. ## What you can do here This page is interactive. You bring up a mixnet tunnel, derive a Railgun wallet, and broadcast a **real shield transaction** on the Sepolia testnet, with every Ethereum RPC call routed through the mixnet. The shield lands on chain (you can open it on Etherscan), but the IP that submitted it is the Nym exit's, not yours. The network-routing piece is a single ethers shim (shown below). Because the Railgun engine talks to the chain through the `ethers` library, routing every ethers call through `mixFetch` is enough to put the whole SDK's traffic behind the mixnet, and the same routing pattern drops into any `ethers`-based app. Railgun itself needs a few extra engine-config workarounds (disabling the POI gate and stubbing quick-sync), which live in [`components/demos/railgun/lib.ts`](https://github.com/nymtech/nym/tree/develop/documentation/docs/components/demos/railgun), not in the shim. ## How it works The [ENS demo](/developers/demos/ens) swapped one provider's transport, but Railgun constructs its own providers internally, so routing only our provider would leak the engine's RPC to clearnet. Instead this demo installs a **global** `ethers` transport: `FetchRequest.registerGetUrl` routes every ethers HTTP call in the page through `mixFetch`, including the ones the Railgun engine makes. ```ts // Every ethers HTTP request in the process now goes through the mixnet. FetchRequest.registerGetUrl(async (req) => { const res = await mixFetch(req.url, { method: req.method, headers: req.headers, // ethers' FetchRequest.body is Uint8Array | null; cast for RequestInit. body: (req.body ?? undefined) as BodyInit | undefined, }); return { statusCode: res.status, statusMessage: res.statusText, headers: Object.fromEntries(res.headers), body: new Uint8Array(await res.arrayBuffer()), }; }); ``` `registerGetUrl` is global static state on the `FetchRequest` class, so this only works if `ethers` is a **single instance** across your bundle. If your app and Railgun resolve to different ethers copies, the handler installs on one and the engine uses the other. Pin the exact version Railgun peer-depends on. The block above is the shape of the shim. One caveat the real version adds: browser `fetch` decompresses gzip transparently and `mixFetch` does not, so the demo runs a `DecompressionStream` over each response before handing it back to ethers. The full handler with decompression is in [`components/demos/shared/mixfetch.ts`](https://github.com/nymtech/nym/tree/develop/documentation/docs/components/demos/shared/mixfetch.ts). On npm: [`@nymproject/mix-fetch`](https://www.npmjs.com/package/@nymproject/mix-fetch), [`@railgun-community/wallet`](https://www.npmjs.com/package/@railgun-community/wallet), and [`ethers`](https://www.npmjs.com/package/ethers). Shielding is a four-step flow, all over the mixnet: sign a shield key, estimate gas, populate the transaction, then sign and broadcast. The broadcast that lands on Sepolia is observable on Etherscan, but the IP that submitted it stays hidden. ## Try it The demo auto-loads a funded Sepolia testnet wallet. Connect the tunnel (the Railgun address derives once the engine is up), check the balance, then shield a small amount. If the wallet is low, top it up at a [Sepolia faucet](https://sepoliafaucet.com/) using the public address shown. **Sepolia testnet only.** The wallet holds test ETH and the mnemonic is stored in plain browser storage. Never paste a mainnet mnemonic. ## What to expect - **Engine init is the slow part.** `loadProvider`'s first RPC calls hit Sepolia over a cold mixnet route, paying TCP-connect and TLS-handshake time, so the ethers request can time out on the first try; the demo retries and the second attempt finds the connection pool warm. - **Shielding makes several RPC calls** (gas estimate, fee data, broadcast, receipt), each a mixnet round trip. The broadcast step retries idempotently: the tx hash is fixed before broadcasting, so a dropped response can be re-sent or detected as already-on-chain. - **Rate limiting.** If RPC calls start failing with 403/429 or connection errors, the exit IP is flagged: disconnect, tick **Use random IPR**, reload, and reconnect for a fresh exit. ## Glossary --- title: mix-tunnel: Shared Mixnet Tunnel for the Browser description: TypeScript package that owns the shared Nym mixnet tunnel in the browser. The base layer for mix-fetch, mix-dns, and mix-websocket. url: https://nym.com/docs/developers/mix-tunnel --- # mix-tunnel [`@nymproject/mix-tunnel`](https://www.npmjs.com/package/@nymproject/mix-tunnel) owns a single mixnet tunnel in the browser and exposes it to the feature packages built on it: [`mix-fetch`](/developers/mix-fetch), [`mix-dns`](/developers/mix-dns), and [`mix-websocket`](/developers/mix-websocket). All three call into the same tunnel, so they share one IPR connection, one userspace TCP/IP stack ([`smoltcp`](https://docs.rs/smoltcp)), and one DNS cache. The tunnel lives in a Web Worker and is built on [smolmix-wasm](https://github.com/nymtech/nym/tree/develop/wasm/smolmix), the WebAssembly build of the [Rust `smolmix` crate](/developers/smolmix). One WASM instance per page, regardless of how many feature packages you import. See [mix-* architecture](/developers/mix-architecture) for how the worker, the Comlink boundary, and the smoltcp + rustls stack fit together. ## When to use it directly Most apps don't import `mix-tunnel` directly. The feature packages re-export `setupMixTunnel`, `disconnectMixTunnel`, and `getTunnelState`, so calling `setupMixTunnel()` from `mix-fetch` brings the tunnel up for all three. Use `mix-tunnel` directly when you want to: - Configure the tunnel once at app startup, before any feature package is loaded. - Inspect tunnel state from UI code that isn't tied to a specific feature package. - Tear down the tunnel explicitly, for example before a page unload. ## In this section - [Get started](/developers/mix-tunnel/get-started): install and bring the tunnel up. - [Reference](/developers/mix-tunnel/guides): configuration and reading tunnel state. - [Architecture](/developers/mix-architecture): how the shared tunnel and Web Worker are wired. - [Exit security](/developers/concepts/exit-security): what the IPR exit can see. - [TypeDoc reference](/developers/mix-tunnel/api/globals): generated from the source. - [Examples](https://github.com/nymtech/nym/tree/develop/sdk/typescript/examples): the mix-fetch, mix-dns, and mix-websocket example apps all bring the tunnel up through this package. --- title: Get started with mix-tunnel description: Install @nymproject/mix-tunnel and bring up the shared mixnet tunnel. url: https://nym.com/docs/developers/mix-tunnel/get-started --- # Get started ## Installation ```bash npm install @nymproject/mix-tunnel ``` The package ships ESM only. The bundled Web Worker and smolmix-wasm are inlined, so no separate WASM-loading config is needed in Webpack, Vite, or esbuild. The smolmix-family packages (`mix-tunnel`, `mix-fetch`, `mix-dns`, `mix-websocket`) are new and version together. The API is still settling: pin exact versions and expect breaking changes between releases until the family reaches a stable line. `mix-fetch` already went through a [v1 to v2 clean break](/developers/mix-fetch/migration). ## Quick start ```ts // Brings the tunnel up. Call once per page; a second call rejects. await setupMixTunnel(); // { state: 'ready' } once the IPR handshake completes. console.log(await getTunnelState()); // Tear down before page unload. The WASM is unusable after this until reload. window.addEventListener('beforeunload', () => { disconnectMixTunnel(); }); ``` After `setupMixTunnel()` resolves, [`mixFetch()`](/developers/mix-fetch), [`mixDNS()`](/developers/mix-dns), and `new MixWebSocket()` (from [`mix-websocket`](/developers/mix-websocket)) all become usable. Bring the tunnel up live in the [mixnet playground](/developers/playground) and inspect its state over the live mixnet. --- title: mix-tunnel guides description: Configure the shared mixnet tunnel and read its connection state. url: https://nym.com/docs/developers/mix-tunnel/guides --- # Reference ## Configuration `setupMixTunnel(opts)` accepts the full smolmix-wasm `SetupOpts` surface plus one TS-layer addition (`debug`). Pass any subset; every field has a default (listed in [`SetupMixTunnelOpts`](/developers/mix-tunnel/api/interfaces/SetupMixTunnelOpts)). ```ts await setupMixTunnel({ // Pin a specific IPR (otherwise auto-discovered from the topology). preferredIpr: 'D1rrUqJY9pesL3pTaMaxLnpZGGYQ4ZpZwpQXCqaeBXTW.6PpFkRvF...', // Cover traffic and Poisson timing are ON by default. Setting these disables // them for lower latency and bandwidth, at the cost of traffic-analysis // resistance (it drops timing-correlation resistance at the entry and exit). disableCoverTraffic: true, disablePoissonTraffic: true, // Verbose console tracing from smolmix-wasm. Useful while integrating. debug: true, }); ``` The complete option surface (IPR pinning, SURB budgets, DNS server overrides, TCP/connect timeouts, gateway selection) is documented in the [`SetupMixTunnelOpts` type reference](/developers/mix-tunnel/api/interfaces/SetupMixTunnelOpts). Call `setupMixTunnel` once. The WASM tunnel is one-shot per page: the first call brings it up, and a second call rejects with `tunnel already initialised`. Because the feature packages share that one tunnel, calling it from `mix-fetch` is enough for `mix-dns` and `mix-websocket` too. If more than one call site might invoke it, guard with `getTunnelState()` and skip setup when the state is already `ready`. ## Tunnel state `getTunnelState()` returns a tagged state for surfacing connection status in the UI. The five states are: | State | Meaning | |---|---| | `connecting` | Default before `setupMixTunnel()` completes. | | `ready` | Tunnel is up. Feature packages can issue requests. | | `shutting_down` | `disconnectMixTunnel()` in progress. | | `shutdown` | Cleanly torn down. The WASM is no longer usable. | | `failed` | Setup or runtime error. The `reason` field carries a human-readable cause. | ```ts const { state, reason } = await getTunnelState(); if (state === 'failed') { console.error('Tunnel failed:', reason); } ``` `await setupMixTunnel()` resolves only once the tunnel is `ready`, so most apps never need to read the state. Poll `getTunnelState()` only to drive a separate status indicator. The transitions are coarse (no progress percentage during `connecting`); the underlying connection events are visible via `debug: true` in the console. --- title: mix-fetch: fetch() Over the Nym Mixnet description: Drop-in fetch() replacement that routes HTTP and HTTPS requests through the Nym mixnet via an IPR exit gateway. url: https://nym.com/docs/developers/mix-fetch --- # mix-fetch [`@nymproject/mix-fetch`](https://www.npmjs.com/package/@nymproject/mix-fetch) routes HTTP and HTTPS through the Nym mixnet behind the browser [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) signature: `mixFetch(url, init)` returns the same `Response` you would get from `fetch(url, init)`. The request travels mixnet hops first, exits at an [IPR (Internet Packet Router)](/network/infrastructure/exit-services#ip-packet-router) gateway, and reaches the destination with the IPR's IP, not yours. It is not a perfect substitute for `fetch`: no cookies or credentials, no HTTP cache, no `AbortController`, and HTTPS-only in practice (plain HTTP is fully visible at the exit; see [drop-in caveats](/developers/mix-fetch/guides#drop-in-caveats)). ```text ┌────────────────────────────────────────────────────────────────────┐ │ Your browser app │ │ └─ mixFetch('https://...') │ │ └─ mix-tunnel (shared singleton, Web Worker, smolmix-wasm) │ │ └─ smoltcp userspace TCP/IP + rustls TLS │ │ └─ WebSocket to entry gateway │ │ └─ Nym mixnet (3 mix layers) │ │ └─ IPR exit gateway → destination │ └────────────────────────────────────────────────────────────────────┘ ``` TLS terminates end-to-end between the WASM bundle and the destination server. The IPR sees destination IP and port; for HTTPS targets, payload is TLS ciphertext. ## In this section - [Get started](/developers/mix-fetch/get-started): install and make your first mixnet request. - [Reference](/developers/mix-fetch/guides): request shape, default headers, drop-in caveats, configuration. - [Concepts & security](/developers/mix-fetch/concepts): what the IPR exit sees. - [Migrating from v1.x](/developers/mix-fetch/migration): the v1 to v2 clean break. - [TypeDoc reference](/developers/mix-fetch/api/globals): generated from the source. - [Browser example](https://github.com/nymtech/nym/tree/develop/sdk/typescript/examples/mix-fetch/browser): a runnable example app. --- title: Get started with mix-fetch description: Install @nymproject/mix-fetch and make your first HTTP request through the Nym mixnet. url: https://nym.com/docs/developers/mix-fetch/get-started --- # Get started ## Installation ```bash npm install @nymproject/mix-fetch ``` ESM only, with the worker and WASM inlined via [`mix-tunnel`](/developers/mix-tunnel/get-started#installation); no bundler config needed. ## Quick start ```ts // Bring the shared mixnet tunnel up. Same call works from mix-dns and mix-websocket. await setupMixTunnel(); // Drop-in fetch. The Response is the real DOM Response, not a wrapper. const res = await mixFetch('https://example.com'); console.log(res.status, await res.text()); // Tear down. The WASM is unusable after this until page reload. await disconnectMixTunnel(); ``` For one-shot use without an explicit setup step, the `createMixFetch` helper combines setup and fetch: ```ts const mixFetch = await createMixFetch({ disableCoverTraffic: true }); const res = await mixFetch('https://example.com'); ``` Call `createMixFetch` once and reuse the function it returns. It calls `setupMixTunnel` internally, so calling `createMixFetch` a second time rejects with `tunnel already initialised`; the tunnel is [one-shot per page](/developers/mix-tunnel/get-started). Run `mixFetch` live in the [mixnet playground](/developers/playground), with a tunnel-vs-clearnet comparison. --- title: mix-fetch guides description: Request shape, default headers, drop-in caveats, and tunnel configuration for mix-fetch. url: https://nym.com/docs/developers/mix-fetch/guides --- # Reference ## Request shape The `init` argument is the standard [`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit). Headers, method, and body all work. `AbortController` (`signal`) is not supported: an in-flight request cannot be cancelled. ```ts const res = await mixFetch('https://httpbin.org/post', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ hello: 'mixnet' }), }); console.log(await res.json()); ``` Binary responses come back via the standard `Response.arrayBuffer()` / `Response.blob()` methods: ```ts const res = await mixFetch('https://example.com/image.png'); const blob = await res.blob(); ``` Repeated headers (`Set-Cookie`, `Vary`, `Link`, `WWW-Authenticate`) are preserved. The wasm side returns headers as a `[name, value]` pair sequence, which `Headers` reconstructs verbatim. ## Default request headers When the caller doesn't set them, `mixFetch` injects four browser-shape headers before the request leaves the tunnel. The shim exists because many CDNs (cloudflare's bot management) and host policies (wikimedia's User-Agent policy) reject requests that look unlike a real browser. Caller-supplied values always win. | Header | Injected default | |--------|------------------| | `User-Agent` | `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36` | | `Accept` | `text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8` | | `Accept-Language` | `en-US,en;q=0.9` | | `Accept-Encoding` | `identity` | `Accept-Encoding` is forced to `identity` rather than `gzip, deflate, br` because the wasm build has no decompressor. Advertising compression would let the server return a compressed body the wasm build cannot decode, so `Response.text()` or `.json()` would see raw compressed bytes. Responses therefore arrive uncompressed, so large text or JSON bodies transfer more bytes over the slower mixnet path. To override any of these, set the header in the `init.headers` bag like normal: ```ts const res = await mixFetch('https://example.com', { headers: { 'User-Agent': 'my-app/1.0' }, }); ``` The shim does not attempt full browser impersonation. TLS fingerprint (JA3), HTTP/2, and header ordering are still distinguishable from a real Chrome request. If you need stronger blend-in, you'll need to handle that at the application or destination layer. ## Drop-in caveats `mixFetch` matches the `fetch()` call signature but is not a perfect substitute. The differences are intentional and follow from running outside the browser's networking stack: | Difference | What it means | What to do | |---|---|---| | **No same-origin restriction** | Requests aren't subject to browser CORS preflight. The IPR honours its exit policy regardless of `Origin`. | Don't rely on CORS as an access-control mechanism for `mixFetch` requests; treat them as you would server-to-server calls. | | **No cookies / credentials** | The browser cookie jar is not shared with the WASM instance. `credentials: 'include'` has no effect. | Pass auth tokens via `Authorization` or other explicit headers. | | **No HTTP cache** | The browser HTTP cache is not consulted. Every call hits the network. | Cache responses at the application layer if needed. | | **No service-worker interception** | Requests don't pass through any `fetch` event handlers registered by service workers. | n/a | | **HTTPS only in practice** | The IPR sees plaintext HTTP in full. | Always target `https://` URLs. | ## Errors `mixFetch` follows `fetch` semantics for HTTP status: a 4xx or 5xx response **resolves** with a `Response` carrying that status, so check `response.ok` or `response.status` yourself. The promise **rejects** only on a transport-level failure: a connection or TLS failure, a DNS failure, or the IPR refusing the destination under its exit policy. A rejection is a plain `Error` whose message describes the cause; there is no typed error class, so match on the message if you need to branch. ## Timeouts and cancellation There is no per-request timeout, and `AbortController` / `signal` is ignored: an in-flight `mixFetch` cannot be cancelled. To bound how long you wait, race it against a timer. This stops you waiting but does not cancel the underlying request: ```ts const res = await Promise.race([ mixFetch(url), new Promise((_, reject) => setTimeout(() => reject(new Error('mixFetch timeout')), 30_000), ), ]); ``` Connection and DNS timeouts at the tunnel level are set once via `connectTimeoutMs` and `dnsTimeoutMs` in [`SetupMixTunnelOpts`](/developers/mix-tunnel/api/interfaces/SetupMixTunnelOpts). ## Configuration `setupMixTunnel(opts)` (and `createMixFetch(opts)`) accept the shared tunnel options from [`@nymproject/mix-tunnel`](/developers/mix-tunnel/guides#configuration). The most commonly touched are: ```ts await setupMixTunnel({ // Pin a specific IPR (otherwise auto-discovered from the topology). preferredIpr: 'D1rrUqJY9pesL3pTaMaxLnpZGGYQ4ZpZwpQXCqaeBXTW.6PpFkRvF...', // Lower latency and bandwidth at the cost of traffic-analysis resistance. disableCoverTraffic: true, disablePoissonTraffic: true, }); ``` The full option surface is documented under [`SetupMixTunnelOpts`](/developers/mix-tunnel/api/interfaces/SetupMixTunnelOpts). The first `mixFetch` call after `setupMixTunnel()` may take a few seconds: gateway handshake, IPR discovery, and the first DNS resolution all happen on demand. Subsequent calls reuse the tunnel and complete in roughly the time of a normal HTTPS request plus mixnet latency. --- title: mix-fetch concepts & security description: What the IPR exit sees when you route HTTP through mix-fetch, and what TLS keeps private. url: https://nym.com/docs/developers/mix-fetch/concepts --- # Concepts & security ## Security model `mix-fetch` follows the shared [mixnet exit security model](/developers/concepts/exit-security): the IPR exit sees your destination, and you rely on TLS to keep the payload as ciphertext to it. What that means specifically for HTTP/S: | At the IPR exit | What's visible | |---|---| | HTTPS (`https://`) | Destination IP and port. Payload is TLS ciphertext, terminating at the destination rather than the IPR. | | HTTP (`http://`) | Destination IP and port, plus the full request and response in plaintext. | TLS terminates inside the WASM instance (via [`rustls`](https://docs.rs/rustls) in smolmix-wasm), not in the browser. The Mozilla CA bundle is compiled into the WASM. Mixed content rules still apply at the page level, so serve your app over HTTPS. --- title: mix-fetch: Migrating from v1.x to v2 description: What changed between mix-fetch v1 and v2: removed packages, the new setup options, dropped request-init flags, and the underlying architecture change to a single WASM module exiting via an IPR. url: https://nym.com/docs/developers/mix-fetch/migration --- # Migrating from v1.x v2 is a clean break. The package no longer ships a Go-WASM HTTP/TLS client or a SOCKS5-shaped Network Requester request; it routes through the shared [`mix-tunnel`](/developers/mix-tunnel) and exits at an IPR. If you're starting fresh, you don't need this page; see [mix-fetch](/developers/mix-fetch). ## Removed packages The five-variant publish (`@nymproject/mix-fetch`, `mix-fetch-full-fat`, `mix-fetch-commonjs`, `mix-fetch-commonjs-full-fat`, `mix-fetch-node-commonjs`) is gone. There is one package, ESM only: | v1.x | v2.0 | |---|---| | `@nymproject/mix-fetch` (ESM) | `@nymproject/mix-fetch` | | `@nymproject/mix-fetch-full-fat` | `@nymproject/mix-fetch` (always inlined) | | `@nymproject/mix-fetch-commonjs` | (no CJS build) | | `@nymproject/mix-fetch-commonjs-full-fat` | (no CJS build) | | `@nymproject/mix-fetch-node-commonjs` | (no Node build) | The single v2 package is ESM-only and always inlines its wasm (the v1 `full-fat` behaviour is now the default), so there is no separate bundler-config step. CommonJS and Node builds are not shipped. The Node build in particular is not just a packaging variant: the mixnet transport runs on browser globals (`WebSocket`, Web Worker) inside the wasm, so a Node target would need a runtime-compatibility layer, not a CommonJS rebuild. If you need a CJS or Node variant, open an issue describing the use case. ## Removed options The v1 setup options bag covered Network Requester selection and gateway tuning. v2 replaces it with the smolmix [`SetupMixTunnelOpts`](/developers/mix-tunnel/api/interfaces/SetupMixTunnelOpts) surface: | v1 option | v2 equivalent | |---|---| | `clientId` | retained as `clientId` in `SetupMixTunnelOpts` (defaults to `smolmix-wasm` if unset) | | `preferredGateway` | (replaced by `preferredIpr`, the exit-side pin) | | `preferredNetworkRequester` | (no replacement: v2 exits via IPR, not Network Requester) | | `mixFetchOverride.requestTimeoutMs` | (no replacement at TS layer: surface via smolmix-wasm config if needed) | | `forceTls: true` | retained as `forceTls` in `SetupMixTunnelOpts` (defaults to WSS; you rarely need to set it) | | `extra.hiddenGateways` | (no replacement) | ## Removed request-init flags The v1 `mode: 'unsafe-ignore-cors'` flag is gone. v2 doesn't perform browser-side CORS checks at all (see [Drop-in caveats](/developers/mix-fetch/guides#drop-in-caveats)), so the flag has no meaning. ## New setup ```ts // v1 const mixFetch = await createMixFetch({ clientId: 'my-app', preferredGateway: 'q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1', mixFetchOverride: { requestTimeoutMs: 60_000 }, forceTls: true, }); // v2 const mixFetch = await createMixFetch({ // Optional: pin an exit IPR instead of an entry gateway. // preferredIpr: '...', }); ``` ## Architectural change v1 ran two WASM modules: a Go module with `crypto/tls` + Mozilla CA bundle, and a Rust module handling Nym's `Socks5Request` framing to a Network Requester exit. v2 runs one WASM module ([smolmix-wasm](https://github.com/nymtech/nym/tree/develop/wasm/smolmix)) with a userspace TCP/IP stack ([smoltcp](https://docs.rs/smoltcp)) and `rustls` for TLS. The exit is an IPR, not a Network Requester. Consequences: - **One WASM module, smaller bundle.** v1's Go runtime accounted for ~6 MB of the full-fat bundle; v2 drops it. - **Shared infrastructure with `mix-dns` and `mix-websocket`.** The same tunnel handles all three. - **IPR exit policies apply.** What was allowed by your previous Network Requester may not be allowed by your default IPR, which applies the current [Nym exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt). --- title: mix-dns: Hostname Resolution Over the Nym Mixnet description: TypeScript package that resolves hostnames through the Nym mixnet as UDP DNS via an IPR exit gateway. url: https://nym.com/docs/developers/mix-dns --- # mix-dns [`@nymproject/mix-dns`](https://www.npmjs.com/package/@nymproject/mix-dns) resolves hostnames to IPs through the Nym mixnet. The query travels as a UDP datagram to a public resolver via the [IPR (Internet Packet Router)](/network/infrastructure/exit-services#ip-packet-router) exit gateway, not through the browser or OS resolver. ```text ┌──────────────────────────────────────────────────────────────┐ │ Your browser app │ │ └─ mixDNS('example.com') │ │ └─ mix-tunnel (smolmix-wasm, Web Worker) │ │ └─ UDP datagram via the IPR │ │ └─ public resolver (default 8.8.8.8:53) │ └──────────────────────────────────────────────────────────────┘ ``` The resolver sees a query from the IPR's IP, not yours, and the browser's own resolver path (the OS stub resolver, any local DoH) is bypassed entirely. ## When to use it `mix-dns` is for cases where you need the resolved IP itself, not a connection that uses it. [`mix-fetch`](/developers/mix-fetch) and [`mix-websocket`](/developers/mix-websocket) already resolve via the mixnet internally; you don't need to call `mixDNS` before either. Direct uses: - Validate that a hostname resolves to an expected IP range before connecting through any path. - Build IP-based allow / deny lists for an app that performs the connection itself. - Probe whether a hostname is reachable from the IPR exit's perspective without opening a connection. ## In this section - [Get started](/developers/mix-dns/get-started): install and resolve your first hostname. - [Reference & security](/developers/mix-dns/guides): configure the resolver, and what it sees. - [TypeDoc reference](/developers/mix-dns/api/globals): generated from the source. - [Browser example](https://github.com/nymtech/nym/tree/develop/sdk/typescript/examples/mix-dns/browser): a runnable example app. --- title: Get started with mix-dns description: Install @nymproject/mix-dns and resolve a hostname through the Nym mixnet. url: https://nym.com/docs/developers/mix-dns/get-started --- # Get started ## Installation ```bash npm install @nymproject/mix-dns ``` ESM only, with the worker and WASM inlined via [`mix-tunnel`](/developers/mix-tunnel/get-started#installation); no bundler config needed. ## Quick start ```ts await setupMixTunnel(); const ip = await mixDNS('example.com'); console.log(ip); // e.g. an IPv4 address string await disconnectMixTunnel(); ``` `mixDNS` returns the first resolved address as a string: an IPv4 A record when available, otherwise IPv6. It rejects if the hostname cannot be resolved. To resolve and immediately use the result via `mixFetch`, the simpler path is to skip `mixDNS` entirely and call `mixFetch('https://example.com')`, which handles resolution itself. Resolve hostnames live in the [mixnet playground](/developers/playground), with a tunnel-vs-clearnet (DoH) comparison. --- title: mix-dns reference & security description: Configure the DNS resolver used by mix-dns, and what the resolver sees through the IPR exit. url: https://nym.com/docs/developers/mix-dns/guides --- # Reference & security ## Configuration The DNS resolver is configured at tunnel setup, not per-call. Pass the resolver in `setupMixTunnel`: ```ts await setupMixTunnel({ // Set the resolver explicitly. Defaults are 8.8.8.8:53 primary and // 1.1.1.1:53 fallback. Both fields take a `host:port` socket address; // fallbackDns is used if the primary fails to respond. primaryDns: '8.8.8.8:53', fallbackDns: '1.1.1.1:53', }); ``` The full options surface is documented under [`SetupMixTunnelOpts`](/developers/mix-tunnel/api/interfaces/SetupMixTunnelOpts). ## Security model `mix-dns` follows the shared [mixnet exit security model](/developers/concepts/exit-security). The transport-specific exposure: at the IPR exit the query leaves as a plain UDP DNS request to the resolver, so the resolver sees the queried hostname and the IPR's IP, never yours. There is no TLS to terminate; the query and response are plaintext on the IPR-to-resolver leg. At the resolver the query is plaintext UDP. The resolver can read the hostname you are looking up, while the mixnet keeps it from learning who you are. Choosing `8.8.8.8` vs `1.1.1.1` only changes which third party sees the queries; both see them coming from the IPR. To remove the resolver from your trust set, pick one you already trust, or layer DNS-over-HTTPS via `mixFetch` to a DoH endpoint instead of `mixDNS`. --- title: mix-websocket: WebSocket Over the Nym Mixnet description: TypeScript package that exposes a WebSocket-like class for WS and WSS traffic routed through the Nym mixnet. url: https://nym.com/docs/developers/mix-websocket --- # mix-websocket [`@nymproject/mix-websocket`](https://www.npmjs.com/package/@nymproject/mix-websocket) exposes `MixWebSocket`, a class that mirrors the browser [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) API for traffic that needs to travel through the Nym mixnet. WS and WSS endpoints are reached via the [IPR (Internet Packet Router)](/network/infrastructure/exit-services#ip-packet-router) exit gateway; the destination server sees the connection coming from the IPR's IP, not yours. ```text ┌──────────────────────────────────────────────────────────────┐ │ Your browser app │ │ └─ new MixWebSocket('wss://...') │ │ └─ mix-tunnel (smolmix-wasm, Web Worker) │ │ └─ smoltcp userspace TCP/IP + rustls TLS │ │ └─ Nym mixnet → IPR exit gateway │ │ └─ destination WS server │ └──────────────────────────────────────────────────────────────┘ ``` The TLS handshake (for `wss://` targets) terminates inside the WASM bundle, end-to-end with the destination. The IPR sees TCP frames addressed to the destination's IP and port; for WSS, the payload is TLS ciphertext. ## In this section - [Get started](/developers/mix-websocket/get-started): install and open your first mixnet WebSocket. - [Reference](/developers/mix-websocket/guides): sending and receiving, error handling, configuration. - [Concepts & security](/developers/mix-websocket/concepts): how it differs from the browser WebSocket, and the security model. - [TypeDoc reference](/developers/mix-websocket/api/globals): generated from the source. - [Browser example](https://github.com/nymtech/nym/tree/develop/sdk/typescript/examples/mix-websocket/browser): a runnable example app. --- title: Get started with mix-websocket description: Install @nymproject/mix-websocket and open a WebSocket through the Nym mixnet. url: https://nym.com/docs/developers/mix-websocket/get-started --- # Get started ## Installation ```bash npm install @nymproject/mix-websocket ``` ESM only, with the worker and WASM inlined via [`mix-tunnel`](/developers/mix-tunnel/get-started#installation); no bundler config needed. ## Quick start Echo against `wss://echo.websocket.org`, the same endpoint the smolmix dev tool uses: ```ts await setupMixTunnel(); const ws = new MixWebSocket('wss://echo.websocket.org'); ws.addEventListener('open', () => { console.log('connected'); ws.send('hello mixnet'); }); ws.addEventListener('message', (e) => { console.log('received:', e.data); ws.close(); }); ws.addEventListener('close', () => console.log('closed')); ws.addEventListener('error', (e) => console.error('error:', e)); ``` Or `await` on the upgrade instead of subscribing to the `open` event: ```ts const ws = new MixWebSocket('wss://echo.websocket.org'); await ws.opened(); ws.send('hello mixnet'); ``` Open a `MixWebSocket` live in the [mixnet playground](/developers/playground), which echoes messages and runs an echo burst over the live mixnet. --- title: mix-websocket guides description: Send and receive frames, handle errors, and configure the tunnel for mix-websocket. url: https://nym.com/docs/developers/mix-websocket/guides --- # Reference ## Sending and receiving ```ts ws.addEventListener('message', (e) => { if (typeof e.data === 'string') { console.log('text frame:', e.data); } else { // ArrayBuffer for binary frames console.log('binary frame:', e.data.byteLength, 'bytes'); } }); // Send a text frame await ws.send('hello'); // Send a binary frame await ws.send(new Uint8Array([0x01, 0x02, 0x03])); ``` `send()` rejects if called when `readyState` is not `OPEN`. Use `await ws.opened()` to gate the first send, and check `ws.readyState` if you keep references around for later use. ## Error handling The `error` event carries a non-standard `.message` field with the underlying cause (the standard `Event` carries no payload). Use it for diagnostics: ```ts ws.addEventListener('error', (e) => { const msg = (e as Event & { message?: string }).message; console.error('mix-websocket failure:', msg); }); ``` If the upgrade fails before `open` fires, `MixWebSocket` transitions to `CLOSED` and dispatches `error`. `opened()` resolves either on `open` or on the failure event, so callers don't hang. ## Configuration `MixWebSocket` has no per-instance configuration beyond `url` and `protocols`. Tunnel-level options live on `setupMixTunnel`, the same shared call that mix-fetch and mix-dns use: ```ts await setupMixTunnel({ preferredIpr: '...', disableCoverTraffic: true, }); ``` See [`SetupMixTunnelOpts`](/developers/mix-tunnel/api/interfaces/SetupMixTunnelOpts) for the full surface. --- title: mix-websocket concepts & security description: How MixWebSocket differs from the browser WebSocket, and what the IPR exit sees. url: https://nym.com/docs/developers/mix-websocket/concepts --- # Concepts & security ## Differences from the browser WebSocket `MixWebSocket` extends `EventTarget` and mirrors the standard WebSocket surface where it makes sense. Differences are intentional and follow from the underlying WASM transport: | Browser `WebSocket` | `MixWebSocket` | Why | |---|---|---| | Synchronous constructor; readiness via the `open` event | Asynchronous constructor; readiness via `await ws.opened()` or the `open` event | The mixnet connect, smoltcp socket open, and TLS handshake all happen in the worker. | | `binaryType: 'blob' \| 'arraybuffer'` (default Blob) | `binaryType` is fixed to `'arraybuffer'` | Blob construction in a Web Worker requires a transferable; ArrayBuffer is the lowest common denominator. | | `bufferedAmount` | Not exposed | Writes queue inside the worker; no main-thread byte counter. | | `send()` returns `void` synchronously | `send()` returns `Promise` | The send hops the worker boundary. | | `close(code, reason)` returns `void` | `close(code, reason)` returns `Promise` | Same reason. | | `protocols` argument supports string or string[] | Same | (no difference) | The standard `open`, `message`, `close`, and `error` events fire as you would expect. `MessageEvent.data` is `string` for text frames and `ArrayBuffer` for binary frames. ## Security model `mix-websocket` follows the shared [mixnet exit security model](/developers/concepts/exit-security). What that means specifically for WS/WSS: | At the IPR exit | What's visible | |---|---| | Secure (`wss://`) | Destination IP and port. Frames are TLS ciphertext, terminating at the destination. | | Plain (`ws://`) | Destination IP and port, plus every frame in plaintext. | TLS terminates inside the WASM bundle (via [`rustls`](https://docs.rs/rustls) in smolmix-wasm), not in the browser. Mozilla's CA bundle is compiled into the WASM. Use `wss://` for any non-public traffic; `ws://` is visible to the IPR in full. --- title: mix-* Architecture: How the Browser Mixnet Packages Are Wired description: The shared browser architecture behind mix-tunnel, mix-fetch, mix-dns, and mix-websocket: one Web Worker, one WASM instance, a Comlink boundary, and a smoltcp + rustls stack reaching the mixnet via an IPR. url: https://nym.com/docs/developers/mix-architecture --- # Architecture The four mix-* packages ([`mix-tunnel`](/developers/mix-tunnel), [`mix-fetch`](/developers/mix-fetch), [`mix-dns`](/developers/mix-dns), and [`mix-websocket`](/developers/mix-websocket)) are not four independent clients. They are thin facades over a single shared tunnel, which runs in a Web Worker. This page explains that shared machinery once, so the per-package pages can stay focused on their own API. ```text Main thread (your app) mix-fetch, mix-dns, mix-websocket │ each re-exports mix-tunnel's controls and calls into it ▼ mix-tunnel → Comlink proxy │ ▼ postMessage (structured clone), into the worker Web Worker (one per page) smolmix-wasm ├─ IPR client → WebSocket (WSS) to entry gateway ├─ smoltcp (userspace TCP/IP stack) └─ rustls (TLS, Mozilla CA bundle compiled in) │ ▼ Nym mixnet: entry → 3 mix layers → IPR exit → internet ``` ## The package family Only `mix-tunnel` owns the tunnel. The three feature packages each depend on it and **re-export** its controls (`setupMixTunnel`, `disconnectMixTunnel`, and `getTunnelState`) alongside their own operation: | Package | Adds | Re-exports from mix-tunnel | |---|---|---| | `mix-tunnel` | the tunnel itself | (owns them) | | `mix-fetch` | `mixFetch`, `createMixFetch` | setup / disconnect / state | | `mix-dns` | `mixDNS` | setup / disconnect / state | | `mix-websocket` | `MixWebSocket` | setup / disconnect / state | So `import { setupMixTunnel, mixFetch } from '@nymproject/mix-fetch'` and `import { setupMixTunnel } from '@nymproject/mix-tunnel'` reach the **same** `setupMixTunnel`. You rarely import `mix-tunnel` directly; you get it transitively through whichever feature package you use. ## One tunnel, one WASM instance The bundler deduplicates [`@nymproject/mix-tunnel`](https://www.npmjs.com/package/@nymproject/mix-tunnel) to a single module, so no matter how many feature packages a page imports, there is exactly one Web Worker and one `smolmix-wasm` instance. Any feature package reaches that same instance through `getMixTunnel`. Bringing the tunnel up with `setupMixTunnel`, though, is a one-time operation: the first call succeeds and a second rejects with `tunnel already initialised`, so call it once. This is why the tunnel is configured once, at the first `setupMixTunnel`, and why options passed to a later call have no effect until teardown. It is also why a single connection to the entry gateway, a single IPR exit, and a single DNS cache are shared across all your `mixFetch` / `mixDNS` / `MixWebSocket` traffic. ## The worker boundary The mixnet work (Sphinx packet construction, cover traffic, Poisson send timing, the smoltcp poll loop) is CPU-bound and must not block the UI thread. So `mix-tunnel` runs all of it in a Web Worker and talks to it over [Comlink](https://github.com/GoogleChromeLabs/comlink), which wraps `postMessage` in an async RPC. The main thread holds a `Comlink.Remote` proxy; every call (`mixFetch`, `mixDNS`, `ws.send`) is an `await` that hops the worker boundary. That boundary is the reason `MixWebSocket.send()` and `.close()` return promises where the browser `WebSocket` returns `void`. **The `proxy` re-export.** `mix-websocket` needs to pass a message callback *into* the worker. Comlink marks a value as "transfer this by proxy, not by clone" using a `Symbol` that is created per module instance. If `mix-websocket` bundled its own copy of Comlink, that symbol would not match the one the worker-owning module's serialiser checks for, and the callback would fall through to structured clone, which cannot clone functions, so it throws. To avoid that, `mix-tunnel` re-exports `proxy`, and `mix-websocket` imports it from there, so both sides share one Comlink instance and the marker symbol matches. ## Inside the worker The worker hosts `smolmix-wasm`, the WebAssembly build of the Rust [`smolmix`](/developers/smolmix) crate. Three pieces do the work: - **IPR client**: opens a WebSocket (WSS by default, via `forceTls`) to a Nym entry gateway and speaks the mixnet protocol, exiting at an [IPR (Internet Packet Router)](/network/infrastructure/exit-services#ip-packet-router). - **`smoltcp`**: a userspace TCP/IP stack. Because the browser exposes no raw sockets, smolmix runs its own TCP and UDP over the mixnet's IP transport. A reactor polls smoltcp and wakes the relevant tasks when data arrives. - **`rustls`**: terminates TLS for `https://` and `wss://` end-to-end with the destination, with the Mozilla CA bundle compiled into the WASM. The IPR sees only ciphertext for encrypted targets. The tunnel is **one-shot per WASM instance**: `setupMixTunnel` can initialise it once. After `disconnectMixTunnel`, the instance is spent and the page must reload to build a new tunnel. ## Tunnel lifecycle ```text (no tunnel) ──setupMixTunnel()──▶ connecting ──▶ ready ──┐ │ mixFetch / mixDNS / MixWebSocket ▼ shutdown ◀──disconnectMixTunnel()── (still ready) ``` `getTunnelState()` reflects this as `connecting | ready | shutting_down | shutdown | failed` (see [tunnel state](/developers/mix-tunnel/guides#tunnel-state); the diagram shows the happy path, `shutting_down` is the transient during teardown and `failed` carries a `reason`). The transitions are coarse; the fine-grained gateway, IPR-discovery, and smoltcp events are logged to the browser console when the tunnel is brought up with `debug: true`. ## Going deeper - The native crate and its design: [`smolmix`](/developers/smolmix). - The package sources, including bundler/WASM-inlining specifics and the worker plumbing: [`sdk/typescript/packages`](https://github.com/nymtech/nym/tree/develop/sdk/typescript/packages) and the WASM crate under [`wasm/smolmix`](https://github.com/nymtech/nym/tree/develop/wasm/smolmix). - See it run: the [mixnet playground](/developers/playground). --- title: TypeScript SDK: Mixnet Messaging for the Browser description: TypeScript SDK for end-to-end mixnet messaging from a browser app. For HTTP, DNS, and WebSocket over the mixnet, see the mix-fetch / mix-dns / mix-websocket packages. url: https://nym.com/docs/developers/typescript --- # TypeScript SDK `@nymproject/sdk` is the browser-side raw messaging SDK. It opens a Nym mixnet client in a Web Worker and exposes a message-passing API: send text or binary payloads to another Nym address, receive payloads via event subscriptions, optionally reply anonymously with SURBs. ```text ┌──────────────────────────────────────────────────────────────┐ │ Your browser app (alice) │ │ └─ Nym Mixnet Client (WASM, Web Worker) │ │ └─ WebSocket to entry gateway │ │ └─ Nym mixnet (entry → 3 mix layers → exit) │ │ └─ Peer MixnetClient (bob) │ │ └─ Your peer's app │ └──────────────────────────────────────────────────────────────┘ ``` Both ends run a Nym client. Sphinx encryption protects every hop end-to-end; neither gateway nor any mix node can link sender to receiver. This is the **end-to-end messaging** path, where you control both sides. If you need HTTP, DNS, or WebSocket connections through the mixnet to third-party services, this isn't the right SDK. Use [`mix-fetch`](/developers/mix-fetch), [`mix-dns`](/developers/mix-dns), or [`mix-websocket`](/developers/mix-websocket), all built on [`mix-tunnel`](/developers/mix-tunnel) for IPR exit routing. For background on Sphinx, the mixnet exit model, and what the exit gateway can see, see [Exit security](/developers/concepts/exit-security). ## Packages | Package | Variant | When to use | |---|---|---| | `@nymproject/sdk` | ESM | Modern project, configurable bundler. See [Bundling](/developers/typescript/bundling/bundling). | | `@nymproject/sdk-full-fat` | ESM, inlined | Modern project, no bundler config available. Larger bundle. | | `@nymproject/sdk-commonjs` | CJS | Legacy project, configurable bundler. | | `@nymproject/sdk-full-fat-commonjs` | CJS, inlined | Legacy project, no bundler config available. Larger bundle. | | `@nymproject/contract-clients` | ESM | Query and execute Nym smart contracts on the Nyx chain. Separate transport (CosmWasm RPC, not the mixnet). See [Smart Contracts](/developers/typescript/smart-contracts). | The `*-full-fat` variants embed the WASM and Web Worker as Base64 in the JS bundle. Bundle size is large (tens of MB). Prefer a standard variant where bundler configuration is possible. ## Quick start ```ts const nym = await createNymMixnetClient(); nym.events.subscribeToTextMessageReceivedEvent((e) => { console.log('Received:', e.args.payload); }); await nym.client.start({ clientId: crypto.randomUUID(), nymApiUrl: 'https://validator.nymtech.net/api', forceTls: true, }); await nym.client.send({ payload: { message: 'hello mixnet', mimeType: 'text/plain' }, recipient: nym.client.selfAddress(), }); ``` For the full walkthrough including SURB replies and disconnect, see [Quick Start](/developers/typescript/quick-start). ## Smart contracts [`@nymproject/contract-clients`](https://www.npmjs.com/package/@nymproject/contract-clients) provides query and signing clients for every Nym smart contract on the Nyx chain (Mixnet, Coconut DKG, Vesting, Service Provider Directory, and more). It uses [CosmJS](https://github.com/cosmos/cosmjs) and the Nyx RPC endpoint, not the mixnet, so payloads are not routed through Sphinx. See [Smart Contracts](/developers/typescript/smart-contracts) for query and execute examples, and [Cosmos Kit](/developers/typescript/cosmos-kit) for wallet integration. ## Where to go next | | | |---|---| | [Quick Start](/developers/typescript/quick-start) | Vanilla TypeScript walkthrough of the messaging API. | | [Smart Contracts](/developers/typescript/smart-contracts) | Query and execute Nym contracts via `@nymproject/contract-clients`. | | [Cosmos Kit](/developers/typescript/cosmos-kit) | Wallet connection (Keplr, Ledger, Wallet Connect) for Nyx. | | [Bundling](/developers/typescript/bundling/bundling) | Webpack and esbuild configurations for WASM + Web Workers. | | [TypeDoc Reference](/developers/typescript/api/sdk/globals) | Generated API reference for `@nymproject/sdk`. | --- title: TypeScript SDK Quick Start description: Send and receive messages over the Nym mixnet using @nymproject/sdk. Vanilla TypeScript, no framework. url: https://nym.com/docs/developers/typescript/quick-start --- # Quick start A minimal, framework-free example. Connect to the mixnet, subscribe to incoming messages, send a message to yourself, disconnect. ## Installation ```bash npm install @nymproject/sdk-full-fat ``` `sdk-full-fat` inlines the WASM and Web Worker as Base64, so no bundler configuration is needed. For smaller bundles, install `@nymproject/sdk` instead and follow the [Bundling guide](/developers/typescript/bundling/bundling). ## Send and receive ```ts const nymApiUrl = 'https://validator.nymtech.net/api'; const nym = await createNymMixnetClient(); nym.events.subscribeToConnected((e) => { console.log('Connected:', e.args.address); }); nym.events.subscribeToTextMessageReceivedEvent((e) => { console.log('Received:', e.args.payload); }); await nym.client.start({ clientId: crypto.randomUUID(), nymApiUrl, forceTls: true, // WSS to entry gateway, required on HTTPS pages }); const selfAddress = nym.client.selfAddress(); await nym.client.send({ payload: { message: 'hello mixnet', mimeType: 'text/plain' }, recipient: selfAddress, }); // ... receive event fires, message logged ... await nym.client.stop(); ``` That's the whole loop: subscribe, start, send, receive, stop. The mixnet handshake takes a few seconds on first run; subsequent calls in the same session reuse the client. ## Anonymous replies (SURBs) Every outgoing message carries Single Use Reply Blocks by default. The recipient gets an opaque `sender_tag` and can reply without learning the sender's address: ```ts nym.events.subscribeToTextMessageReceivedEvent(async (e) => { if (e.args.senderTag) { await nym.client.replyWithSurb({ senderTag: e.args.senderTag, payload: { message: 'hello back, anonymously', mimeType: 'text/plain' }, }); } }); ``` The Nym mixnet has no persistent connections and no guaranteed ordering. The SDK abstracts this, but it shapes the patterns you can build: request/response works (via SURBs), but assumptions like "the third message will arrive third" don't hold. ## Where to go next - [TypeScript SDK landing](/developers/typescript): package variants, smart contracts, full options surface. - [TypeDoc reference](/developers/typescript/api/sdk/globals): generated API docs for `@nymproject/sdk`. - [Bundling](/developers/typescript/bundling/bundling): Webpack and esbuild configurations for the non-full-fat variants. If you encounter a persistent gateway client error, open your browser console, go to the "Application" tab, and delete the databases listed under "IndexedDB". The SDK persists client identity there; clearing it forces a fresh handshake. --- title: Nym Smart Contract Clients description: Query and execute on Nym smart contracts using TypeScript contract clients. Covers Mixnet, Coconut, Vesting, Name Service, and other on-chain contracts. url: https://nym.com/docs/developers/typescript/smart-contracts --- # Nym Smart Contract Clients To query or execute on any of the Nym contracts, use the [`Contract Clients`](https://www.npmjs.com/package/@nymproject/contract-clients), which contain read-only query and signing clients for all of Nym's smart contracts. ## Contract Clients | Client | Functionality | Methods | | :---: | :---: | :---: | | Coconut Bandwidth | Manages depositing and release of funds. Tracks double spending. | [Source](https://github.com/nymtech/nym/blob/develop/sdk/typescript/codegen/contract-clients/src/CoconutBandwidth.client.ts) | | Coconut DKG | Allows signers to derive keys for issuing Coconut credentials. | [Source](https://github.com/nymtech/nym/blob/develop/sdk/typescript/codegen/contract-clients/src/CoconutDkg.client.ts) | | Cw3FlexMultisig | Used by the Coconut APIs to issue credentials. [cw3-flex-multisig](https://github.com/CosmWasm/cw-plus/tree/main/contracts/cw3-flex-multisig) backed by cw4. | [Source](https://github.com/nymtech/nym/blob/develop/sdk/typescript/codegen/contract-clients/src/Cw3FlexMultisig.client.ts) | | Cw4Group | Used by the Coconut APIs. [Cw4 Group](https://github.com/CosmWasm/cw-plus/tree/main/contracts/cw4-group) stores members with an admin. | [Source](https://github.com/nymtech/nym/blob/develop/sdk/typescript/codegen/contract-clients/src/Cw4Group.client.ts) | | Mixnet | Manages the network topology, tracking delegations and rewards. | [Source](https://github.com/nymtech/nym/blob/develop/sdk/typescript/codegen/contract-clients/src/Mixnet.client.ts) | | Name Service | Directory of user-defined aliases, analogous to DNS. | [Source](https://github.com/nymtech/nym/blob/develop/sdk/typescript/codegen/contract-clients/src/NameService.client.ts) | | Service Provider Directory | Public directory for registering service providers. | [Source](https://github.com/nymtech/nym/blob/develop/sdk/typescript/codegen/contract-clients/src/ServiceProviderDirectory.client.ts) | | Vesting | Manages NYM token vesting functionality. | [Source](https://github.com/nymtech/nym/blob/develop/sdk/typescript/codegen/contract-clients/src/Vesting.client.ts) | ## Environment Setup Create a new project with Vite: ```bash npm create vite@latest ``` Choose React + TypeScript, then: ```bash cd npm i npm run dev ``` ## Query Example ### Installation ```bash npm install @nymproject/contract-clients @cosmjs/cosmwasm-stargate ``` ### Querying the Mixnet Contract This example uses `MixnetQueryClient` to fetch a paged list of mixnodes from the contract. Create a `settings.ts` file for your network configuration: ```ts filename="settings.ts" export const settings = { url: "wss://rpc.nymtech.net:443", mixnetContractAddress: "n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr", }; ``` ```ts copy filename="App.tsx" const getClient = async () => { const cosmWasmClient = await SigningCosmWasmClient.connect(settings.url); const client = new contracts.Mixnet.MixnetQueryClient( cosmWasmClient, settings.mixnetContractAddress ); return client; }; export const Mixnodes = () => { const [mixnodes, setMixnodes] = useState(); const getMixnodes = async () => { const client = await getClient(); const { nodes } = await client.getMixNodesDetailed({}); setMixnodes(nodes); }; useEffect(() => { getMixnodes(); }, []); if (!mixnodes) { return ( ); } return ( {mixnodes?.length && mixnodes.map((mixnode: any) => ( {`id: ${mixnode.bond_information.mix_id}`} {`owner: ${mixnode.bond_information.owner}`} ))} ); }; ``` ## Execute Example ### Installation ```bash npm install @nymproject/contract-clients @cosmjs/cosmwasm-stargate @cosmjs/proto-signing ``` ### Executing Contract Methods This example uses `MixnetClient` with a signer to execute methods like delegation. Update your `settings.ts` to include signing credentials: ```ts filename="settings.ts" export const settings = { url: "wss://rpc.nymtech.net:443", mixnetContractAddress: "", mnemonic: "", address: "", }; ``` ```ts copy filename="App.tsx" export default function Exec() { let signer: DirectSecp256k1HdWallet; let signerMixnetClient: any; let cosmWasmSigningClient: SigningCosmWasmClient; let mixId: number; let amountToDelegate: string; let nodeAddress: string; let amountToSend: string; let delegations: any; async function ExecuteOnNyx() { signer = await DirectSecp256k1HdWallet.fromMnemonic(settings.mnemonic, { prefix: "n", }); const cosmWasmClient = await SigningCosmWasmClient.connectWithSigner( settings.url, signer, { gasPrice: GasPrice.fromString("0.025unym") } ); cosmWasmSigningClient = cosmWasmClient; const mixnetClient = new contracts.Mixnet.MixnetClient( cosmWasmSigningClient, settings.address, settings.mixnetContractAddress ); signerMixnetClient = mixnetClient; } const getDelegations = async () => { if (!signerMixnetClient) return; delegations = await signerMixnetClient.getDelegatorDelegations({ delegator: settings.address, }); }; const doDelegation = async () => { if (!signerMixnetClient) return; const res = await signerMixnetClient.delegateToMixnode( { mixId }, "auto", undefined, [{ amount: `${amountToDelegate}`, denom: "unym" }] ); console.log(res); }; const doUndelegateAll = async () => { for (const delegation of delegations.delegations) { await signerMixnetClient.undelegateFromMixnode( { mixId: delegation.mix_id }, "auto" ); } }; const doSendTokens = async () => { const res = await cosmWasmSigningClient.sendTokens( settings.address, nodeAddress, [{ amount: amountToSend, denom: "unym" }], "auto", "test sending tokens" ); console.log(res); }; ExecuteOnNyx(); setTimeout(() => getDelegations(), 1000); return (

Send Tokens

(nodeAddress = e.target.value)} /> (amountToSend = e.target.value)} />

Delegate

(mixId = +e.target.value)} /> (amountToDelegate = e.target.value)} /> ); } ``` --- title: Cosmos Kit: Wallet Connection for Nyx description: Connect Keplr, Ledger, and Wallet Connect wallets to the Nyx chain from a React app using Cosmology's Cosmos Kit. url: https://nym.com/docs/developers/typescript/cosmos-kit --- # Cosmos Kit Cosmology's [Cosmos Kit](https://cosmoskit.com/) works with Nyx. The kit covers: - Wallets such as Keplr, Cosmostation, and others from your React application - The [Ledger hardware wallet](https://docs.cosmoskit.com/integrating-wallets/ledger) from your browser - Any wallet that supports [Wallet Connect v2.0](https://docs.cosmoskit.com/integrating-wallets/adding-new-wallets) ## Environment setup Create a new project with Vite: ```bash npm create vite@latest ``` Choose React, then TypeScript. Navigate to your application directory and run: ```bash cd npm i npm run dev ``` ## Installation ```bash npm install @cosmos-kit/react @cosmos-kit/keplr @cosmos-kit/ledger chain-registry ``` You need to polyfill some nodejs modules in order to use keplr and ledger wallets by modifying your `vite.config.js` file: ```bash npm install @esbuild-plugins/node-globals-polyfill ``` ```js // vite.config.js export default defineConfig({ plugins: [react()], optimizeDeps: { esbuildOptions: { define: { global: 'globalThis' }, plugins: [ NodeGlobalsPolyfillPlugin({ buffer: true }) ] } } }) ``` Your components have to be wrapped into a [ChainProvider](https://docs.cosmoskit.com/chain-provider), in order to use the `useChain('nyx')` hook. The nyx chain is provided in the 'chain-registry' NPM package by default. Now, go to the `src` folder and open your `App.tsx` file to replace all the code with the following, which will allow you to connect and disconnect a Ledger or Keplr wallet to Nyx: ```ts export const getDoc = (address: string) => { const chainId = 'nyx'; const msg: AminoMsg = { type: '/cosmos.bank.v1beta1.MsgSend', value: MsgSend.fromPartial({ fromAddress: address, toAddress: 'n1nn8tghp94n8utsgyg3kfttlxm0exgjrsqkuwu9', amount: [{ amount: '1000', denom: 'unym' }], }), }; const fee = { amount: [{ amount: '2000', denom: 'ucosm' }], gas: '180000', // 180k }; const memo = 'Use your power wisely'; const accountNumber = 15; const sequence = 16; const doc = makeSignDoc([msg], fee, chainId, memo, accountNumber, sequence); return doc }; function MyComponent() { const {wallet, address, connect, disconnect, getOfflineSignerAmino } = useChain('nyx'); React.useEffect(() => { connect(); disconnect(); }, []); const sign = async () => { if (!address) return const doc = getDoc(address); return getOfflineSignerAmino().signAmino(address, doc); }; return ( {wallet &&
Connected to {wallet?.prettyName}
Address: {address}
} {wallet ? ( ) : ( )} ); } export default function App() { const assetsFixedUp = React.useMemo(() => { const nyx = assets.find((a) => a.chain_name === 'nyx'); if (nyx) { const nyxCoin = nyx.assets.find((a) => a.name === 'nyx'); if (nyxCoin) { nyxCoin.coingecko_id = 'nyx'; } nyx.assets = nyx.assets.reverse(); } return assets; }, [assets]); return ( c.chain_id === 'nyx')!]} assetLists={assetsFixedUp} wallets={[...ledger, ...keplr]} signerOptions={{ preferredSignType: () => 'amino', }} > ) } ``` --- title: TypeScript SDK Bundling Troubleshooting description: Fix common bundling issues with the Nym TypeScript SDK: WASM files missing from output, web worker configuration for Webpack and other bundlers. url: https://nym.com/docs/developers/typescript/bundling/bundling --- # Troubleshooting ## Bundling issues ### WASM and web worker not included in output bundle (Webpack) You might need to use the CopyPlugin by adding this to your Webpack config: ```js const CopyPlugin = require('copy-webpack-plugin'); module.exports = { plugins: [ new CopyPlugin({ patterns: [ { from: path.resolve(path.dirname(require.resolve('@nymproject/mix-fetch/package.json')), '*.wasm'), to: '[name][ext]', }, { from: path.resolve(path.dirname(require.resolve('@nymproject/mix-fetch/package.json')), '*worker*.js'), to: '[name][ext]', }, ], }), ], } ``` `require.resolve('@nymproject/mix-fetch/package.json')` finds the disk location of the Nym SDK package. `path.dirname` resolves the directory, and the `*.wasm` glob matches the WASM files. Use `[name][ext]` to preserve the output filename, because the package expects it to stay the same. ### ESM not supported If your bundler does not support ECMAScript Modules (ESM), CommonJS packages are supported for most parts of the SDK. For those that don't have ESM versions, you will need to use a tool like [Babel](https://babeljs.io/) to convert ESM to CommonJS. ### CSP prevents loading If you are using a `*-full-fat` package, or if you inline WASM or web workers, you may not be able to load them if the [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) prevents WASM from being instantiated from a string. You'll have to experiment with either adjusting the CSP or use another variant that is unbundled. ## Mixnet client issues ### Insufficient topology error The mixnet client will complain about insufficient topology in the following cases: - There are empty mix layers (rare) - The gateway you've registered with does not appear in the network topology; it is either unbonded or was blacklisted - The gateway you want to send packets to does not appear in the network topology; it is either unbonded or was blacklisted To avoid the last two, make sure the gateway you are using is bonded and whitelisted. ### Checking gateway status Your client address has the format: `client-id.client-dh@gateway-id` For example: `DpB3cHAchJi...suko.ANNWrvHq...U2Vx@2BuMSfMW...3SEh` - First part: client's identity key - Second part: client's Diffie-Hellman key - After `@`: gateway's identity key. Search for this in the [Nym Explorer](https://nym.com/explorer) to check its status --- title: Troubleshooting bundling with ESbuild url: https://nym.com/docs/developers/typescript/bundling/esbuild --- # Troubleshooting bundling with ESbuild If you have followed the steps in the Examples section, your development environment should be configured as follows. #### Environment Setup Create your directory and set up your app environment: ```bash npm create vite@latest ``` Choose React, then Typescript. Navigate to your application directory and run: ```bash cd < YOUR_APP > npm i npm run dev ``` ##### Installation Install the required package: ```bash npm install @nymproject/< PACKAGE_NAME > ``` The CosmosKit example requires polyfills. With the code from the step-by-step examples section in place, the application should run without bundling errors. --- title: Troubleshooting bundling with Webpack url: https://nym.com/docs/developers/typescript/bundling/webpack --- # Troubleshooting bundling with Webpack ## Webpack > 5 ESM For any project using Webpack, you´ll need the following rule in your `webpack.config.js` above version 5: ```json { test: /\.(m?js)$/, resolve: { fullySpecified: false } } ``` ### Create-react-app #### General cases To use Webpack with the code from the step-by-step examples section: ```bash npx create-react-app nymapp --template typescript cd nymapp ``` Install the required dependencies, then paste the code from the step-by-step section into your app's `App.tsx`. #### Contract client With webpack, the `Contract client` for querying or executing may need polyfills. Since create-react-app doesn't expose the Webpack config without ejecting, override it as follows: ##### Install contract-clients dependencies ```bash npm install @nymproject/contract-clients @cosmjs/cosmwasm-stargate @cosmjs/proto-signing ``` In your app's `App.tsx`, replace the existing code with the code from the step-by-step examples section. ##### Polyfilling Copy the following to your terminal and run: ```bash npm install react-app-rewired npm install --save-dev crypto-browserify stream-browserify assert stream-http https-browserify os-browserify url buffer process cat < config-overrides.js const webpack = require('webpack'); const path = require('path') module.exports = function override(config) { const fallback = config.resolve.fallback || {}; Object.assign(fallback, { "crypto": require.resolve("crypto-browserify"), "stream": require.resolve("stream-browserify"), "assert": require.resolve("assert"), "http": require.resolve("stream-http"), "https": require.resolve("https-browserify"), "os": require.resolve("os-browserify"), "url": require.resolve("url") }) config.resolve.fallback = fallback; config.plugins = (config.plugins || []).concat([ new webpack.ProvidePlugin({ process: 'process/browser', Buffer: ['buffer', 'Buffer'] }) ]) config.module.rules = (config.module.rules || []).concat([ { test: /\.(m?js)$/, resolve: { fullySpecified: false } } ]) return config; } EOF ``` #### Edit the `package.json` file as follows: ```json "scripts": { "start": "react-app-rewired start", "build": "react-app-rewired build", "test": "react-app-rewired test", "eject": "react-scripts eject" }, ``` --- title: NymVPN CLI: Run NymVPN from the Command Line description: Install and run NymVPN from the terminal on Linux, macOS, and Windows. Includes ARM64 .deb packages, account setup, tunnel configuration, and gateway selection. url: https://nym.com/docs/developers/nymvpncli --- # Nym VPN CLI This is a short guide to setting up and using the `nym-vpnc` tool, which is used in conjunction with the `nym-vpnd` daemon. Download and run instructions for the GUIs can be found [here](https://nymvpn.com/en/download/linux). ## Download & Extract Binary Check the [release page](https://github.com/nymtech/nym-vpn-client/releases/) page for the latest release version and modify the instructions accordingly. These instructions use the latest as of the time of writing. ```sh wget -q https://github.com/nymtech/nym-vpn-client/releases/download/nym-vpn-core-v1.29.0/nym-vpn-core-v1.29.0_.tar.gz && tar -xzf nym-vpn-core-v1.29.0_.tar.gz && cd nym-vpn-core-v1.29.0_/ && chmod u+x * ``` ### Linux ARM64 (.deb) ARM64 `.deb` packages are available for Linux distributions that support them (e.g. Ubuntu/Debian on Raspberry Pi or ARM servers). Install both the daemon and the client: ```sh sudo dpkg -i nym-vpnd__arm64.deb sudo dpkg -i nym-vpnc__arm64.deb ``` The `.deb` package installs a systemd service that starts `nym-vpnd` automatically. Verify the service is running: ```sh service nym-vpnd status ``` You should see output similar to: ```sh ● nym-vpnd.service - nym-vpnd daemon Loaded: loaded (/usr/lib/systemd/system/nym-vpnd.service; enabled; preset: enabled) Active: active (running) ``` Verify the installed version with `nym-vpnc info`: ```sh nym-vpnc info ``` ```sh nym-vpnd: version: 1.25.0 build_timestamp (utc): 2026-03-02 16:25:31.229479864 +00:00:00 triple: aarch64-unknown-linux-gnu platform: Ubuntu; Linux (Ubuntu 24.04); aarch64 git_commit: fce7a84e612b8d2cb48b66695cdaf023d7f9a42b ``` ## Build from Source ### Prerequisites All operating systems require both [Rust](https://www.rust-lang.org/tools/install) and [Go](https://go.dev/doc/install). **Arch specific packages:** ```sh yay -S gcc make protobuf base-devel clang ``` **Ubuntu24 specific packages:** ```sh apt install gcc make protobuf-compiler pkconfig libdbus-1-dev build-essential clang ``` Older Debian/Ubuntu versions need to manually install `protobuf-compiler` >= v3.21.12 ### Clone & `make` ```sh git clone https://github.com/nymtech/nym-vpn-client.git cd nym-vpn-client/ make ``` ## Start the Daemon If you installed via `.deb` packages, the daemon is already running as a systemd service. You can check its status with: ```sh service nym-vpnd status ``` If you are running from pre-built binaries or a source build, start the daemon manually: ```sh sudo ./PATH/TO/nym-vpnd ``` Leave the daemon running and run `nym-vpnc` commands in a separate terminal window. ## Account Setup ### Create an Account Head to [https://nym.com/account/create](https://nym.com/account/create) and obtain a passphrase (mnemonic). ### Log In Store your account passphrase on this device: ```sh nym-vpnc account set "" ``` ### Check Account Status Verify that the device is logged in and view account details: ```sh nym-vpnc account get ``` Example output: ```sh Account identity: n1wlmrpa7ts7znz7nxvmxevaw65796cr6q6pht69 Canonical Account identity: n1wlmrpa7ts7znz7nxvmxevaw65796cr6q6pht69 Account mode: Some(Api) Account state: Error(BandwidthExceeded { context: "SYNCING_STATE" }) ``` ### Account Summary & Balance ```sh nym-vpnc account summary nym-vpnc account balance ``` ### Account Links Get URLs for managing your NymVPN account: ```sh nym-vpnc account links ``` ### Forget Account Remove the stored passphrase, device keys, and local credentials from this device: ```sh nym-vpnc account forget ``` ### Device Information View the current device identity: ```sh nym-vpnc device get ``` ## Pay as You Go: Decentralized Access to Nym You can fund your VPN usage directly from your own wallet instead of going through the NymVPN account system. You deposit `$NYM` into the ticketbook smart contract and receive zk-nym ticketbooks that authenticate you on the network. If you already have an account stored in `nym-vpnc`, you must remove it first with `nym-vpnc account forget` before setting a new mnemonic. ### Set Your Mnemonic Store the recovery phrase for your on-chain wallet address (`n1...`) that holds your `$NYM` tokens: ```sh nym-vpnc account set "" --location blockchain ``` You must fund this address yourself, for example by transferring `$NYM` from an exchange or another wallet. The `--location blockchain` flag tells `nym-vpnc` to use the on-chain wallet directly rather than the NymVPN account system. ### Obtain Ticketbooks Deposit `$NYM` into the ticketbook smart contract and receive zk-nym credentials: ```sh nym-vpnc account obtain-ticketbooks --amount 1 --source smartcontract ``` You can omit `--source` to use the default: ```sh nym-vpnc account obtain-ticketbooks --amount 1 ``` The `--amount` flag specifies how many ticketbooks to obtain **per ticket type**. Each request issues one ticketbook for each of the three types listed below, so `--amount 1` produces 3 ticketbooks total and `--amount 2` produces 6. Each ticketbook contains **50 tickets** and is valid for **7 days**. Each ticketbook costs **75 NYM**, so `--amount 1` deposits **225 NYM** (75 × 3 types) and `--amount 2` deposits **450 NYM**. | Kind | Ticket size | Ticketbook capacity | Used for | |------|-------------|---------------------|----------| | Mixnet Entry | 200 MB | 10 GB (200 MB × 50) | 5-hop mixnet mode | | WireGuard Entry | 500 MB | 25 GB (500 MB × 50) | 2-hop WireGuard mode (entry side) | | WireGuard Exit | 500 MB | 25 GB (500 MB × 50) | 2-hop WireGuard mode (exit side) | If you only use two-hop (WireGuard) mode, the Mixnet Entry ticketbooks will go unused. There is currently no way to obtain ticketbooks for a single type. This command: - Deposits `$NYM` into the ticketbook smart contract (plus a small fee buffer per deposit) - Requests credential issuance from the decentralised Nym API validators - Stores the resulting zk-nym ticketbooks locally on the device ### Connect Connect using the locally stored ticketbooks: ```sh nym-vpnc connect-v2 ``` The CLI uses the ticketbooks to authenticate with entry nodes (gateways) and connect to the Nym network. You will see the connection happening in the `nym-vpnd` logs. ## Tunnel Configuration Print current tunnel configuration: ```sh nym-vpnc tunnel get ``` Enable two-hop mode (WireGuard): traffic jumps directly from entry gateway to exit gateway: ```sh nym-vpnc tunnel set --two-hop on ``` Enable Mixnet (5-hop): disable two-hop to route traffic through the full mixnet for maximum privacy: ```sh nym-vpnc tunnel set --two-hop off ``` Enable or disable IPv6: ```sh nym-vpnc tunnel set --ipv6 on ``` Enable censorship circumvention transports (currently QUIC): ```sh nym-vpnc tunnel set --circumvention-transports on ``` Run `nym-vpnc tunnel set --help` for all available tunnel options including mixnet timing parameters. ## Gateway Configuration Set entry and exit gateways bound to specific countries using [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country codes: ```sh nym-vpnc gateway set --entry-country US --exit-country JP ``` Print current gateway configuration: ```sh nym-vpnc gateway get ``` Example output: ```sh Entry point: Country { two_letter_iso_country_code: "US" } Exit point: Country { two_letter_iso_country_code: "JP" } Residential exit: off ``` Only use residential exit nodes: ```sh nym-vpnc gateway set --residential-exit on ``` ### List Available Gateways List available WireGuard gateways (use a wide terminal window for the table output): ```sh nym-vpnc gateway list wg ``` You can also list mixnet entry and exit gateways: ```sh nym-vpnc gateway list mixnet-entry nym-vpnc gateway list mixnet-exit ``` ## Connect & Disconnect Connect using the settings stored in `nym-vpnd`: ```sh nym-vpnc connect ``` Disconnect: ```sh nym-vpnc disconnect ``` Reconnect: ```sh nym-vpnc reconnect ``` Print the current tunnel status: ```sh nym-vpnc status ``` Continuously stream tunnel status in real time: ```sh nym-vpnc status --listen ``` ## Ad-Block NymVPN includes a built-in ad-blocker (Brave ad-engine). Ad-blocking is only active while the tunnel is connected. Enable ad-block: ```sh nym-vpnc ad-block set enabled ``` Disable ad-block: ```sh nym-vpnc ad-block set disabled ``` You can test ad-blocking with [adblock.turtlecute.org](https://adblock.turtlecute.org/). Some browsers cache DNS internally, so toggling ad-block on/off at runtime may not have an immediate effect; a browser restart may be needed. Use `nslookup` or `dig` to verify that domains are being blocked. ## DNS View current DNS configuration: ```sh nym-vpnc dns get nym-vpnc dns get-default ``` Set custom DNS servers: ```sh nym-vpnc dns set 1.1.1.1 9.9.9.9 nym-vpnc dns enable ``` Disable custom DNS and revert to defaults: ```sh nym-vpnc dns disable ``` Clear custom DNS servers: ```sh nym-vpnc dns clear ``` ## Local Network Access Control whether local network (LAN) traffic is allowed while the tunnel is active: ```sh nym-vpnc lan get nym-vpnc lan set allow nym-vpnc lan set block ``` ## SOCKS5 Proxy NymVPN can expose a local SOCKS5 proxy: ```sh nym-vpnc socks5 enable nym-vpnc socks5 disable nym-vpnc socks5 status ``` ## Network View or change the Nym network (requires a daemon restart): ```sh nym-vpnc network get nym-vpnc network set mainnet ``` ## Diagnostic Run connectivity diagnostics: ```sh nym-vpnc diagnostic run ``` ## Getting Help Any `nym-vpnc` command has built-in help. Add `--help` to the end of any command to view available options: ```sh nym-vpnc --help nym-vpnc connect --help nym-vpnc tunnel set --help ``` ## Default Config Directories Configurations are stored in `/etc/nym`. State stored between runs (keys, mnemonic, etc) are stored in `/var/lib/nym-vpnd`. --- title: Interacting with the Nyx Blockchain description: Query and transact against Nyx, the Cosmos-SDK chain underpinning Nym: the nyxd CLI wallet, Ledger, the Cosmos chain registry, and running an RPC node. url: https://nym.com/docs/developers/chain --- # Interacting with the Nyx blockchain Nyx is the Cosmos-SDK blockchain that underpins Nym. It holds the NYM token and the mixnet smart contracts (node bonding, rewarding, and the directory). This section covers the ways to query it and submit transactions. For smart-contract access from TypeScript, see [`@nymproject/contract-clients`](https://www.npmjs.com/package/@nymproject/contract-clients), covered under [Smart Contracts](/developers/typescript/smart-contracts) in the TypeScript SDK. ## In this section - [CLI Wallet](/developers/chain/cli-wallet): use the `nyxd` binary to create keypairs and to sign and broadcast transactions from the command line. - [Ledger Live](/developers/chain/ledger-live): use a Ledger hardware wallet with the Nyx chain. - [Cosmos Registry](/developers/chain/cosmos-registry): Nyx's entry in the Cosmos chain registry (chain info and RPC endpoints). - [RPC Nodes](/developers/chain/rpc-node): run a node that holds a copy of the chain for querying and broadcasting, without taking part in consensus. --- title: CLI Wallet url: https://nym.com/docs/developers/chain/cli-wallet --- # CLI Wallet If you have read our [validator setup and maintenance documentation](../../operators/nodes/validator-setup), you will have seen that we compile and use the `nyxd` binary primarily for our validators. This binary can also be used for other tasks, such as creating and using keypairs for wallets, or automated setups that require the signing and broadcasting of transactions. ### Using `nyxd` binary as a CLI wallet You can use `nyxd` as a minimal CLI wallet if you want to set up one or more accounts. Compile the binary as per the documentation, stopping after the [building your validator](../../operators/nodes/validator-setup#building-your-validator) step is complete. Then run `nyxd keys --help` to see how to set up and store keypairs for interacting with the Nyx blockchain. --- title: Ledger Live Support url: https://nym.com/docs/developers/chain/ledger-live --- # Ledger Live Support Use the following instructions to interact with the Nyx blockchain (either with deployed smart contracts, or to send tokens) using your Ledger device to sign transactions. ## Prerequisites * Download and install [Ledger Live](https://www.ledger.com/ledger-live). * Compile the `nyxd` binary as per the instructions [here](../../operators/nodes/validator-setup). Stop after you can successfully run `nyxd` and get the helptext in your console output. ## Prepare your Ledger App * Plug in your Ledger device * Install the `Cosmos (ATOM)` app by following the instructions [here](https://hub.cosmos.network/main/resources/ledger.html). This app works with any Cosmos SDK chain, so you can manage your ATOM, OSMOSIS, NYM tokens, etc. * On the device, navigate to the Cosmos app and open it ## Create a keypair Add a reference to the ledger device on your local machine by running the following command in the same directory as your `nyxd` binary: ``` nyxd keys add ledger_account --ledger ``` ## Command help with `nyxd` More information about each command is available by consulting the help section (`--help`) at each layer of `nyxd`'s commands: ``` # logging top level command help nyxd --help # logging top level command help for transaction commands nyxd tx --help # logging top level command help for transaction commands utilising the 'bank' module nyxd tx bank --help ``` ## Sending tokens between addresses Perform a transaction from the CLI with `nyxd`, appending the `--ledger` option to the command. As an example, the below command will send 1 `NYM` from the ledger account to the `$DESTINATION_ACCOUNT`: ``` nyxd tx bank send ledger_account $DESTINATION_ACCOOUNT 1000000unym --ledger --node https://rpc.dev.nymte.ch:443 ``` > When a command is run, the transaction will appear on the Ledger device and will require physical confirmation from the device before being signed. ## Nym-specific transactions Nym-specific commands and queries, like bonding a Mix Node or delegating unvested tokens, are available in the `wasm` module, and follow the following pattern: ``` # Executing commands nyxd tx wasm execute $CONTRACT_ADDRESS $JSON_MSG # Querying the state of a smart contract nyxd query wasm contract-state smart $CONTRACT_ADDRESS $JSON_MSG ``` You can find the value of `$CONTRACT_ADDRESS` in the [`network defaults`](https://github.com/nymtech/nym/blob/master/common/network-defaults/src/mainnet.rs) file. The value of `$JSON_MSG` will be a blog of `json` formatted as defined for each command and query. You can find these definitions for the mixnet smart contract [here](https://github.com/nymtech/nym/blob/master/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs) and for the vesting contract [here](https://github.com/nymtech/nym/blob/master/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs) under `ExecuteMsg` and `QueryMsg`. ### Example command execution: #### Delegate to a Mix Node You can delegate to a Mix Node from the CLI using `nyxd` and signing the transaction with your ledger by filling in the values of this example: ``` CONTRACT_ADDRESS=mixnet_contract_address ./nyxd tx wasm execute $CONTRACT_ADDRESS '{"delegate_to_mixnode":{"mix_identity":"MIX_NODE_IDENTITY","amount":{"amount":"100000000000","denom":"unym"}}}' --ledger --from admin --node https://rpc.dev.nymte.ch:443 --gas-prices 0.025unymt --gas auto -b block ``` > By replacing the value of `CONTRACT_ADDRESS` with the address of the vesting contract, you could use the above command to use tokens held in the vesting contract. #### Query a vesting schedule You can query for (e.g.) seeing the current vesting period of an address by filling in the values of the following: ``` CONTRACT_ADDRESS=vesting_contract_address nyxd query wasm contract-state smart $CONTRACT_ADDRESS '{"get_current_vesting_period"}:{"address": "address_to_query_for"}' --ledger --from admin --node https://rpc.dev.nymte.ch:443 --chain-id qa-net --gas-prices 0.025unymt --gas auto -b block ``` --- title: Cosmos Registry url: https://nym.com/docs/developers/chain/cosmos-registry --- # Cosmos Registry You can find all relevant information (chain info, RPC endpoints, etc) on the [Cosmos Chain Registry](https://github.com/cosmos/chain-registry/tree/master/nyx). --- title: Run a Nyx RPC Node for the Nym Network description: Set up and run a dedicated RPC node for the Nyx blockchain. Query network state, serve chain data, and interact with Nym smart contracts programmatically. url: https://nym.com/docs/developers/chain/rpc-node --- # RPC Nodes RPC Nodes (sometimes called 'Lite Nodes' or 'Full Nodes') differ from Validators in that they hold a copy of the Nyx blockchain but do **not** participate in consensus / block-production. You may want to set up an RPC Node for querying the blockchain, or to provide an endpoint that your app can use to send transactions. To set up an RPC Node, follow the instructions to set up a [Validator](../../operators/nodes/validator-setup), but **exclude the `nyxd tx staking create-validator` command**. If you want to fast-sync your node, check out the Polkachu snapshot and their other [resources](https://polkachu.com/seeds/nym). --- title: Nym Developer Tools: CLI, Diagnostics & TcpProxy description: Overview of Nym developer tools including nym-cli for blockchain interaction, diagnostic tool for troubleshooting, and standalone TcpProxy binary downloads. url: https://nym.com/docs/developers/tools --- # Tools Standalone binaries for development and testing that don't require an SDK. Download or compile them and use them directly. | Tool | Use case | |---|---| | [nym-cli](./tools/nym-cli) | Command-line interface for interacting with the Nyx blockchain: querying state, submitting transactions, managing keys. An easier-to-use wrapper around `nyxd`. | | [Diagnostic Tool](./tools/diagnostic-tool) | Network diagnostic utility for troubleshooting connectivity issues. | | [Standalone TcpProxy](./tools/standalone-tcpproxy) | Pre-built binaries of the TcpProxy client and server for proxying TCP traffic through the Mixnet. The TcpProxy module is unmaintained; use the [Stream module](./rust/stream) for new projects. | --- title: Nym CLI: Mixnet & Blockchain Commands description: Use nym-cli to interact with the Nym mixnet and Nyx blockchain. Manage nodes, delegate stake, and query network state from the command line. url: https://nym.com/docs/developers/tools/nym-cli --- # Nym-CLI This is a CLI tool for interacting with: * the Nyx blockchain (account management, querying the chain state, etc) * the smart contracts deployed on Nyx (bonding and un-bonding mixnodes, collecting rewards, etc) It provides a convenient wrapper around the `nymd` client, and has similar functionality to the `nyxd` binary for querying the chain or executing smart contract methods. --- title: Usage url: https://nym.com/docs/developers/tools/nym-cli/usage --- # Usage ## Building The `nym-cli` binary can be built by running `cargo build --release` in the `nym/tools/nym-cli` directory. ## Usage See the [commands](/developers/tools/nym-cli/commands) page for an overview of all command options. ## Staking on someone's behalf (for custodians) The staking address can only perform the following actions (visible via the Nym Wallet): - Bond on the gateway's or Mix Node's behalf. - Delegate or Un-delegate (to a Mix Node in order to begin receiving rewards) - Claiming the rewards on the account ```admonish note title="" The staking address has no ability to withdraw any coins from the parent's account. ``` The staking address must maintain the same level of security as the parent mnemonic; while the parent mnemonic's delegations and bonding events will be visible to the parent owner, the staking address will be the only account capable of undoing the bonding and delegating from the Mix Nodes or gateway. Query for staking on behalf of someone else ``` ./nym-cli --mnemonic mixnet delegators delegate --mix-id --identity-key --amount ``` --- title: `nym-cli` Binary Commands (Autogenerated) url: https://nym.com/docs/developers/tools/nym-cli/commands --- # `nym-cli` Binary Commands (Autogenerated) These docs are autogenerated by the [`autodocs`](https://github.com/nymtech/nym/tree/max/new-docs-framework/documentation/autodoc) script. ```sh A client for interacting with Nym smart contracts and the Nyx blockchain Usage: nym-cli [OPTIONS] Commands: account Query and manage Nyx blockchain accounts signature Sign and verify messages ecash Ecash related stuff block Query chain blocks cosmwasm Manage and execute WASM smart contracts tx Query for transactions vesting-schedule Create and query for a vesting schedule mixnet Manage your mixnet infrastructure, delegate stake or query the directory generate-fig Generates shell completion help Print this message or the help of the given subcommand(s) Options: --mnemonic Provide the mnemonic for your account. You can also provide this is an env var called MNEMONIC. -c, --config-env-file Overrides configuration as a file of environment variables. Note: individual env vars take precedence over this file. --nyxd-url Overrides the nyxd URL provided either as an environment variable NYXD_VALIDATOR or in a config file --nym-api-url Overrides the validator API URL provided either as an environment variable API_VALIDATOR or in a config file --mixnet-contract-address Overrides the mixnet contract address provided either as an environment variable or in a config file --vesting-contract-address Overrides the vesting contract address provided either as an environment variable or in a config file -h, --help Print help ``` ## `account` ```sh Query and manage Nyx blockchain accounts Usage: nym-cli account [OPTIONS] nym-cli account Commands: create Create a new mnemonic - note, this account does not appear on the chain until the account id is used in a transaction balance Gets the balance of an account pub-key Gets the public key of an account send Sends tokens to another account send-multiple Batch multiple token sends help Print this message or the help of the given subcommand(s) Options: --mnemonic Provide the mnemonic for your account. You can also provide this is an env var called MNEMONIC. -c, --config-env-file Overrides configuration as a file of environment variables. Note: individual env vars take precedence over this file. --nyxd-url Overrides the nyxd URL provided either as an environment variable NYXD_VALIDATOR or in a config file --nym-api-url Overrides the validator API URL provided either as an environment variable API_VALIDATOR or in a config file --mixnet-contract-address Overrides the mixnet contract address provided either as an environment variable or in a config file --vesting-contract-address Overrides the vesting contract address provided either as an environment variable or in a config file -h, --help Print help ``` ## `account create` ```sh Create a new mnemonic - note, this account does not appear on the chain until the account id is used in a transaction Usage: nym-cli account create [OPTIONS] Options: --mnemonic Provide the mnemonic for your account. You can also provide this is an env var called MNEMONIC. --word-count -c, --config-env-file Overrides configuration as a file of environment variables. Note: individual env vars take precedence over this file. --nyxd-url Overrides the nyxd URL provided either as an environment variable NYXD_VALIDATOR or in a config file --nym-api-url Overrides the validator API URL provided either as an environment variable API_VALIDATOR or in a config file --mixnet-contract-address Overrides the mixnet contract address provided either as an environment variable or in a config file --vesting-contract-address Overrides the vesting contract address provided either as an environment variable or in a config file -h, --help Print help ``` ## `account balance` ```sh Gets the balance of an account Usage: nym-cli account balance [OPTIONS] [ADDRESS] Arguments: [ADDRESS] The account address to get the balance for Options: --denom Optional currency to show balance for --mnemonic Provide the mnemonic for your account. You can also provide this is an env var called MNEMONIC. -c, --config-env-file Overrides configuration as a file of environment variables. Note: individual env vars take precedence over this file. --hide-denom Optionally hide the denom --nyxd-url Overrides the nyxd URL provided either as an environment variable NYXD_VALIDATOR or in a config file --raw Show as a raw value --nym-api-url Overrides the validator API URL provided either as an environment variable API_VALIDATOR or in a config file --mixnet-contract-address Overrides the mixnet contract address provided either as an environment variable or in a config file --vesting-contract-address Overrides the vesting contract address provided either as an environment variable or in a config file -h, --help Print help ``` ## `account pub-key` ```sh Gets the public key of an account Usage: nym-cli account pub-key [OPTIONS] [ADDRESS] Arguments: [ADDRESS] Optionally, show the public key for this account address, otherwise generate the account address from the mnemonic Options: --from-mnemonic If set, get the public key from the mnemonic, rather than querying for it --mnemonic Provide the mnemonic for your account. You can also provide this is an env var called MNEMONIC. -c, --config-env-file Overrides configuration as a file of environment variables. Note: individual env vars take precedence over this file. --nyxd-url Overrides the nyxd URL provided either as an environment variable NYXD_VALIDATOR or in a config file --nym-api-url Overrides the validator API URL provided either as an environment variable API_VALIDATOR or in a config file --mixnet-contract-address Overrides the mixnet contract address provided either as an environment variable or in a config file --vesting-contract-address Overrides the vesting contract address provided either as an environment variable or in a config file -h, --help Print help ``` ## `account send` ```sh Sends tokens to another account Usage: nym-cli account send [OPTIONS] Arguments: The recipient account address Amount to transfer in micro denomination (e.g. unym or unyx) Options: --denom Override the denomination --mnemonic Provide the mnemonic for your account. You can also provide this is an env var called MNEMONIC. -c, --config-env-file Overrides configuration as a file of environment variables. Note: individual env vars take precedence over this file. --memo --nyxd-url Overrides the nyxd URL provided either as an environment variable NYXD_VALIDATOR or in a config file --nym-api-url Overrides the validator API URL provided either as an environment variable API_VALIDATOR or in a config file --mixnet-contract-address Overrides the mixnet contract address provided either as an environment variable or in a config file --vesting-contract-address Overrides the vesting contract address provided either as an environment variable or in a config file -h, --help Print help ``` ## `account send-multiple` ```sh Batch multiple token sends Usage: nym-cli account send-multiple [OPTIONS] --input Options: --memo --mnemonic Provide the mnemonic for your account. You can also provide this is an env var called MNEMONIC. -c, --config-env-file Overrides configuration as a file of environment variables. Note: individual env vars take precedence over this file. --input Input file path (CSV format) with account/amount pairs to send --nyxd-url Overrides the nyxd URL provided either as an environment variable NYXD_VALIDATOR or in a config file --output An output file path (CSV format) to create or append a log of results to --nym-api-url Overrides the validator API URL provided either as an environment variable API_VALIDATOR or in a config file --mixnet-contract-address Overrides the mixnet contract address provided either as an environment variable or in a config file --vesting-contract-address Overrides the vesting contract address provided either as an environment variable or in a config file -h, --help Print help ``` ## `signature` ```sh Sign and verify messages Usage: nym-cli signature [OPTIONS] nym-cli signature Commands: sign Sign a message verify Verify a message help Print this message or the help of the given subcommand(s) Options: --mnemonic Provide the mnemonic for your account. You can also provide this is an env var called MNEMONIC. -c, --config-env-file Overrides configuration as a file of environment variables. Note: individual env vars take precedence over this file. --nyxd-url Overrides the nyxd URL provided either as an environment variable NYXD_VALIDATOR or in a config file --nym-api-url Overrides the validator API URL provided either as an environment variable API_VALIDATOR or in a config file --mixnet-contract-address Overrides the mixnet contract address provided either as an environment variable or in a config file --vesting-contract-address Overrides the vesting contract address provided either as an environment variable or in a config file -h, --help Print help ``` ## `signature sign` ```sh Sign a message Usage: nym-cli signature sign [OPTIONS] Arguments: The message to sign Options: --mnemonic Provide the mnemonic for your account. You can also provide this is an env var called MNEMONIC. -c, --config-env-file Overrides configuration as a file of environment variables. Note: individual env vars take precedence over this file. --nyxd-url Overrides the nyxd URL provided either as an environment variable NYXD_VALIDATOR or in a config file --nym-api-url Overrides the validator API URL provided either as an environment variable API_VALIDATOR or in a config file --mixnet-contract-address Overrides the mixnet contract address provided either as an environment variable or in a config file --vesting-contract-address Overrides the vesting contract address provided either as an environment variable or in a config file -h, --help Print help ``` ## `signature verify` ```sh Verify a message Usage: nym-cli signature verify [OPTIONS] Arguments: The public key of the account, or the account id to query for a public key (NOTE: the account must have signed a message stored on the chain for the public key record to exist) The signature to verify as hex The message to verify as a string Options: --mnemonic Provide the mnemonic for your account. You can also provide this is an env var called MNEMONIC. -c, --config-env-file Overrides configuration as a file of environment variables. Note: individual env vars take precedence over this file. --nyxd-url Overrides the nyxd URL provided either as an environment variable NYXD_VALIDATOR or in a config file --nym-api-url Overrides the validator API URL provided either as an environment variable API_VALIDATOR or in a config file --mixnet-contract-address Overrides the mixnet contract address provided either as an environment variable or in a config file --vesting-contract-address Overrides the vesting contract address provided either as an environment variable or in a config file -h, --help Print help ``` ## `ecash` ```sh Ecash related stuff Usage: nym-cli ecash [OPTIONS] nym-cli ecash Commands: issue-ticket-book recover-ticket-book import-ticket-book generate-ticket import-coin-index-signatures import-expiration-date-signatures import-master-verification-key help Print this message or the help of the given subcommand(s) Options: --mnemonic Provide the mnemonic for your account. You can also provide this is an env var called MNEMONIC. -c, --config-env-file Overrides configuration as a file of environment variables. Note: individual env vars take precedence over this file. --nyxd-url Overrides the nyxd URL provided either as an environment variable NYXD_VALIDATOR or in a config file --nym-api-url Overrides the validator API URL provided either as an environment variable API_VALIDATOR or in a config file --mixnet-contract-address Overrides the mixnet contract address provided either as an environment variable or in a config file --vesting-contract-address Overrides the vesting contract address provided either as an environment variable or in a config file -h, --help Print help ``` ## `ecash issue-ticket-book` ```sh Usage: nym-cli ecash issue-ticket-book [OPTIONS] <--client-config |--output-file > Options: --mnemonic Provide the mnemonic for your account. You can also provide this is an env var called MNEMONIC. --ticketbook-type Specify which type of ticketbook should be issued [default: v1-mixnet-entry] -c, --config-env-file Overrides configuration as a file of environment variables. Note: individual env vars take precedence over this file. --client-config Config file of the client that is supposed to use the credential --nyxd-url Overrides the nyxd URL provided either as an environment variable NYXD_VALIDATOR or in a config file --output-file Output file for the ticketbook --bs58-output Specifies whether the output file should use binary or bs58 encoded data --nym-api-url Overrides the validator API URL provided either as an environment variable API_VALIDATOR or in a config file --include-expiration-date-signatures Specifies whether the file output should contain expiration date signatures --mixnet-contract-address Overrides the mixnet contract address provided either as an environment variable or in a config file --include-coin-index-signatures Specifies whether the file output should contain coin index signatures --vesting-contract-address Overrides the vesting contract address provided either as an environment variable or in a config file --include-master-verification-key Specifies whether the file output should contain master verification key --bs58-encoded-client-secret Secret value that's used for deriving underlying ecash keypair -h, --help Print help ``` ## `ecash recover-ticket-book` ```sh Usage: nym-cli ecash recover-ticket-book [OPTIONS] --client-config Options: --client-config Config file of the client that is supposed to use the credential --mnemonic Provide the mnemonic for your account. You can also provide this is an env var called MNEMONIC. -c, --config-env-file Overrides configuration as a file of environment variables. Note: individual env vars take precedence over this file. --nyxd-url Overrides the nyxd URL provided either as an environment variable NYXD_VALIDATOR or in a config file --nym-api-url Overrides the validator API URL provided either as an environment variable API_VALIDATOR or in a config file --mixnet-contract-address Overrides the mixnet contract address provided either as an environment variable or in a config file --vesting-contract-address Overrides the vesting contract address provided either as an environment variable or in a config file -h, --help Print help ``` ## `ecash import-ticket-book` ```sh Usage: nym-cli ecash import-ticket-book [OPTIONS] --credentials-store <--credential-data |--credential-path > <--standalone|--full> Options: --credentials-store Config file of the client that is supposed to use the credential --mnemonic Provide the mnemonic for your account. You can also provide this is an env var called MNEMONIC. -c, --config-env-file Overrides configuration as a file of environment variables. Note: individual env vars take precedence over this file. --credential-data Explicitly provide the encoded credential data (as base58) --credential-path Specifies the path to file containing binary credential data --nyxd-url Overrides the nyxd URL provided either as an environment variable NYXD_VALIDATOR or in a config file --nym-api-url Overrides the validator API URL provided either as an environment variable API_VALIDATOR or in a config file --standalone Specifies whether we're attempting to import a standalone ticketbook (i.e. serialised `IssuedTicketBook`) --full Specifies whether we're attempting to import full ticketboot (i.e. one that **might** contain required global signatures; that is serialised `ImportableTicketBook`) --mixnet-contract-address Overrides the mixnet contract address provided either as an environment variable or in a config file --vesting-contract-address Overrides the vesting contract address provided either as an environment variable or in a config file -h, --help Print help ``` ## `coconut` ## `coconut generate-freepass` ## `coconut issue-credentials` ## `coconut recover-credentials` ## `coconut import-credential` ## `block` ```sh Query chain blocks Usage: nym-cli block [OPTIONS] nym-cli block Commands: get Gets a block's details and prints as JSON time Gets the block time at a height current-height Gets the current block height help Print this message or the help of the given subcommand(s) Options: --mnemonic Provide the mnemonic for your account. You can also provide this is an env var called MNEMONIC. -c, --config-env-file Overrides configuration as a file of environment variables. Note: individual env vars take precedence over this file. --nyxd-url Overrides the nyxd URL provided either as an environment variable NYXD_VALIDATOR or in a config file --nym-api-url Overrides the validator API URL provided either as an environment variable API_VALIDATOR or in a config file --mixnet-contract-address Overrides the mixnet contract address provided either as an environment variable or in a config file --vesting-contract-address Overrides the vesting contract address provided either as an environment variable or in a config file -h, --help Print help ``` ## `block get` ```sh Gets a block's details and prints as JSON Usage: nym-cli block get [OPTIONS] Arguments: The block height Options: --mnemonic Provide the mnemonic for your account. You can also provide this is an env var called MNEMONIC. -c, --config-env-file Overrides configuration as a file of environment variables. Note: individual env vars take precedence over this file. --nyxd-url Overrides the nyxd URL provided either as an environment variable NYXD_VALIDATOR or in a config file --nym-api-url Overrides the validator API URL provided either as an environment variable API_VALIDATOR or in a config file --mixnet-contract-address Overrides the mixnet contract address provided either as an environment variable or in a config file --vesting-contract-address Overrides the vesting contract address provided either as an environment variable or in a config file -h, --help Print help ``` ## `block time` ```sh Gets the block time at a height Usage: nym-cli block time [OPTIONS] Arguments: The block height Options: --mnemonic Provide the mnemonic for your account. You can also provide this is an env var called MNEMONIC. -c, --config-env-file Overrides configuration as a file of environment variables. Note: individual env vars take precedence over this file. --nyxd-url Overrides the nyxd URL provided either as an environment variable NYXD_VALIDATOR or in a config file --nym-api-url Overrides the validator API URL provided either as an environment variable API_VALIDATOR or in a config file --mixnet-contract-address Overrides the mixnet contract address provided either as an environment variable or in a config file --vesting-contract-address Overrides the vesting contract address provided either as an environment variable or in a config file -h, --help Print help ``` ## `block current-height` ```sh Gets the current block height Usage: nym-cli block current-height [OPTIONS] Options: --mnemonic Provide the mnemonic for your account. You can also provide this is an env var called MNEMONIC. -c, --config-env-file Overrides configuration as a file of environment variables. Note: individual env vars take precedence over this file. --nyxd-url Overrides the nyxd URL provided either as an environment variable NYXD_VALIDATOR or in a config file --nym-api-url Overrides the validator API URL provided either as an environment variable API_VALIDATOR or in a config file --mixnet-contract-address Overrides the mixnet contract address provided either as an environment variable or in a config file --vesting-contract-address Overrides the vesting contract address provided either as an environment variable or in a config file -h, --help Print help ``` ## `cosmwasm` ```sh Manage and execute WASM smart contracts Usage: nym-cli cosmwasm [OPTIONS] nym-cli cosmwasm Commands: upload Upload a smart contract WASM blob init Init a WASM smart contract generate-init-message Generate an instantiate message migrate Migrate a WASM smart contract execute Execute a WASM smart contract method raw-contract-state Obtain raw contract state of a cosmwasm smart contract help Print this message or the help of the given subcommand(s) Options: --mnemonic Provide the mnemonic for your account. You can also provide this is an env var called MNEMONIC. -c, --config-env-file Overrides configuration as a file of environment variables. Note: individual env vars take precedence over this file. --nyxd-url Overrides the nyxd URL provided either as an environment variable NYXD_VALIDATOR or in a config file --nym-api-url Overrides the validator API URL provided either as an environment variable API_VALIDATOR or in a config file --mixnet-contract-address Overrides the mixnet contract address provided either as an environment variable or in a config file --vesting-contract-address Overrides the vesting contract address provided either as an environment variable or in a config file -h, --help Print help ``` ## `cosmwasm upload` ```sh Upload a smart contract WASM blob Usage: nym-cli cosmwasm upload [OPTIONS] --wasm-path Options: --mnemonic Provide the mnemonic for your account. You can also provide this is an env var called MNEMONIC. --wasm-path -c, --config-env-file Overrides configuration as a file of environment variables. Note: individual env vars take precedence over this file. --memo --nyxd-url Overrides the nyxd URL provided either as an environment variable NYXD_VALIDATOR or in a config file --nym-api-url Overrides the validator API URL provided either as an environment variable API_VALIDATOR or in a config file --mixnet-contract-address Overrides the mixnet contract address provided either as an environment variable or in a config file --vesting-contract-address Overrides the vesting contract address provided either as an environment variable or in a config file -h, --help Print help ``` ## `cosmwasm init` ```sh Init a WASM smart contract Usage: nym-cli cosmwasm init [OPTIONS] --init-message Arguments: Options: --memo --mnemonic Provide the mnemonic for your account. You can also provide this is an env var called MNEMONIC. -c, --config-env-file Overrides configuration as a file of environment variables. Note: individual env vars take precedence over this file. --label