TL;DR. JoinMarket market makers reuse the same coins across many CoinJoins, and what they leave behind (change outputs with predictable values, respent round after round) lets a patient chain analyst cluster a maker's history for free, passively, from public chain data. We spent a while researching and prototyping ways out, and this writeup collects what we found. Everything below is demonstrated with real transactions on Bitcoin signet:
- Makers should also be Lightning swap providers. No protocol change at all, it earns fees, and in our simulations it fragments a maker's history by two orders of magnitude. It also gives takers something we think is underrated, and arguably the most novel point here: a maker's ordinary spends are the taker's real hide set. Every equal output is spent eventually, and if makers only ever cycle coins back into rounds, the spends that leave the CoinJoin cluster are disproportionately takers. Makers who clear swaps spend constantly, so maker swaps are as much a taker-anonymity mechanism as a maker defense (Section 4).
- Change as a co-funded Lightning channel. Two participants' change becomes one channel funding output; the split between them is invisible and can keep changing off-chain.
- Change as a shared 2-of-2 coinpool. The same hidden-split idea without running Lightning, secured by one pre-signed exit transaction. Simpler, but it gives up the off-chain re-randomization, and we show honestly where that bites.
Working on 2 and 3 side by side taught us the most useful lesson: they are the same construction at two points on one axis, "how safely can a hidden ownership split change before it settles on-chain?" The coinpool is a Lightning channel minus safe off-chain balance updates, and that missing piece is exactly its residual leak. No 2-party trick beats an observer who can attribute every round (totals are conserved), but each step here moves the attack from free dragnet to paid, targeted forensics. Our current belief: ship these as tiers that compose, not rivals.
One caveat to carry through the whole read: not every tier severs the chain. The whole-coin swap cut and the Lightning ring genuinely break a maker's peel chain; the coinpool (our default tier) only delays attribution until an endpoint event resolves it, because a plain roll-forward conserves the hidden split. The break for the coinpool lands only if in-CoinJoin re-splits work out, which we have not built (Section 6). We flag this up front so the honesty is not buried.
Every claim below is made against one of a small set of adversaries. Pulling them into one table keeps the later sections legible: each construction targets a specific row, and none of them beats the last one.
| Adversary | What it sees | What it cannot see | Which tier answers it |
|---|---|---|---|
| Global passive observer (the free dragnet) | all public chain data, all advertised fees, forever; no wire access | which inputs and change shared an owner inside a round | the whole design is built against this one |
| Active prober | the above, plus the co-owned UTXOs a maker offers when it opens a round it then abandons (an Eve-Alice-Eve counterparty probe) | anything the maker does not reveal on the wire | fee homogeneity limits the fingerprint; the break is still on-chain shape |
| Counterparty (own leg) | its swap segment boundary, or its own coin's split in a pool or channel | any other party's split; the maker's future respends | a decentralized market keeps each such view to one hop |
| Sybil maker | many legs at once, by posing as many partners | more than fidelity bonds let it afford | fidelity-bond-weighted partner selection prices it |
| Taker-as-coordinator | as deployed, every maker's change and the pairing and split of the round it funds | rounds it does not fund; any maker's future respends | acceptable per-round leak; maker-to-maker rendezvous is the hardening (Section 9) |
| Bayesian attacker | all of the passive view plus the open-source split sampler and the adoption rate; weights every consistent interpretation | still not the split itself, if the sampler is high-entropy | high-entropy sampler and broad adoption (Section 6) |
The last row is the one we simulate hardest (Sections 2 and 6): a passive observer that knows the protocol and the adoption rate, not a naive one that treats every ambiguity as a coin flip. That is the adversary a chain-analysis firm actually is. Two properties recur and are worth stating once: the per-round anonymity ceiling is always N (you are visibly one of the round's participants), and aggregates are conserved (an adversary that fully attributes every round can compute per-party totals no matter how splits are shuffled). Everything here targets the much larger space of weaker adversaries, where the useful property is persistence: a round left unattributed should stay that way, not be unwound backwards from one solved endpoint.
A CoinJoin is one Bitcoin transaction that several people build together for privacy: many inputs go in, and the outputs are arranged so an observer cannot cleanly tell whose input paid whose output.
JoinMarket is one popular style. It uses equal-sized outputs and a market:
- A taker wants privacy now, pays a small fee, and picks any round amount it likes.
- Makers provide liquidity and earn that fee. Fidelity bonds (timelocked bitcoin) make running many fake maker identities expensive. (The bond is also a linkage risk of its own, since it is public and identity-bound; Section 9.)
- Everyone gets back one equal output of the round amount plus a change output for the leftover. The taker hides among the identical equal outputs.
A maker keeps mixing the same coins for months. Each round consumes some of its coins and leaves an equal output and change behind, ready for the next round. That reuse is where the trouble starts.
Within a single round the equal outputs really are interchangeable. The damage happens across rounds, through three structural edges that no fee policy or wallet hygiene hides:
- The change peel chain. A maker's change has a nearly deterministic
value (
inputs - round amount - tx fee + maker fee), goes back to its wallet, and eventually funds a later round. That respend is an ordinary on-chain spend: the analyst does not have to guess it, just follow it. What it has to guess is which inputs and which change belonged together inside each round, and each round's amounts admit only a few consistent groupings. - Co-spends. Whenever the maker funds a round with two of its coins at once (which greedy coin selection does all the time), those coins' entire histories merge, by the oldest heuristic in the book.
- The forward edge. A maker's equal output from one round is later spent into another round as a fill input. On its own this edge is ambiguous (any of the round's equal outputs could be the one), but combined with the other two edges and the constraint that each maker contributes exactly one equal output per round, it usually resolves.
Two things make it worse. Distinct advertised fees act as a fingerprint: makers post their fees publicly, so a coin trajectory that keeps realizing the same odd fee increment keeps pointing at the same maker. And ground truth is cheap: an analyst can open a round with a maker, record which UTXOs the maker offers as inputs (all co-owned), and abandon. This probing round costs almost nothing and seeds everything else with certainty. In the standard vocabulary it is an Eve-Alice-Eve counterparty attack (the adversary is both sides of the interaction it learns from), and the cross-round threading it seeds is an intersection attack over provenance clusters: each reused coin is a fresh constraint, and the constraints intersect until the maker's rounds collapse into one cluster.
flowchart TB
classDef txn fill:#2b2b3a,stroke:#c9c9d6,color:#fff,font-weight:bold
classDef known fill:#15361f,stroke:#5fd08a,color:#fff
classDef amb fill:#4a2f12,stroke:#f7931a,color:#fff
classDef other fill:#243a5e,stroke:#9fb0c8,color:#fff
classDef guess fill:#2a1a3a,stroke:#c39bd3,color:#fff,font-style:italic
%% --- how to read this diagram ---
subgraph KEY["how to read this"]
direction LR
K1(["a transaction"]):::txn
K2["a coin it spends or creates (a UTXO)"]:::other
K3["green: analyst is certain it is A's"]:::known
K4["orange: ambiguous to the analyst"]:::amb
end
%% --- the analyst's free head start ---
PROBE["<b>Probing round</b><br/>open a round with A, note the<br/>co-owned coins A offers, then abandon"]:::known
%% --- round 1: a real CoinJoin. coins in on top, coins out below ---
IA["A's input coins"]:::known
IB["B's input coins"]:::other
IT["taker's input coins"]:::other
T1(["tx: CoinJoin round 1"]):::txn
IA --> T1
IB --> T1
IT --> T1
T1 --> E1["equal #1"]:::amb
T1 --> E2["equal #2"]:::amb
T1 --> CA["A's change"]:::known
T1 --> CB["B's change"]:::amb
PROBE -.->|"reveals A's input coins"| IA
%% --- round 2: reached only by following the change peel ---
T2(["tx: CoinJoin round 2"]):::txn
CA ==>|"change peel: an ordinary on-chain<br/>spend, so the analyst just follows it"| T2
T2 --> EA2["A's 2nd equal"]:::known
T2 --> CA2["A's change again"]:::known
%% --- round 3: the co-spend that resolves the ambiguity ---
T3(["tx: a later CoinJoin round"]):::txn
EA2 -->|"respent as a fill input"| T3
E1 -->|"respent as a fill input"| T3
T3 --> E3["equal outputs"]:::amb
T3 --> C3["change"]:::amb
DED["equal #1 and A's 2nd equal were spent together<br/>in tx 3, so they share an owner, so equal #1 was<br/>A's, linking A's round-1 and round-2 slots"]:::guess
T3 -.->|"co-spend heuristic"| DED
Each edge only fires when the specific coins that chain are actually reused, so early in a maker's life the picture is fragmented. But the intersections accumulate: in our simulations the attacker's recall of true cross-round links grows from about 0.17 at 150 rounds to 0.99 by 300 and saturates near 1.0 (the multi-round corpus study; setup and code in "Reproducing the numbers" below). A defense that looks good in a short demo can be worthless over the corpus a real maker produces in a year, so everything below is evaluated at the long horizon. (More mixdepths do not help: they relabel the bucket a reused coin sits in, they do not stop the reuse.)
The attack costs the analyst nothing: passive, retroactive, public data, forever. We judge every defense by how much it raises that cost, not by whether some information still exists somewhere.
The change output is what threads a maker's rounds together, so each defense does something different with it:
| Swap flows | Shared coinpool | LN channel | |
|---|---|---|---|
| What happens to the change | Displaced or sold off entirely | One 2-of-2 P2TR shared with another maker | One channel funding shared with another maker |
| Hidden split | None (whole coin leaves) | Exists, frozen until an on-chain re-split | Exists, updatable off-chain |
| Peel chain | Broken (nothing chains) | Delayed, not broken (endpoint resolves it) | Broken (split moves off-chain) |
| On-chain look | Taproot key-path spend | Taproot key-path spend | Taproot key-path spend |
| Extra machinery | A swap counterparty | MuSig2 + one pre-signed tx | A full Lightning node |
Read the "Peel chain" row before anything else. The whole-coin cut and the LN ring sever the chain; the coinpool, our default tier, only delays attribution (the split rolls forward unchanged until an endpoint reveals it, Section 6). We lead with this so nobody mistakes the coinpool's impressive per-round ambiguity (Section 5) for a break it does not yet deliver.
Reading the middle column against the right one was the biggest lesson of this work: the coinpool is a Lightning channel minus safe off-chain balance updates, and those updates are exactly what lets a hidden split genuinely change between creation and settlement. Remove them and the split freezes (Section 6); keep them and you inherit the Lightning stack (Section 5). The swap flows sidestep the axis: no shared output, no split to leak, at the cost of a fee and a counterparty per swap.
One nice property everywhere: every settlement below is a Taproot key-path spend, one Schnorr signature, indistinguishable from an ordinary payment.
This one needs no protocol change and no coordination with anyone. A maker runs a Lightning swap provider from the same wallet: strangers trade on-chain coins against Lightning payments, the maker clears the swaps, earns the swap fees, and in doing so keeps pushing prior-round coins out of the inventory that funds its next fills.
In our long-horizon simulation this alone collapses the clustering picture: a
maker whose ~180 round-slots merge into one wallet at baseline fragments
into about 107 effective wallets (largest linked fragment down from ~180
slots to ~6.5) at one swap per round. Two metrics are quoted together here and
mean different things: the largest linked fragment is the size of the single
biggest cluster the attacker still links (so ~6.5 of the maker's slots), while
effective wallets is 2^H over the fragment-size distribution (the
Serjantov-Danezis effective anonymity set, an entropy-derived count, not a raw
cluster count and not inverse-Simpson). A maker whose slots scatter into many
small fragments scores high on the second even at a modest first. It is a
partial fix, not a clean break, because a swap that pays out an arbitrary amount
still leaves change that chains.
Is the two-orders-of-magnitude claim robust or knife-edge? It holds well below one swap per round. Sweeping the swap frequency (same corpus, 30 makers, 600 rounds; code and setup below):
| swaps per round | largest linked fragment | effective wallets | cross-round link recall |
|---|---|---|---|
| 0 (baseline) | ~180 | 1.0 | 0.997 |
| 0.5 | ~11 | ~68 | 0.015 |
| 1 | ~6.5 | ~107 | 0.006 |
| 2 | ~4.3 | ~139 | 0.002 |
Even half a swap per round drops cross-round recall from ~1.0 to ~0.015, so the effect is not knife-edge on the operating point; more swaps help monotonically. Which leads to the two refinements:
Sell whole coins, no change. A swap sized so the maker spends its entire coin ends that coin's trace completely: nothing chains, because nothing is left behind. On the other side sits a specialized buyer, an arbitrageur, who accepts history-laden coins at a small discount, pools them, and resells the value later as fresh liquidity. The discount is the market price of shedding history, and concentrating the linkage problem onto one compensated, specialized actor (who manages its own privacy) turns out to be a feature.
Cut the chain, not every link. Even for makers using the constructions in Sections 5 and 6, histories leak in segments (between endpoint events). An occasional whole-coin cut bounds how long any leaked segment can get. This is the on-chain analogue of bounding segment length to restore own-origin robustness (robust connectivity in nothingmuch's terms): you cannot make a coin's whole past unknowable, but you can cap how far back any single leak reaches. Swapping every round would eat a maker's earnings; an occasional cut is cheap:
flowchart LR
classDef tx fill:#1a2238,stroke:#9fb0c8,color:#fff
classDef cut fill:#15361f,stroke:#5fd08a,color:#fff,font-weight:bold
A["rounds 1..N<br/>(one leakable segment)"]:::tx --> S1["whole-coin<br/>swap cut"]:::cut
S1 --> B["rounds N+1..2N<br/>(a fresh segment)"]:::tx
B --> S2["swap cut"]:::cut
S2 --> C["..."]:::tx
And the systemic point we flagged in the TL;DR, spelled out: takers need makers to spend. Every equal output gets spent eventually. If makers almost never author ordinary spends (their coins just cycle into new rounds, which has a recognizable shape), then the spends that leave the CoinJoin cluster are disproportionately taker spends, and the equal-output anonymity set decays in practice. Makers who clear swaps are constantly making ordinary-looking spends from the same wallets that hold equal outputs. That crowd is the hide set a spending taker actually needs, which reframes maker swaps as a taker-anonymity mechanism, not only maker self-defense.
The swap itself is a single P2TR output whose internal key is a MuSig2
aggregate of provider and client; cooperative settlement is a key-path spend,
and the hash/timelock script branches exist only as a fallback. Live on
signet:
swap-in 867f575d,
swap-out f36d73b6,
whole-coin
807685bd:
Discovery runs over Nostr (providers advertise terms and verifiable fidelity bonds; clients recompute the funding address before paying), so the swap market and the CoinJoin market share no coordinator.
Instead of a change output, a maker's change becomes the funding output of a real Lightning channel (LND simple-taproot, P2TR) co-funded with another participant of the same round. The change has no successor coin in the maker's wallet, and the chain shows only the pooled total of two participants' change, never who put in how much. At scale, the taker arranges all change-bearing participants in a ring of shared channels, and the round has no plain change at all.
Here is the actual ring from our live signet CoinJoin
(54301a31),
with one fully consistent example of who funded what. The four channel
totals are on chain; the per-edge contributions, and even the per-party
change totals, are not, and that is the point:
flowchart TB
classDef party fill:#243a5e,stroke:#9fb0c8,color:#fff
classDef chan fill:#3a2a12,stroke:#f7931a,color:#fff,font-weight:bold
MA["Maker A<br/>example change 148,351"]:::party
CAB["channel A+B<br/>total 331,232"]:::chan
MB["Maker B<br/>example change 300,000"]:::party
CBT["channel B+Taker<br/>total 159,815"]:::chan
TK["Taker<br/>example change 350,000"]:::party
CTC["channel Taker+C<br/>total 275,778"]:::chan
MC["Maker C<br/>example change 250,000"]:::party
CCA["channel C+A<br/>total 281,526"]:::chan
MA -->|"70,000?"| CAB
MB -->|"261,232?"| CAB
MB -->|"38,768?"| CBT
TK -->|"121,047?"| CBT
TK -->|"228,953?"| CTC
MC -->|"46,825?"| CTC
MC -->|"203,175?"| CCA
MA -->|"78,351?"| CCA
Every edge number carries a question mark because the assignment is underdetermined by construction: add some amount d to each party's clockwise contribution and subtract it from its counter-clockwise one, and every channel total and every party total still checks out. Shift 10,000 sats around this ring in either direction and all eight edge values change while the transaction stays byte-identical. Even we, having run the round, can only reconstruct the splits up to that free parameter without consulting the wallets' private logs.
How much ambiguity does this actually buy? A fair question, and worth being precise about, because the answer is not "the anonymity set is 8,341." What we count is how many ways the round's inputs can be partitioned into owner groups consistently with the observed amounts. That candidate space grows super-exponentially with participants and inputs (it is a set-partition count); what matters is how many candidates the amounts kill:
| construction | N=3 | N=4 | N=5 |
|---|---|---|---|
| plain change per participant | 1 | 1 | 1 |
| equal-valued ring | 28 | 296 | 3,995 |
| random-sized ring | 36 | 460 | 8,341 |
With plain change, each group must hit an exact residual, and exactly one partition survives: the round is uniquely solved, every time. A co-funded output replaces the exact equation with an interval constraint (each side contributed something, only the total shows), plus the ring's free parameter, so most of the combinatorial space survives. Three honest qualifications:
- The per-round anonymity ceiling is still N. You are visibly one of the round's participants; no partition count changes that. What the ambiguity buys is different: it stops the analyst from uniquely attributing residuals, which is what the cross-round threading of Section 2 needs to grab onto. A count a few orders of magnitude above N is enough for that; beyond it, more ambiguity is not more anonymity.
- Equal outputs are still doing their own, separate job: they are the taker's per-round hiding set (one of N to outsiders, one of N-1 against each maker, who knows its own). The ring hides residuals; the equal outputs hide the taker's payload. Both are needed.
- A Bayesian attacker who knows the (open-source) split sampler does somewhat better than these uniform counts, which is why the deployed sampler is high-entropy, and why we treat these numbers as shape, not gospel.
(These counts are reproducible: enumerate the owner-partitions consistent with the observed amounts, per the subset-sum sweep in the coinjoin-simulator repository.)
The same ambiguity, read for the coinpool, is a delay budget, not a break. This table lives in the Lightning section because that is where the split genuinely keeps moving, but the per-round partition count applies to the shared coinpool of Section 6 too: a coinpool round is also underdetermined at funding time, one of thousands of consistent owner-maps. The difference is what happens next. In the ring, the split moves off-chain and the ambiguity is permanent; in the coinpool, the split rolls forward unchanged, so an endpoint event (an exit at the true split, a Sybil partner, or the creation pair-sum) collapses the whole trajectory retroactively. So for the coinpool, read the 8,341 as how long a chain analyst is delayed before the endpoint resolves the round, not whether the chain is ever severed. The break only lands if in-CoinJoin re-splits work (Section 6). This is the single sharpest seam in the design, and we would rather you see it here than infer it.
Where this sits relative to WabiSabi. JoinMarket is an equal-amount market; Wasabi's WabiSabi already achieves arbitrary-amount ambiguity a different way, with anonymous credentials (KVACs) that let a coordinator issue exact-value outputs without learning who owns which. A fair reader will ask why not just move to credential-based arbitrary-amount joins. The honest answer is that this work is a retrofit: it buys arbitrary-amount-like residual ambiguity for the equal-amount market that already exists, and it does so specifically through the change outputs (co-funding them so only totals show), without a new credential system, a new coordinator role, or a protocol migration. It is the cheaper, narrower niche, not a replacement for the credential approach.
A related thought we have not evaluated but find intriguing: with no equal outputs at all (every output a co-funded, arbitrary-valued P2TR), a transaction would not even reveal how many participants it had. That is the "deniability" end of the CoinJoinXT design space, and hiding N itself may have anonymity properties that equal-output constructions cannot reach.
Why bother with Lightning at all? Because the split can then keep moving
after the round, safely, for free. One of these ring channels later routed a
payment and closed cooperatively at 61,000 + 98,429
(4a53c398),
balances decorrelated from whatever the funding split was: one key-path
signature, one input, two outputs, nothing special to see.
The honest costs:
- A hard Lightning node dependency per maker (state backups, force-close risk during fee spikes, monitoring). Realistically this limits adoption to operators who already run Lightning.
- A persistent node identity: channels stay unannounced, but the partner sees a node id, so identities have to rotate on some schedule.
- The privacy edge is conditional on use. A channel that never moves balance closes at its funding split, which leaks the same way the coinpool below does, just with more machinery around it. Channels should route or pay, at least a little.
- A force close is loud: it reveals the output was a channel, and its final balance. Cooperative closes keep the story clean.
Drop Lightning entirely. When makers A and B are both in a round, they take one shared Taproot output whose key is a MuSig2 aggregate of one key from each, instead of two plain change outputs.
- The split is a secret. On-chain the output is one ordinary P2TR. Only A and B know who owns how much of it.
- Safety is one pre-signed transaction. Before the shared output exists, A and B co-sign an exit transaction paying each side its share. Either can broadcast it at any time. Trust is bounded to liveness, never custody: the worst a partner can do is force you to take your money out.
- Respends look like anything else. A later round spends the shared coin with one aggregate signature, indistinguishable from a single-key input.
- The mesh. Each maker spreads its change across shared coins with several partners, chosen privately at random, so its liquidity dissolves into a web of two-party pots.
flowchart TB
classDef tx fill:#1a2238,stroke:#9fb0c8,color:#fff
classDef sh fill:#15361f,stroke:#5fd08a,color:#fff,font-weight:bold
classDef exit fill:#2a1a3a,stroke:#c39bd3,color:#fff
classDef ch fill:#4a2f12,stroke:#f7931a,color:#fff
R1["CoinJoin round<br/>(A and B both participate)"]:::tx
R1 --> S1["shared output A+B<br/>one P2TR, split secret"]:::sh
X1["exit tx: pays A its share,<br/>B its share<br/>(co-signed BEFORE the round<br/>is even broadcast)"]:::exit
S1 -.->|"kept in both wallets,<br/>never needs to be used"| X1
S1 -->|"normal path: both online,<br/>co-sign the next respend"| R2["next CoinJoin<br/>(or a payment, or a swap)"]:::tx
S1 -->|"partner vanishes:<br/>broadcast the stored exit"| X1
X1 --> PL["two plain outputs<br/>(privacy benefit gone, funds safe)"]:::ch
We ran the full arc with real transactions, shaped like real rounds (a taker with its own equal output and plain change in every CoinJoin; our two makers pool their change). Built from our implementation's MuSig2 and Taproot primitives; every shared-coin spend is a single 64-byte signature, and every exit was co-signed before the coin it spends existed:
| step | tx | what it shows |
|---|---|---|
| CJ round 1: makers A+B pool change into shared coin S1 (true split 150,250/70,250, invisible); taker gets its equal + plain change | 31abb432 |
one shared P2TR replaces two change outputs in an otherwise normal round |
| CJ round 2: A respends S1 as its fill input (B just co-signs from home), fresh taker; new shared change S1' | d7ce8447 |
the shared coin works as an ordinary CoinJoin input; B's share rides through unchanged (70,250) |
| A clears a swap-out as provider: pays a client 25,000 from the shared coin, remainder stays shared | 3b70afbc |
maker spends happen after rounds and look like any payment with change |
| The pre-signed exit of the final shared coin, broadcast exactly as stored | b48ca119 |
funds safety needs no cooperation, and see below: B's revealed share still equals its round-1 residual |
We simulated the mesh against a Bayesian chain observer (it weights every consistent interpretation of each transaction by how common pooling is overall) across maker histories (setup in "Reproducing the numbers" below):
- Adoption is the lever that matters. At 25% adoption the attacker still holds ~87% confidence in the true change link; only broad adoption pushes it down hard (~12% at 100%). Some people mixing in a crowd is much weaker than everyone looking the same.
- One honest partner cuts the trail. A corrupt partner learns the split of the one coin it co-owns, nothing more; tracing a k-hop trajectory needs every partner corrupted. More hops with fresh partners help, not hurt.
- Partner choice must be private randomness. Any pairing rule computable from public data lets an observer recover the partner graph; a private RNG protects the chooser and needs no verifiability.
- It is cheap. Partners are makers already on the public order book; a shared input adds roughly one percentage point of round-failure risk, and one fewer output actually saves the taker fees.
Now the honest part, and our own demo shows it. Rolling a shared coin forward is not re-randomization. When A respends S1 with B passive, value conservation forces B's share of the new coin to be exactly what it was: 70,250 sats in round 1, 70,250 in S1', 70,250 in S1''. When the exit finally hit the chain, it paid B 70,050 (its share minus half the exit fee), a number any analyst who solved round 1 can recognize. Every hop was an alias for the same secret, and the endpoint resolved the whole trajectory backwards. This is precisely the problem Lightning's penalty-update mechanism exists to solve, and the coinpool has to solve it another way or settle for delaying the peel chain rather than breaking it.
The direction we believe in, but have not built: pair re-splits inside CoinJoins. When A and B hold two or more shared coins, they co-spend them in a round they were joining anyway and recreate shared outputs with freshly sampled splits, only the per-party totals conserved. Each recreated output beyond the first carries a genuinely new secret, even against an attacker who knows every residual, and new exits are co-signed before the round is, so the safety story never lapses. But two open questions have to be answered first:
- The co-spend itself leaks. Spending two coins of the same pair in one round reveals that those two coins were pair-linked, something the chain did not show before. We have not yet quantified what that costs against what the re-split gains.
- Coordinating it through the taker leaks. Today the taker computes every maker's change and would see the re-split structure plainly. How two makers slot a re-split into a round without telling the taker (or the chain) more than a normal round does is unexplored design space.
Until those are answered, our honest position is: the coinpool severs the mechanical peel chain today (no plain change to follow), delays full attribution substantially, and its endpoint leak closes only if in-CoinJoin re-splits (or something better) land. What it offers in exchange for that open question: no Lightning node, no persistent identity, no force-close games, funds safety in one pre-signed transaction, and a realistic shot at the broad adoption that the simulations say matters more than anything else.
A maker's fee income does not depend on its own privacy, so why would it do any of this? Three pressures, from softest to most structural:
- Self-interest. Probing rounds mean anyone, including a maker's own adversaries, can cluster its wallet today. Makers hold meaningful sums.
- Economics already point the right way. Pooled change saves the taker fees (one fewer output), swap-providing earns fees while shedding history, and the whole-coin discount is a price signal, not a tax.
- Provenance, a gentle nudge. A coin is "due" when a public but
unpredictable function says so (say,
hash(block_hash || outpoint) mod q == 0), and privacy-seeking takers refuse due coins as maker inputs. Due-ness follows the value, so an ordinary self-send does not shed it; the cleanest escape is a whole-coin swap, exactly the supply the arbitrageur wants.
Our honest take on provenance: worth shipping as an option, not a dependency. It cannot be enforced (a determined maker can swap with itself through a second identity, though that already pays the intended cost), and it only works if takers adopt the refusal rule. But it is the only leaderless, demand-side pressure we know of, and makers who are also takers need no push at all.
flowchart TB
classDef base fill:#15361f,stroke:#5fd08a,color:#fff
classDef opt fill:#2a1a3a,stroke:#9b59b6,color:#fff
classDef all fill:#3a2a12,stroke:#f7931a,color:#fff
H["Hygiene (every maker):<br/>fee homogeneity, overlapping timing"]:::all
S["Swap flows (every maker):<br/>provider role + occasional whole-coin cuts,<br/>no protocol change, earns fees"]:::all
B["Default tier: shared coinpool change<br/>(no new dependencies)"]:::base
L["Opt-in tier: LN channel ring<br/>(operators already running Lightning,<br/>channels actively used)"]:::opt
H --> S
S --> B
S --> L
- Everyone: hygiene and swap flows. Homogeneous fees (distinct fees quietly undo everything else), settlement timing drawn from overlapping schedules, the provider role, and periodic whole-coin cuts that bound every leaked segment. This tier alone is a large improvement and needs nobody's permission.
- Default: the coinpool. The weakest per-hop guarantees of the shared constructions, but the only one most makers will plausibly run, and adoption dominates every other parameter we measured. Ships with the endpoint-leak caveat stated plainly, and gets stronger if in-CoinJoin re-splits work out.
- Opt-in: the Lightning ring. For operators already paying the Lightning complexity cost, it is the only mechanism we know where the hidden split keeps moving for free. It exists and works today.
What success would mean. Today the peel chain is free to exploit. Under this design an analyst needs active spending (probing rounds), retroactive multi-round forensics over an underdetermined space, and even then recovers bounded segments rather than full histories. That is the difference between dragnet surveillance and a targeted investigation, which we think is the right bar for ordinary wallets on a public ledger; a mixnet-grade guarantee is not on offer from any 2-party construction.
- Counterparties always learn their own leg. The swap partner sees one segment boundary; the pool or channel partner knows its coin's split. The break is against passive chain observers; a decentralized market of many counterparties is what keeps those views fragmented.
- The taker coordinates the round. As deployed, the taker computes every maker's change and relays the pooling data, so it knows the pairing and split of coins it helped create. We rate this acceptable rather than blocking: the taker pays per round, learns only rounds it funds, and cannot follow a maker into future respends; an attacker wanting broad coverage would run Sybil makers instead, which fidelity bonds already price. Hiding the pairing from the taker entirely (maker-to-maker rendezvous) is hardening we want, not a precondition, and the same open question as the re-split coordination above.
- Aggregates are conserved. Against an adversary that fully attributes every round, per-party totals are computable no matter how splits are shuffled. Everything here targets the (much larger) space of weaker adversaries, and the useful property is persistence: a round the attacker could not attribute should stay unresolved forever, instead of being unwound backwards from one attributed endpoint. The coinpool does not have that property yet (Section 6); the LN ring and whole-coin cuts do.
- Sybil partners are the shared constructions' worst case. One adversary posing as many partners raises the effective corrupt-partner fraction. The defense is economic (fidelity-bond-weighted partner selection), inheriting the same analysis that protects the CoinJoin market itself.
- The fidelity bond is its own linkage surface. We lean on bonds twice, for Sybil-resistant partner weighting and for Nostr advertising, but the bond UTXO is exactly the kind of coin the rest of this design works to avoid: it is persistent, timelocked, publicly announced, and tied to the advertised identity. A maker can de-cluster its change perfectly and still be anchored if its bond was funded from or later spent into operational coins, because the bond's funding transaction and its eventual reclaim are ordinary, followable on-chain edges pinned to a known identity. Treat bond hygiene as a first-class requirement: fund the bond from a source with no link to the mixing wallet, never recycle a reclaimed bond straight back into fills, and keep bond-to-operational separation as strict as change-to-change separation. Nothing above protects a maker that undoes it at the bond.
- Timing classifies. Swap claims, channel closes, and exits have different natural lifetimes; spread them across overlapping schedules and never settle a ring or a mesh as a cohort.
- These are demos, not deployments. The signet transactions prove the shapes are consensus-valid and look the way we claim; the privacy compounds only across many rounds and a populated market. Simulation numbers are model numbers; we report the qualitative shape and expect the exact curves to move.
All on Bitcoin signet; open any link with ?showDetails=true and check the
witness: a single Schnorr signature each time.
| what | tx |
|---|---|
| Swap-in claim (Section 4) | 867f575d |
| Swap-out claim (Section 4) | f36d73b6 |
| Whole-coin swap, no change left (Section 4) | 807685bd |
| Ring CoinJoin, 4 co-funded LN channels, no plain change (Section 5) | 54301a31 |
| Cooperative channel close, decorrelated balances (Section 5) | 4a53c398 |
| Coinpool CJ round 1, shared change S1 (Section 6) | 31abb432 |
| Coinpool CJ round 2, shared coin as fill input (Section 6) | d7ce8447 |
| Swap payout from the shared coin (Section 6) | 3b70afbc |
| Pre-signed exit, broadcast as stored (Section 6) | b48ca119 |
The curves in this writeup are the empirical backbone, so here is the setup, and the runnable code lives in the coinjoin-simulator repository (every study fixes its seeds). These are model numbers: they report the shape of an effect, not a forecast.
The shared-change mesh (Sections 5 and 6). A market of N makers
(default 20) with persistent balances drawn uniformly from [300k, 3M] sats and
per-round liveness in [0.75, 0.98]. Each round, four of them join a CoinJoin
at a 1M-sat denomination; mesh makers pair with another mesh maker under
private uniform random pairing and pool their change above a 27,300-sat
co-funding floor, while everyone else takes solo change. The attacker is
Bayesian and knows the protocol (Kerckhoffs): it maintains a running,
causally learned estimate of the pooled fraction (no lookahead) and weights each
consistent interpretation of every respend by that prior and a value-likelihood
term, then is scored on its posterior confidence in the true peel edge. This is
the 87% -> 12% curve. The Section 5 partition-count table is a direct
owner-partition enumeration consistent with the observed amounts (the
repository's subset-sum sweep computes the same object).
The multi-round corpus study (Sections 2 and 4). The cross-round clustering
build-up (~0.17 at 150 rounds to ~0.99 by 300) and the swap-provider
fragmentation (~180 slots into ~107 effective wallets, and the swap-frequency
sensitivity table) come from a larger model,
run_maker_swap_provider_study.py in the coinjoin-simulator repository. Its
setup: 30 makers, 9 per CoinJoin, 600 rounds, 3 seeds; greedy coin selection
(the source of the co-spend edge in Section 2); the taker draws a round amount
and pays each maker its advertised fee; the chain observer runs a matching
attacker (peel + common-input + reused-equal-output edges) alongside a diffusion
attacker. Corpus lengths swept: 150 / 300 / 600 / 1,200 rounds. Swap rates
swept: 0 / 0.5 / 1 / 2 per round.
Metric definitions. Largest linked fragment is the size of the single
biggest cluster the attacker links (baseline ~180, i.e. one wallet). Effective
wallets is 2^H over the fragment-size distribution: the Serjantov-Danezis
effective anonymity set, an entropy-derived number, not a raw cluster count
and not inverse-Simpson. Diaz degree is H / log2(n), normalized to
[0, 1] (0 = one wallet, 1 = every slot independent).
- Greg Maxwell, "CoinJoin: Bitcoin privacy for the real world" (2013): the original CoinJoin proposal.
- Adam Gibson (waxwing), "CoinJoinXT: a more flexible, extended approach to CoinJoin" (2018): pre-signed transaction graphs over jointly-owned outputs, the deniability-versus-intrinsic-fungibility framing, and the dual-funded channel amount-decorrelation trick that Sections 5 and 6 build on.
- F. Maurer, T. Neudecker, M. Florian, "Anonymous CoinJoin Transactions with Arbitrary Values" (2017): knapsack mixing, the subset-sum-hardness lineage of Section 5's ambiguity argument.
- Antoine Riard and Gleb Naumenko, "CoinPool: exploring generic payment pools for Fun and Privacy" (bitcoin-dev, 2020) and the CoinPool paper: n-party shared UTXOs need covenants; the 2-party case in Section 6 needs nothing new.
- Chris Belcher, "Design for improving JoinMarket's resistance to sybil attacks using fidelity bonds" (2019) and the JoinMarket fidelity bonds documentation: the Sybil-cost analysis our partner-selection economics inherit.
- Gavenda, Coufal, Veselý, Matyáš, "Analysis of Input-Output Mappings in Coinjoin Transactions with Arbitrary Values" (2025): Bayesian mapping weights over arbitrary-value CoinJoins, the attacker model our simulations follow.
- Ádám Ficsór, Yuval Kogman, Lucas Ontivero, István András Seres, "WabiSabi: Centrally Coordinated CoinJoins with Variable Amounts" (2021): the credential-based arbitrary-amount CoinJoin (Wasabi) that this work is positioned against; we retrofit arbitrary-amount-like residual ambiguity into JoinMarket's equal-amount market through the change outputs instead.
- Yuval Kogman (nothingmuch): Collaborative transaction privacy
- BIP 327 (MuSig2): the multisignature scheme behind every shared output above.
- JoinMarket: the reference implementation of the maker/taker CoinJoin market.
- coinjoin-simulator: the multi-round corpus study behind Sections 2 and 4 and the subset-sum ambiguity sweep behind Section 5.




Great work with all the examples and technical details. Can we provide both options initially (opt-in) for makers?
Coinpool might complicate things for makers and takers. Also we need taproot support first for any of these solutions.