DigiByte v8.22.2 implements Dandelion++, an enhanced privacy protocol for transaction propagation based on BIP-156. This document provides a comprehensive analysis of every aspect of the Dandelion++ implementation in DigiByte's codebase, serving as the definitive reference for porting to v8.26.
- Protocol Summary
- File Structure and Dependencies
- Core Implementation Details
- Data Structures and Classes
- Key Functions and Methods
- Message Flow and Processing
- Stempool Implementation
- Embargo System
- Route Management
- Logging and Debugging
- Configuration and Constants
- Testing
- Integration Points
- Critical Implementation Notes
Dandelion++ is a privacy-enhancing transaction routing mechanism that protects users from network-level deanonymization attacks. It operates in two distinct phases:
- Stem Phase: Transactions are forwarded along a linear path through randomly selected peers
- Fluff Phase: Transactions are broadcast using traditional flooding propagation
The protocol provides near-optimal anonymity guarantees by breaking the symmetry of transaction propagation patterns.
DigiByte implements Dandelion++ (the enhanced version) with these key differences:
- Two destinations instead of one for improved reliability
- Embargo system for additional timing obfuscation
- Discovery mechanism using special inventory messages
- Stempool as a separate transaction pool for stem phase
The main Dandelion++ implementation file containing:
- Connection management functions
- Route selection and shuffling algorithms
- Embargo management
- Thread management for periodic shuffling
- Debug string generation
All Dandelion methods are declared within the CConnman class in net.h
Network protocol definitions:
// Message type for stem phase transactions
extern const char *DANDELIONTX;
// Inventory types
MSG_DANDELION_TX = 5
MSG_DANDELION_WITNESS_TX = MSG_DANDELION_TX | MSG_WITNESS_FLAG
// In protocol.cpp
const char *DANDELIONTX = "dandeliontx";Network layer integration with Dandelion data structures and methods:
- Peer vectors and routing maps
- Configuration constants
- PoissonNextSend function
- Connection handling
Message processing and transaction relay:
- ProcessMessage handling for DANDELIONTX
- RelayDandelionTransaction implementation
- CheckDandelionEmbargoes periodic check
- Fluff probability constant
Transaction broadcasting interface:
- Wallet transaction submission to stempool
- Embargo creation for local transactions
- Dandelion routing for wallet transactions
Memory pool with stempool support:
- m_is_stempool flag
- isStempool() method
- Constructor modification for stempool creation
Transaction validation with stempool:
- Stempool parameter in CChainState
- AcceptToMemoryPool handling for stempool
- Reorg handling for both pools
/src/wallet/wallet.h- DEFAULT_DISABLE_DANDELION constant/src/wallet/init.cpp- Command-line option "-disabledandelion"/src/dummywallet.cpp- Stub implementation
/src/init.cpp- Stempool creation and thread startup/src/logging.h&/src/logging.cpp- BCLog::DANDELION category
/src/Makefile.am- Includes dandelion.cpp in build
// Network Constants (net.h)
static const bool DEFAULT_DANDELION = true;
static const int DANDELION_MAX_DESTINATIONS = 2;
static constexpr auto DANDELION_SHUFFLE_INTERVAL = 10min;
static constexpr auto DANDELION_EMBARGO_MINIMUM = 10s;
static constexpr auto DANDELION_EMBARGO_AVG_ADD = 20s;
static const uint256 DANDELION_DISCOVERYHASH = uint256S("0xfff...fff");
// Processing Constants (net_processing.h)
static const unsigned int DANDELION_FLUFF = 10; // 10% fluff probability
// Wallet Constants (wallet.h)
static const bool DEFAULT_DISABLE_DANDELION = false;// Peer Management Vectors
std::vector<CNode*> vDandelionInbound; // Can send us stem txs
std::vector<CNode*> vDandelionOutbound; // We can send stem txs to
std::vector<CNode*> vDandelionDestination; // Selected routing destinations
// Routing Infrastructure
std::map<CNode*, CNode*> mDandelionRoutes; // Inbound->outbound mapping
CNode* localDandelionDestination = nullptr; // Our own stem destination
// Embargo Management
std::map<uint256, std::chrono::microseconds> mDandelionEmbargo;
// Thread Management
std::thread threadDandelionShuffle;class CTxMemPool {
private:
const bool m_is_stempool;
public:
explicit CTxMemPool(CBlockPolicyEstimator* estimator = nullptr,
int check_ratio = 0,
bool isStempool = false);
bool isStempool() const { return m_is_stempool; }
};struct NodeContext {
std::unique_ptr<CTxMemPool> mempool;
std::unique_ptr<CTxMemPool> stempool; // Separate pool for stem txs
};bool CConnman::isDandelionInbound(const CNode* const pnode) constChecks if a peer is in the Dandelion inbound vector.
bool CConnman::setLocalDandelionDestination()Sets the local node's Dandelion destination for outbound stem transactions.
CNode* CConnman::getDandelionDestination(CNode* pfrom)Gets or creates a Dandelion route for an inbound peer.
bool CConnman::localDandelionDestinationPushInventory(const CInv& inv)Pushes inventory to the local Dandelion destination.
CNode* CConnman::SelectFromDandelionDestinations() constSelects a destination with the fewest routes (load balancing).
void CConnman::CloseDandelionConnections(const CNode* const pnode)Comprehensive cleanup when a Dandelion peer disconnects:
- Removes from all vectors
- Updates routes
- Selects replacement if needed
bool CConnman::insertDandelionEmbargo(const uint256& hash, std::chrono::microseconds& embargo)Creates an embargo entry for a transaction.
bool CConnman::isTxDandelionEmbargoed(const uint256& hash) constChecks if a transaction is currently embargoed.
bool CConnman::removeDandelionEmbargo(const uint256& hash)Removes an embargo entry.
void CConnman::DandelionShuffle()Complete route reshuffling algorithm:
- Clears all existing routes
- Clears destinations
- Randomly selects new destinations (max 2)
- Regenerates all inbound routes
void CConnman::ThreadDandelionShuffle()Thread function that:
- Waits for IBD completion
- Shuffles routes every ~10 minutes (randomized)
- Uses PoissonNextSend for timing
void PeerManagerImpl::RelayDandelionTransaction(const CTransaction& tx, CNode* pfrom)Core relay logic:
FastRandomContext rng;
bool willFluff = rng.randrange(100) < DANDELION_FLUFF; // 10% chance
if (willFluff) {
// Move from stempool to mempool
CTransactionRef ptx = m_stempool.get(tx.GetHash());
AcceptToMemoryPool(chainstate, m_mempool, ptx, false);
RelayTransaction(tx.GetHash(), tx.GetWitnessHash());
} else {
// Continue stem phase
CNode* destination = m_connman.getDandelionDestination(pfrom);
if (destination) {
CInv inv(MSG_DANDELION_TX, tx.GetHash());
destination->PushOtherInventory(inv);
}
}void PeerManagerImpl::CheckDandelionEmbargoes()Periodic check that:
- Removes expired embargoes
- Moves embargoed transactions to mempool if found there
- Handles cleanup for missing transactions
std::chrono::microseconds PoissonNextSend(std::chrono::microseconds now,
std::chrono::seconds average_interval)Generates exponentially distributed random delays for:
- Embargo timeouts
- Route shuffling intervals
- Enhanced privacy through timing obfuscation
std::string CConnman::GetDandelionRoutingDataDebugString() constGenerates detailed debug output showing:
- All Dandelion vectors
- Current routes
- Local destination
if (gArgs.GetBoolArg("-dandelion", DEFAULT_DANDELION)) {
// Submit to stempool
AcceptToMemoryPool(chainstate, *node.stempool, tx, false, false);
// Create embargo
auto current_time = GetTime<std::chrono::milliseconds>();
std::chrono::microseconds nEmbargo = DANDELION_EMBARGO_MINIMUM +
PoissonNextSend(current_time, DANDELION_EMBARGO_AVG_ADD);
node.connman->insertDandelionEmbargo(txid, nEmbargo);
// Send via Dandelion
CInv embargoTx(MSG_DANDELION_TX, txid);
node.connman->localDandelionDestinationPushInventory(embargoTx);
}if (msg_type == NetMsgType::DANDELIONTX) {
CTransactionRef ptx;
vRecv >> ptx;
const CTransaction& tx = *ptx;
CInv inv(MSG_DANDELION_TX, tx.GetHash());
if (m_connman.isDandelionInbound(&pfrom)) {
if (!m_stempool.exists(inv.hash)) {
MempoolAcceptResult result = AcceptToMemoryPool(
m_chainman.ActiveChainstate(), m_stempool, ptx, false);
if (result.m_result_type == MempoolAcceptResult::ResultType::VALID) {
// Create embargo
auto current_time = GetTime<std::chrono::milliseconds>();
std::chrono::microseconds nEmbargo = DANDELION_EMBARGO_MINIMUM +
PoissonNextSend(current_time, DANDELION_EMBARGO_AVG_ADD);
m_connman.insertDandelionEmbargo(tx.GetHash(), nEmbargo);
// Relay onward
RelayDandelionTransaction(tx, &pfrom);
}
}
}
}// When accepting inbound connection
vDandelionInbound.push_back(pnode);
CNode* pto = SelectFromDandelionDestinations();
if (pto) {
mDandelionRoutes.insert(std::make_pair(pnode, pto));
}// When making outbound connection
vDandelionOutbound.push_back(pnode);
if (vDandelionDestination.size() < DANDELION_MAX_DESTINATIONS) {
vDandelionDestination.push_back(pnode);
}
// Send discovery message
CInv dummyInv(MSG_DANDELION_TX, DANDELION_DISCOVERYHASH);
pnode->PushInventory(dummyInv);// In ProcessGetData (net_processing.cpp)
if (inv.IsDandelionMsg() && pfrom.m_tx_relay->setDandelionInventoryKnown.count(inv.hash)) {
// Check embargo first
if (m_connman.isTxDandelionEmbargoed(inv.hash)) {
vNotFound.push_back(inv);
continue;
}
// If not embargoed, send if we have it
if (txinfo.tx) {
m_connman.PushMessage(&pfrom, msgMaker.Make(nSendFlags,
NetMsgType::DANDELIONTX, *txinfo.tx));
} else {
vNotFound.push_back(inv);
}
}// Create separate stempool instance
node.stempool = std::make_unique<CTxMemPool>(nullptr, 0, true);
// Initialize chainstate with both pools
chainman.InitializeChainstate(Assert(node.mempool.get()),
Assert(node.stempool.get()));CChainState::CChainState(CTxMemPool* mempool, CTxMemPool* stempool,
BlockManager& blockman,
std::optional<uint256> from_snapshot_blockhash)
: m_mempool(mempool),
m_stempool(stempool),
m_params(::Params()),
m_blockman(blockman),
m_from_snapshot_blockhash(from_snapshot_blockhash) {}Both pools are updated during reorgs:
// Remove from both pools
m_mempool->removeRecursive(**it, MemPoolRemovalReason::REORG);
m_stempool->removeRecursive(**it, MemPoolRemovalReason::REORG);
// Update both pools
m_mempool->UpdateTransactionsFromBlock(vHashUpdate);
m_stempool->UpdateTransactionsFromBlock(vHashUpdate);
// Remove immature from both
m_mempool->removeForReorg(*this, STANDARD_LOCKTIME_VERIFY_FLAGS);
m_stempool->removeForReorg(*this, STANDARD_LOCKTIME_VERIFY_FLAGS);
// Limit both pool sizes
LimitMempoolSize(*m_mempool, ...);
LimitMempoolSize(*m_stempool, ...);- No GetMainSignals notification for stempool additions:
if (!this->m_pool.isStempool()) {
GetMainSignals().TransactionAddedToMempool(ptx, m_pool.GetAndIncrementSequence());
}- Separate validation from regular mempool
- Same size limits as regular mempool
- Moves to mempool during fluff phase
Embargoes prevent timing analysis attacks by:
- Delaying responses to GETDATA requests
- Using randomized delays (10-30 seconds)
- Responding with NOTFOUND during embargo period
- Creation (when receiving/creating stem tx):
auto current_time = GetTime<std::chrono::milliseconds>();
std::chrono::microseconds nEmbargo = DANDELION_EMBARGO_MINIMUM +
PoissonNextSend(current_time, DANDELION_EMBARGO_AVG_ADD);
m_connman.insertDandelionEmbargo(tx.GetHash(), nEmbargo);- Checking (in isTxDandelionEmbargoed):
auto now = GetTime<std::chrono::microseconds>();
if (now < it->second) {
return true; // Still embargoed
}
return false; // Embargo expired- Periodic Cleanup (CheckDandelionEmbargoes):
- Called periodically in message processing
- Removes expired embargoes
- Handles transactions that moved to mempool
- Count connections per destination
- Find destinations with minimum connections
- Randomly select from candidates
- Ensures load balancing across destinations
- Triggered every ~10 minutes (randomized via PoissonNextSend)
- Complete reset:
- All routes cleared
- Destinations re-selected
- New routes generated
- Logging of before/after state
Comprehensive cleanup when peer disconnects:
- Remove from vDandelionInbound
- Remove from vDandelionOutbound
- Remove from vDandelionDestination
- If was destination, select replacement
- Update all routes using this peer
- Replace localDandelionDestination if needed
- Category: BCLog::DANDELION
- Enable:
-debug=dandelion
- Route Changes:
LogPrint(BCLog::DANDELION, "Set local Dandelion destination:\n%s",
GetDandelionRoutingDataDebugString());- Transaction Flow:
LogPrint(BCLog::DANDELION, "Dandelion fluff: %s\n", tx.GetHash().ToString());
LogPrint(BCLog::DANDELION, "dandeliontx %s embargoed for %d seconds\n",
txid.ToString(), embargo_timeout);- Shuffle Events:
LogPrint(BCLog::DANDELION, "Before Dandelion shuffle:\n%s",
GetDandelionRoutingDataDebugString());
LogPrint(BCLog::DANDELION, "After Dandelion shuffle:\n%s",
GetDandelionRoutingDataDebugString());vDandelionInbound: 1 2 3
vDandelionOutbound: 4 5 6
vDandelionDestination: 4 5
mDandelionRoutes: (1,4) (2,5) (3,4)
localDandelionDestination: 4
-
-dandelion=<0|1>(default: 1)- Enables/disables Dandelion++ protocol
- Checked in node/transaction.cpp for wallet transactions
-
-disabledandelion(wallet option)- Disables Dandelion support for wallet
- Added in wallet/init.cpp
-
-debug=dandelion- Enables detailed Dandelion logging
-
Message Types:
dandeliontx- Stem phase transaction message
-
Inventory Types:
MSG_DANDELION_TX = 5MSG_DANDELION_WITNESS_TX = MSG_DANDELION_TX | MSG_WITNESS_FLAG
-
Discovery Hash:
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff- Used to detect Dandelion capability
Tests three key properties:
-
Resistance to Active Probing:
- Transaction sent via stem
- Immediate GETDATA returns NOTFOUND
- Validates embargo system
-
Loop Behavior:
- Tests behavior after ~5 seconds
- Should still return NOTFOUND
-
Resistance to Black Holes:
- After ~45 seconds
- Transaction should be available
- Tests embargo expiration
# Three node ring: 0 --> 1 --> 2 --> 0
self.extra_args = [["-dandelion=1"] for i in range(3)]Found in:
/src/test/denialofservice_tests.cpp- DoS protection/src/test/validation_*_tests.cpp- Validation with stempool/src/wallet/test/wallet_tests.cpp- Wallet integration
node.peerman = PeerManager::make(chainparams, *node.connman, *node.addrman,
node.banman.get(), *node.scheduler, chainman,
*node.mempool, *node.stempool, ignores_incoming_txs);- Checks
-dandelionflag - Routes to stempool vs mempool
- Creates embargo for stem transactions
- Special handling for DANDELIONTX messages
- Embargo checks in GETDATA processing
- Periodic embargo cleanup
- Dandelion setup on connection
- Route generation
- Cleanup on disconnection
- ThreadDandelionShuffle started after networking
- Waits for IBD completion
- Runs until shutdown
- All Dandelion vectors protected by cs_vNodes
- Embargo map accessed under connman lock
- Route modifications synchronized
- Stempool has same size limits as mempool
- Transactions exist in either stempool OR mempool, never both
- Proper cleanup on disconnection
- PoissonNextSend prevents timing correlation
- Per-inbound-edge routing prevents fingerprinting
- Limited destinations reduce graph analysis
- Non-Dandelion nodes handled gracefully
- Discovery mechanism for capability detection
- Fallback to regular broadcast if no stem peers
- 10% fluff probability ensures propagation
- Embargo timeouts prevent indefinite delays
- Route shuffling limits learning attacks
- Missing routes trigger immediate fluff
- Stem transactions are never signed as witness in inventory
- Embargoes use microsecond precision for timing
- Route selection uses load balancing not pure random
- Stempool transactions don't trigger wallet notifications
- CheckDandelionEmbargoes called twice in ProcessMessage
- Two destinations instead of one
- Embargo system not in original BIP-156
- Stempool as separate data structure
- Discovery mechanism for capability detection
- Load-balanced route selection
When porting to v8.26, ensure:
- All 27 files with Dandelion code are updated
- Stempool initialization in init.cpp
- PeerManager constructor accepts stempool
- CChainState constructor accepts stempool
- Network message handlers for DANDELIONTX
- Protocol constants (MSG_DANDELION_TX = 5)
- Thread management in CConnman
- Wallet integration for stem transactions
- Embargo system fully implemented
- Route shuffling thread started
- Logging category added
- Command line options working
- Test coverage ported
- All constants match v8.22.2 values
- Debug string generation working
- Reorg handling for both pools
- Connection lifecycle management
- Load balancing in route selection
- Discovery hash mechanism
- Proper thread shutdown handling
DigiByte's Dandelion++ implementation in v8.22.2 is a sophisticated privacy enhancement that:
- Routes transactions through random linear paths before broadcasting
- Uses a separate stempool for stem phase transactions
- Implements embargoes to prevent timing analysis
- Periodically reshuffles routes to prevent learning attacks
- Maintains compatibility with non-Dandelion nodes
- Provides comprehensive logging for debugging
The implementation spans 27 files with careful integration throughout the codebase, requiring attention to thread safety, memory management, and network protocol details when porting to v8.26.