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