Case Study

ε-tx

Privacy analysis for cryptocurrency transactions. Started as a maths dissertation on Bitcoin cryptography. This is where it went.

TypeScriptInformation TheoryBitcoinMoneroLightningDempster-ShaferVitest
00 / Live output

What a scan returns

etx analyse --adversary law-enforcement
  privacy score: 3.16 bits (medium)
  anonymity set: ~115
  evidence:      Bel(exposed)=0.42, conflict K=0.18

  breakdown:
    timing                  2.16 bits  ██████
    wallet-fingerprint      0.70 bits  ██
    amount-analysis         0.29 bits  █

  wallet: blue-wallet (79%) · p2wpkh+p2sh · bip69 · 0.70 bits
  timing: H=2.45 bits · UTC+0 · 10:00-24:00
  classification: normal-payment (89%)
  adversary: law-enforcement · effective 1.84 bits
    top threats: timing (1.5b), amount (0.2b), fingerprint (0.1b)

  fix:
    ! Switch to random ordering (−0.8b)
    ~ Avoid mixing script types (−0.5b)
    ~ Use CoinJoin for amount privacy (−0.5b)
01 / Engineering

What I had to think about

The API clients are plumbing. These are the parts where the maths matters.

02 / From the source

The maths is the implementation

Dempster-Shafer fusion, inverse-OSPEAD decoy selection, and differential privacy composition. From the repo.

Dempster’s Combination Rule
Fuses two mass functions. Conflict mass K normalises the result. Total conflict produces pure uncertainty.
function combine(m1: MassFunction, m2: MassFunction): MassFunction {
  const ee = m1.exposed * m2.exposed
  const eu = m1.exposed * m2.uncertain
  const ue = m1.uncertain * m2.exposed
  const pp = m1.private_ * m2.private_
  const pu = m1.private_ * m2.uncertain
  const up = m1.uncertain * m2.private_
  const uu = m1.uncertain * m2.uncertain

  const K = m1.exposed * m2.private_ + m1.private_ * m2.exposed
  const norm = 1 - K
  if (norm <= 0) return { exposed: 0, private_: 0, uncertain: 1 }

  return {
    exposed: (ee + eu + ue) / norm,
    private_: (pp + pu + up) / norm,
    uncertain: uu / norm,
  }
}
Inverse-OSPEAD: Optimal Decoy Selection
Finds ages where P_spend/P_decoy ≈ 1. Decoys at these ages are indistinguishable from real spends.
// 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 = maximum indistinguishability
candidates.sort((a, b) => Math.abs(a.ratio - 1) - Math.abs(b.ratio - 1))

// Select top 15 with minimum spacing (avoid clustering fingerprint)
for (const c of candidates) {
  if (selected.length >= numDecoys) break
  const rounded = Math.round(c.logAge * 2) / 2
  if (usedLogAges.has(rounded)) continue
  usedLogAges.add(rounded)
  selected.push(c.age)
}
Cross-Chain Privacy Composition
Basic (Dwork 2006) and advanced (Dwork, Rothblum, Vadhan 2010) composition theorems for multi-chain transfers.
// Basic sequential composition: ε_total ≤ Σ ε_i
const basicComposition = epsilons.reduce((s, e) => s + e, 0)

// Advanced composition: √(2k·ln(1/δ))·max(ε) + k·max(ε)²
const maxEps = Math.max(...epsilons)
const advancedComposition =
  Math.sqrt(2 * k * Math.log(1 / delta)) * maxEps
  + k * maxEps * maxEps

// Use basic for small k (tighter), advanced for large k
const recommended = k < 10 ? basic : Math.min(basic, advanced)
0
attack surfaces
0
tests
0
papers cited
0
source modules