Writing

How private is Monero? Ring entropy says 1.5 bits, not 4

Giuseppe Giona·
Key findings
  • • Monero ring size is 16. Theoretical entropy: log₂(16) = 4 bits. Effective entropy after OSPEAD analysis: ~1.5 bits.
  • • ~80% of real spends are the newest ring member. The anonymity set drops from 16 to ~3.
  • • Selecting decoys at indistinguishability ages (where P_spend/P_decoy = 1) recovers entropy to ~3.4 bits.

Monero ring size is 16. One real spend, 15 decoys. If all 16 were equally likely to be real: H = log₂(16) = 4 bits. An adversary guesses 1 in 16.

They're not equally likely. OSPEAD (Monero Research Lab, April 2025) separates the real spend distribution from the decoy distribution using the Bonhomme-Jochmans-Robin estimator and inverts the mixture with Patra-Sen. About 80% of real spends are the newest ring member.

The entropy calculation

With the newest member at 80% and the remaining 15 sharing 20%:

P = [0.80, 0.013, 0.013, 0.013, 0.013, 0.013, 0.013, 0.013,
     0.013, 0.013, 0.013, 0.013, 0.013, 0.013, 0.013, 0.013]

H = -Σ P(i) · log₂(P(i))
  = -(0.80 · log₂(0.80) + 15 · 0.013 · log₂(0.013))
  ≈ 1.5 bits

Effective anonymity set: 2^1.5 ≈ 2.8

From 4 bits to 1.5. The adversary's odds improve from 1-in-16 to roughly 1-in-3.

1.5 bits is real anonymity. It's not zero. But the gap between 4 and 1.5 is the gap between “nobody can trace this” and “a well-resourced adversary has a reasonable guess.”

Per-member likelihood ratios

Monero's wallet uses a gamma distribution for decoy selection (shape ≈ 19.28, scale ≈ 1/1.61 in log-seconds). Real spending is about 20% more concentrated at recent ages. For each ring member:

P(member_i is real) ∝ P_spend(age_i) / P_decoy(age_i)

High ratio: spending is much more likely than decoy selection at that age. The newest output almost always has the highest ratio.

Inverse-OSPEAD: reading it backwards

OSPEAD identifies the real spend. The inverse question: what decoy ages make identification impossible?

Find the ages where P_spend(age) / P_decoy(age) = 1. At these points, the adversary can't distinguish a decoy from a real spend. They're equally likely either way.

// Scan log-age space for indistinguishability points
for (let logAge = 5; logAge <= 20; logAge += 0.1) {
  const pSpend = gammaPdf(logAge, spendShape, spendScale)
  const pDecoy = gammaPdf(logAge, decoyShape, decoyScale)
  const ratio = pDecoy > 1e-15 ? pSpend / pDecoy : 0
  candidates.push({ logAge, age: Math.exp(logAge), ratio })
}

// Sort by |ratio - 1| — closest to 1 = best decoy age
candidates.sort((a, b) =>
  Math.abs(a.ratio - 1) - Math.abs(b.ratio - 1)
)

15 decoys at indistinguishability ages, spread to avoid clustering. Expected entropy: ~3.4 bits. Not the theoretical 4, but the adversary's advantage drops from 80% to about 40%.

What this doesn't cover

This analysis assumes a single-transaction adversary. Multi-transaction heuristics (Moser et al., 2018) can narrow rings further by correlating across the chain. The OSPEAD paper itself estimates a 23.5% attack success probability per transaction.

The protocol is improving — ring sizes have increased, the decoy algorithm has been updated, and FCMP++ aims to replace rings entirely with full-chain membership proofs. The fundamental tension (default decoy distribution is separable from real spend distribution) is structural until then.

Ring analysis and inverse-OSPEAD decoy constructor: ε-tx repo under packages/core/src/monero/. 13 tests covering ring entropy bounds, likelihood ratio monotonicity, and indistinguishability point detection.