Terminology
The key words "MUST", "MUST NOT", "SHOULD", "SHOULD NOT", "MAY", and "RECOMMENDED" in this document are to be interpreted as described in BCP 14 1 when, and only when, they appear in all capitals.
Stem phase: The unicast relay phase of the Dandelion++ protocol 2 in which a transaction is forwarded to a single randomly-selected peer per epoch before entering the fluff phase.
Fluff phase: The standard flood-broadcast phase of transaction propagation that follows the stem phase.
Stem peer: The single P2P peer to which a Dandelion++ node relays stem-phase transactions during a given epoch.
IP-to-transaction correlation: The ability of a network observer to determine which IP address originated a given transaction by observing when and where the transaction first appeared on the P2P network.
Abstract
This ZIP specifies how Zcash full nodes SHOULD implement Dandelion++ 2 transaction propagation. In Dandelion++, a newly-received transaction first travels through a randomly-selected "stem" of peers (unicast per epoch) before entering a standard flood broadcast ("fluff" phase). This makes it significantly harder for network observers to determine which node originated a transaction, reducing IP-to-transaction correlation.
This ZIP covers only the full-node P2P relay behavior (sometimes called "Component B"). A companion mechanism for wallets to submit transactions directly to a node via P2P rather than through a compact-block server is described in draft-zodl-dandelion-wallet (PR #1380). 3
Motivation
Currently, Zcash transactions are immediately flood-broadcast to all connected peers the moment they enter a node's mempool. A passive network adversary can observe the timing of transaction appearances at many vantage points to identify the originating node with high confidence, even for shielded transactions whose content is cryptographically protected.
Dandelion++ addresses this by introducing an intermediate "stem" phase during which the transaction is relayed to only one peer. The originating node is hidden in the anonymity set of all nodes along the stem path.
Requirements
-
Each Dandelion++ node MUST maintain an epoch with a randomly-chosen duration in the range [600, 720] seconds (10 minutes ± 120 second jitter).
-
At the start of each epoch, the node MUST select a single stem peer uniformly at random from its currently-connected outbound peers. If no outbound peers are available, the node operates in fluff mode for that epoch.
-
For each transaction received locally (via RPC, direct P2P submission, or relayed from a peer), the node MUST:
a. Enter the transaction into stem phase: route it exclusively to the stem peer using a unicast P2P message. b. Record the stem-phase start time. If the transaction has not entered fluff within 30 seconds, the node MUST promote it to fluff phase. c. If the stem peer disconnects or becomes unavailable before the transaction is acknowledged, the node MUST immediately promote the transaction to fluff phase rather than silently dropping it.
-
During stem phase, the node MUST NOT advertise the transaction to any other peer. Specifically:
a. The transaction MUST NOT be included in responses to
mempoolP2P requests from remote peers. b. The transaction MUST NOT be returned by any RPC that exposes mempool contents (e.g.getrawmempool,getrawtransactionwithverbose=0on unconfirmed transactions) until it enters fluff phase. -
Once the transaction enters fluff phase, it MUST be broadcast normally using the existing
inv/txflood mechanism.
Specification
Epoch management
A new epoch begins immediately when the node starts and whenever the previous epoch expires. The epoch duration is:
epoch_duration = BASE_EPOCH_DURATION + uniform_random(0, MAX_EPOCH_JITTER)
where BASE_EPOCH_DURATION = 600 s and MAX_EPOCH_JITTER = 120 s.
The stem peer for an epoch is selected uniformly at random from the addresses of
currently-connected outbound peers at the moment the epoch begins. If no outbound
peers are connected, the stem peer is None and the node operates in fluff mode
for the entire epoch (all transactions are immediately flood-broadcast).
Stem-phase forwarding
When a node enters a new transaction into stem phase, it SHOULD send the transaction
identifier to the stem peer using the standard inv P2P message with type
MSG_TX. The stem peer is expected to request the full transaction via getdata
and continue the stem relay using its own stem peer for the current epoch.
Note: The Dandelion++ paper defines a separate "unadvertised
tx" convention (sending the rawtxmessage to the stem peer without a priorinv) as a signal that the transaction should be treated as stem-phase by the recipient. This convention is OPTIONAL under this ZIP. Nodes that do not implement it MUST fall back gracefully to standardinvforwarding, which provides the same privacy property at the forwarding node. A future ZIP MAY standardize the unadvertised-txconvention once implementations converge.
Stem-phase mempool filtering
Implementations MUST ensure that stem-phase transactions are not observable by remote peers before entering fluff. This applies to:
-
Responses to
mempoolP2P requests — the node MUST filter stem-phase transaction IDs from the list returned to any remote peer. -
Transaction-related RPCs — the node SHOULD suppress stem-phase transactions from any RPC that returns mempool contents. Implementations MAY defer this to a subsequent release.
Fail-closed routing
If the stem peer is not reachable at the time a stem-phase transaction should be forwarded (e.g. the peer has been evicted from the ready set), the node MUST promote the transaction to fluff immediately. The node MUST NOT silently discard the transaction, and MUST NOT fall back to routing to a different peer without updating the propagation state (which would result in the transaction being advertised to a non-stem peer during what the node still records as stem phase).
Fluff phase
After the 30-second stem timeout, or when a stem-peer failure triggers early
promotion, the transaction enters fluff phase and MUST be broadcast to all
connected peers using the normal inv + getdata + tx mechanism.
Rationale
Why per-epoch rather than per-transaction stem peer selection?
The Dandelion++ paper recommends per-transaction selection for maximum anonymity. However, per-epoch selection has an important advantage: an adversary who controls multiple peers cannot learn which peer consistently forwards a given sender's transactions by comparing which peer they saw first for each transaction. With per-epoch selection, an adversary learns only one stem identity per 10-minute window. The privacy trade-off is well-documented in 2.
Why 30 seconds for the stem timeout?
The Dandelion++ paper recommends a stem timeout in the range 30–60 seconds. The lower bound is used here to minimize transaction propagation delay under normal conditions. If a transaction is rejected by the stem peer (e.g. due to policy), the 30-second timeout ensures it is not silently dropped.
Why not a new P2P message type?
Defining a new P2P message type would require a network upgrade and coordination
across all Zcash full node implementations. Using the existing inv message for
stem forwarding achieves the same routing behavior with no protocol-level changes,
allowing incremental deployment. Nodes that do not implement this ZIP process the
inv normally and flood-broadcast the transaction, which is the safe fallback.
Reference Implementation
Component B — Full-node Dandelion++ relay (Zebra)
Reference implementation proposed upstream: zcashfoundation/zebra#10928.
Verified end-to-end on a 3-node regtest network (2026-07-09). See
docs/dandelion-e2e-regtest.md in that PR for the complete test procedure,
build flags, and result evidence (txid accepted, 30 s stem-retention confirmed,
adversary observer saw no inv during the stem window).
Implemented:
- Epoch management with 10 min base + up to 2 min jitter
(
zebra-network/src/dandelion/epoch.rs) - Per-transaction propagation state, 30 s stem timeout,
stem_txids()(zebra-network/src/dandelion/state.rs) - Fail-closed stem unicast via
PeerSet::route_to_peerwithPeerError::NoReadyPeers(zebra-network/src/peer_set/set.rs) - Gossip task wired into
zebradstartup;MempoolChangeKind::StemAddedroutes locally-submitted txs through stem; peer-relayedAddedgoes directly to fluff (zebrad/src/components/mempool/dandelion_gossip.rs,mempool.rs) - Phase 4 — stem-phase hiding from all exposure surfaces:
MempoolTransactionIdsP2P response (inbound handler)TransactionsByIdP2P getdata handler (adversary cannot fetch full tx during stem)getrawmempoolRPC (verbose and non-verbose)getrawtransactionRPC- Zebra indexer gRPC
mempool_changestream (suppressesStemAddedevents; Zaino/lightwalletd only sees the tx once it enters fluff)
- 10 unit tests (epoch, propagation state, Phase 4 filter) + 2 routing tests
Remaining (optional / future coordination): unadvertised-tx wire convention
(requires other implementations to recognize the signal).
Component A — Wallet direct P2P submission
Proposed upstream to the Zcash wallet SDKs:
- Android: zcash/zcash-android-wallet-sdk#2010
- iOS (Swift): zcash/zcash-swift-wallet-sdk#1804
Both SDKs add a DandelionSubmitConfig opt-in (DirectP2P / directP2P) that:
- Resolves peers via the Zcash mainnet DNS seeders (same set as Zebra defaults).
- Opens a TCP connection directly to a full node.
- Performs the Zcash P2P version/verack handshake.
- Sends the raw
txmessage without a priorinv(the unadvertised-tx convention described in §Stem-phase forwarding above). - Disconnects.
Chain synchronisation always uses the lightwalletd endpoint regardless of this setting. A fallback to lwd submission is available when all P2P peers are unreachable.
The wallet-side specification (peer discovery, tx message format, error handling) is in the companion draft (PR #1380). 3
Changelog
- 2026-07-09: Initial draft. Component B reference implementation complete (Zebra).
- 2026-07-09: Component A reference implementations added (Android + iOS SDKs). Updated Reference Implementation section to list all PRs.
- 2026-07-09: Reference implementations proposed upstream — Zebra Component B to
zcashfoundation/zebra, and Component A tozcash/zcash-android-wallet-sdk#2010andzcash/zcash-swift-wallet-sdk#1804.
References
Footnotes
-
Venkatakrishnan, S.B., Fanti, G., Viswanath, P. "Dandelion++: Lightweight Cryptocurrency Networking with Formal Anonymity Guarantees." ACM SIGMETRICS 2018. https://arxiv.org/abs/1805.11060 ↩ ↩2 ↩3
-
draft-zodl-dandelion-wallet: Direct Wallet Transaction Submission via Dandelion++ P2P Relay. PR: https://github.com/zcash/zips/pull/1380 (In preparation — see zcash/zips#1380.) ↩ ↩2