Skip to content

Instantly share code, notes, and snippets.

@nothingmuch
Last active July 24, 2026 08:13
Show Gist options
  • Select an option

  • Save nothingmuch/d84ba390d89b5b08897af2d95009c2a1 to your computer and use it in GitHub Desktop.

Select an option

Save nothingmuch/d84ba390d89b5b08897af2d95009c2a1 to your computer and use it in GitHub Desktop.

Collaborative Transaction Privacy

This document examines some challenges for privacy in a variety of multiparty transaction settings, from PayJoin to CoinJoin. None of the attacks described are novel, but some of the cited results of the privacy literature originated in a different setting.

Common misconceptions about how privacy works on chain for such transactions paint a much rosier picture than what the published literature has shown. Unlike in cryptography, where the burden of proof for claims of security is rightfully rather high, various apologists and interested parties often promote the notion that the burden of proof should be reversed, or that addressing these problems is trivial.

This document is not concerned with providing empirical proof of the viability of different attacks, partly because some of the structures whose weaknesses are examined are still hypothetical, but mostly because of the lack of novelty, the cited works are supported by evidence and most are peer reviewed.

This document is concerned with defining specific failure modes, and by extension the success criteria for claims about privacy on chain. Vendors of privacy enhancing technologies, as is the norms in other industries, should be expected to address these widely understood attacks, and argue for how privacy is maintained in the face of such adversaries.

Despite this being presented as a series of attacks against increasingly sophisticated defences, please do not conclude that privacy is doomed. In fact, I will argue by the end that so long as certain properties of the transaction graph are realized those widely held misconceptions wouldn't really be misconceptions anymore. While concrete proposals for how to realize those properties lie outside of the scope of this discussion, rest assured that for the problems defined here, there will be a followup with what is hopefully a credible response that hopefully bears that burden of proof successfully.

Unilateral transaction privacy

Unilateral transactions are highly susceptible to deanonymization using clustering techniques. After address reuse, the most obvious way to cluster, or link a user's coins is the common-input-ownership heuristic (CIOH): a transaction that spends several inputs is evidence that one entity owns all of them, since at the protocol level signing for all of them normally requires all of their keys.12

flowchart LR
  classDef ac fill:#7993b6,stroke:#000,color:#111,rx:12,ry:12
  classDef bc fill:#e9a969,stroke:#000,color:#111,rx:12,ry:12
  classDef cc fill:#da817d,stroke:#000,color:#111,rx:12,ry:12
  classDef txn fill:#444,stroke:#000,color:#fff
  a1("input 0.5"):::ac --> tx1["transaction 1"]:::txn
  a2("input 0.3"):::ac --> tx1
  tx1 --> pay1("payment 0.6"):::bc
  tx1 --> chg1("change 0.2"):::ac
  chg1 --> tx2["transaction 2"]:::txn
  a3("input 0.4"):::ac --> tx2
  tx2 --> pay2("payment 0.5"):::cc
  tx2 --> chg2("change 0.1"):::ac
Loading

CIOH links the inputs of each transaction, and change identification links the two transactions: all five blue coins merge into a single cluster.

Practical clustering combines heuristics beyond CIOH to build a wallet cluster: a set of addresses and coins inferred to share one owner.3456789

Collaborative transactions

A transaction that spends inputs owned by more than one user falsifies CIOH by construction. Starting with just two parties, the observable structure can have (at least) two forms.10 The on-chain transaction must be compatible with the actual allocation of inputs, outputs, and payments among the participants, but might not reveal that allocation in its entirety.

The first form is a batched transaction, or a CoinJoin: two independent and balanced sub-transactions aggregated into a single transaction.

Here, a sub-transaction means some, but not all, of a transaction's inputs and outputs attributed to one participant's action. A more precise, probabilistic notion will be discussed below.

flowchart LR
  classDef ac fill:#7993b6,stroke:#5778a4,color:#111,rx:12,ry:12
  classDef bc fill:#e9a969,stroke:#e49444,color:#111,rx:12,ry:12
  classDef payA fill:#da817d,stroke:#5778a4,stroke-width:3px,color:#111,rx:12,ry:12
  classDef payB fill:#9dc5c1,stroke:#e49444,stroke-width:3px,color:#111,rx:12,ry:12
  classDef txn fill:#444,stroke:#000,color:#fff
  ai("Alice's input 0.9"):::ac --> tx["transaction"]:::txn
  bi("Bob's input 0.3"):::bc --> tx
  tx --> pa("payment 0.5"):::payA
  tx --> cb("Bob's change 0.1"):::bc
  tx --> pb("payment 0.2"):::payB
  tx --> ca("Alice's change 0.4"):::ac
  linkStyle 0,2,5 stroke:#5778a4,stroke-width:3px
  linkStyle 1,3,4 stroke:#e49444,stroke-width:3px
Loading

A batch: two independent spends aggregated, SharedCoin style. Each sub-transaction balances on its own (0.9 = 0.5 + 0.4 and 0.3 = 0.2 + 0.1).

In the shown example although CIOH is falsified, the amounts strongly suggest one way of partitioning the transaction, and CIOH still applies within each sub-transaction separately. We will return to this form in later sections.

Two-party payjoin

The other main form a two party transaction can take is a payjoin,11121314 which consists of a single payment connecting the sender's sub-transaction to the receiver's, the sender's surplus covering the receiver's sub-transaction's funding deficit.

A single payment from Alice to Bob, but Bob also contributes one of his own inputs alongside Alice's:

flowchart LR
  classDef alice fill:#7993b6,stroke:#000,color:#111,rx:12,ry:12
  classDef bob fill:#e9a969,stroke:#000,color:#111,rx:12,ry:12
  classDef txn fill:#444,stroke:#000,color:#fff
  ai("Alice's input 0.9"):::alice
  bi("Bob's input 0.6"):::bob
  tx["transaction"]:::txn
  pay("Bob's output 1.1"):::bob
  chg("Alice's change 0.4"):::alice
  ai --> tx
  bi --> tx
  tx --> pay
  tx --> chg
Loading

Here Alice pays Bob 0.5: Bob's 0.6 input grows into a 1.1 output. The payment amount appears nowhere on chain, and the ownership coloring is of course not part of the on-chain data either.

CIOH, seeing two inputs, wrongly concludes one owner. There is no single unambiguous interpretation: looking at just this transaction in isolation, an analyst cannot be sure which inputs are Alice's, which output is the real payment, or even that a payjoin occurred at all rather than an ordinary two-input spend.

For privacy, a somewhat teleological but more general definition of Payjoin is a collaborative transaction that should be indistinguishable from an ordinary single-party transaction. The unnecessary-input heuristics flag an input when the remaining inputs could already fund the outputs and fees; such an input may reveal that another participant contributed it.1516 If collaborative transactions were sufficiently common, naive application of CIOH would quickly lead to cluster collapse, forcing a more nuanced approach to clustering.

With only two parties, there are only a few interpretations, all the different ways of assigning inputs and outputs to sub-transactions attributed to Alice and Bob separately. The typical uncertainty is therefore on the order of a few bits to an external observer, even when looking only at the transaction in isolation and ignoring its context.17

The next few sections discuss extensions to two party payjoin, but bear in mind that as they are more difficult to analyze, the same concerns apply even more to the two party case.

Additionally, a two-party payjoin cannot provide privacy from the counterparty. Each participant can eliminate their their own inputs and outputs from consideration which entails that everything else is attributable to the only other party. Privacy within a transaction requires three or more parties.

Many senders, one receiver (NS1R)

A natural generalization is from one sender to many.18 Several senders each contribute inputs to a single transaction that pays one common receiver, so it resembles an ordinary batched payout the receiver might have made by consolidating funds:

flowchart LR
  classDef s1c fill:#7993b6,stroke:#000,color:#111
  classDef s2c fill:#e9a969,stroke:#000,color:#111
  classDef s3c fill:#da817d,stroke:#000,color:#111
  classDef rc fill:#9dc5c1,stroke:#000,color:#111
  ls1(["Alice"]):::s1c -- "0.2" --> lr(["Dave"]):::rc
  ls2(["Bob"]):::s2c -- "0.3" --> lr
  ls3(["Carol"]):::s3c -- "0.7" --> lr
Loading

The payment graph, which is not observable on chain.

flowchart LR
  classDef s1c fill:#7993b6,stroke:#000,color:#111,rx:12,ry:12
  classDef s2c fill:#e9a969,stroke:#000,color:#111,rx:12,ry:12
  classDef s3c fill:#da817d,stroke:#000,color:#111,rx:12,ry:12
  classDef rc fill:#9dc5c1,stroke:#000,color:#111,rx:12,ry:12
  classDef txn fill:#444,stroke:#000,color:#fff
  r1("Dave's input 0.4"):::rc
  s1("Alice's input 0.5"):::s1c
  s2("Bob's input 0.6"):::s2c
  s3("Carol's input 0.9"):::s3c
  tx["transaction"]:::txn
  pay("Dave's output 1.6"):::rc
  c1("Alice's change 0.3"):::s1c
  c2("Bob's change 0.3"):::s2c
  c3("Carol's change 0.2"):::s3c
  r1 --> tx
  s1 --> tx
  s2 --> tx
  s3 --> tx
  tx --> c2
  tx --> pay
  tx --> c3
  tx --> c1
Loading

Alice, Bob and Carol pay 0.2, 0.3 and 0.7 respectively; Dave consolidates the payments with a 0.4 input of his own. None of the payment amounts appear on chain.

This allows more payments to be consolidated into a single receiver output, so it is cheaper than paying separately. There is also now a limited form of counterparty privacy: the receiver, seeing a pile of inputs, cannot always be sure which input belongs to which sender, giving the senders some plausible deniability.

The receiver still learns each sender's change output, because it knows each sender's payment amount and can read off what is left over. The senders gain a little cover against outsiders while handing the receiver a map of their remaining coins.

This structure can't really hide much more than that, as the change outputs will likely still link and as the number of senders grows, to leverage the savings the receiver must consolidate more aggressively which is a detectable fingerprint. Avoiding it means foregoing the fee savings and splitting the received funds into several outputs of a smaller magnitude in order to better match the distribution of the change outputs. Unfortunately, this in turn creates another liability: spending sibling outputs of the same transaction together, in addition to being wasteful, is a very unusual pattern, and should also be avoided if privacy is to be preserved.

Many senders, many receivers (NSNR)

The two sides can instead be made symmetric. Let $n = 2m$ be the number of participants, half spending and half receiving. The transaction consists of $m$ payments between the $n$ sub-transactions, pairing sub-transactions uniquely.

flowchart LR
  classDef s1c fill:#7993b6,stroke:#000,color:#111,rx:12,ry:12
  classDef s2c fill:#e9a969,stroke:#000,color:#111,rx:12,ry:12
  classDef s3c fill:#da817d,stroke:#000,color:#111,rx:12,ry:12
  classDef r1c fill:#9dc5c1,stroke:#000,color:#111,rx:12,ry:12
  classDef r2c fill:#88b279,stroke:#000,color:#111,rx:12,ry:12
  classDef r3c fill:#ecd580,stroke:#000,color:#111,rx:12,ry:12
  classDef txn fill:#444,stroke:#000,color:#fff
  s1("Alice's input 0.9"):::s1c --> tx["transaction"]:::txn
  r3("Frank's input 0.15"):::r3c --> tx
  s2("Bob's input 0.4"):::s2c --> tx
  r1("Dave's input 0.3"):::r1c --> tx
  s3("Carol's input 0.45"):::s3c --> tx
  r2("Erin's input 0.35"):::r2c --> tx
  tx --> p2("Erin's output 0.85"):::r2c
  tx --> c3("Carol's change 0.25"):::s3c
  tx --> p1("Dave's output 0.5"):::r1c
  tx --> c1("Alice's change 0.4"):::s1c
  tx --> p3("Frank's output 0.45"):::r3c
  tx --> c2("Bob's change 0.1"):::s2c
Loading

A multiparty payjoin transaction with three payments between six participants, with one input and one output per party.

flowchart LR
  classDef s1c fill:#7993b6,stroke:#000,color:#111
  classDef s2c fill:#e9a969,stroke:#000,color:#111
  classDef s3c fill:#da817d,stroke:#000,color:#111
  classDef r1c fill:#9dc5c1,stroke:#000,color:#111
  classDef r2c fill:#88b279,stroke:#000,color:#111
  classDef r3c fill:#ecd580,stroke:#000,color:#111
  ls1(["Alice"]):::s1c -- "0.5" --> lr2(["Erin"]):::r2c
  ls2(["Bob"]):::s2c -- "0.3" --> lr3(["Frank"]):::r3c
  ls3(["Carol"]):::s3c -- "0.2" --> lr1(["Dave"]):::r1c
Loading

The underlying payment graph, which is not observable on chain.

A different interpretation of the same transaction might swap the candidate ownership of Alice's 0.4 change and Erin's 0.85 output:

flowchart LR
  classDef s1c fill:#7993b6,stroke:#000,color:#111,rx:12,ry:12
  classDef s2c fill:#e9a969,stroke:#000,color:#111,rx:12,ry:12
  classDef s3c fill:#da817d,stroke:#000,color:#111,rx:12,ry:12
  classDef r1c fill:#9dc5c1,stroke:#000,color:#111,rx:12,ry:12
  classDef r2c fill:#88b279,stroke:#000,color:#111,rx:12,ry:12
  classDef r3c fill:#ecd580,stroke:#000,color:#111,rx:12,ry:12
  classDef txn fill:#444,stroke:#000,color:#fff
  s1("Alice's input 0.9"):::s1c --> tx["transaction"]:::txn
  r3("Frank's input 0.15"):::r3c --> tx
  s2("Bob's input 0.4"):::s2c --> tx
  r1("Dave's input 0.3"):::r1c --> tx
  s3("Carol's input 0.45"):::s3c --> tx
  r2("Erin's input 0.35"):::r2c --> tx
  tx --> c1("Alice's change 0.85"):::s1c
  tx --> c3("Carol's change 0.25"):::s3c
  tx --> p1("Dave's output 0.5"):::r1c
  tx --> p2("Erin's output 0.4"):::r2c
  tx --> p3("Frank's output 0.45"):::r3c
  tx --> c2("Bob's change 0.1"):::s2c
Loading

Under this alternative assignment, Alice's 0.9 input leaves 0.85 in change, implying a payment of only 0.05 to Erin.

Because each participant only participates in exactly one payment, each hidden sub-transaction is related to only one payment of one amount. The amounts therefore still carry information. In the example, the alternative 0.05 payment may seem less plausible than the original 0.5 payment if its magnitude is atypical relative to prior on-chain payments and auxiliary context such as exchange rates, timing, fees, and wallet fingerprints. This is essentially the same logic as the unnecessary input heuristics.1516 And if the adversary happens to know that Alice's cluster contains a smaller UTXO that the 0.9 one which still suffices for a payment of 0.05, that would be rather strong evidence in favor of the payment amount being 0.5.

Net-settlement with cycles

Net-settlement is the most general form of multiparty payjoin. Where NSNR fixed $\frac{n}{2}$ payments among $n$ participants, net-settlement allows odd $n$ and up to $\frac{n(n-1)}{2}$ net payments to be aggregated: any participant may pay any other.

Participants settle an arbitrary set of mutual obligations in a single transaction, including cycles (e.g. Alice pays Bob, Bob pays Carol, Carol pays Alice). A participant may both pay and receive payments in the same settlement. The number of participants $n$ is in general not observable from the transaction (though in some cases it can be statistically or directly inferred from its structure).

flowchart LR
  classDef ac fill:#7993b6,stroke:#000,color:#111,rx:12,ry:12
  classDef bc fill:#e9a969,stroke:#000,color:#111,rx:12,ry:12
  classDef cc fill:#da817d,stroke:#000,color:#111,rx:12,ry:12
  classDef txn fill:#444,stroke:#000,color:#fff
  ia("Alice's input 0.6"):::ac --> tx["transaction"]:::txn
  ib("Bob's input 0.2"):::bc --> tx
  ic("Carol's input 0.5"):::cc --> tx
  tx --> oc("Carol's output 0.4"):::cc
  tx --> oa("Alice's output 0.5"):::ac
  tx --> ob("Bob's output 0.4"):::bc
Loading

A single settlement transaction. Only the net balances (Alice −0.1, Bob +0.2, Carol −0.1) are reflected on chain; none of the obligation amounts appear anywhere.

flowchart LR
  classDef ac fill:#7993b6,stroke:#000,color:#111
  classDef bc fill:#e9a969,stroke:#000,color:#111
  classDef cc fill:#da817d,stroke:#000,color:#111
  A(["Alice"]):::ac -- "0.5" --> B(["Bob"]):::bc
  B -- "0.3" --> C(["Carol"]):::cc
  C -- "0.4" --> A
Loading

Three mutual obligations, including a cycle.

Once cycles are allowed, a participant's implied payment balance is no longer a single, plausible magnitude amount but a signed sum of what they sent and received, which can net to anything, including near zero. This removes the discriminating power that payment amounts had in the previous scenario: once more than one payment per sub-transaction is possible, the net balance of a candidate sub-transaction is much harder to interpret.

Since net-settlement is strictly more general, if it is supported in implementations then every multiparty batch could in principle be a net settlement transaction even if that's not actually the case. Conversely, if the only thing actually deployed is the restricted NSNR above, users are forced back into that weaker regime where amounts still leak. The strength of the defense depends on the general form being the norm, not the exception.

Unfortunately, this is not enough for privacy. The resulting graph still has community structure. The adversary does not see only a single transaction; it sees the transaction graph, and some clustering of it (which may be based just on the graph structure, or be based on blockchain external information too). A settlement among counterparties is a slice of an economic network, and economic networks are not featureless: they have recurring relationships, and the behaviors can often follow predictable patterns.

Sparse datasets

Deanonymization of networks with exactly this kind of structure is a well studied problem. In 2008, Narayanan and Shmatikov showed that in high dimensional space, because every point in the feature space will almost certainly have no neighbors deanonymization is typically not only possible, but also robust to noise.1920

Any observable pattern in a cluster is potentially useful for such an analysis. These divide into statistical features (fee-rate habits, activity timing and time zone, value distributions) and structural features (the shape a cluster carves through the graph, and its links to other clusters).

Wallet-software fingerprints are a special case of the statistical kind: signature grinding, fee-rate selection, script and address types, nSequence and locktime conventions each vary from one wallet to the next. While some wallets randomize these, and specific entities may use more than one piece of software, fingerprint based clustering both more generally2122 and specifically in the context of payjoin23 has thus far proven to be extremely powerful.

In principle this can be mitigated by the Sisyphean task of making all wallets behave the same, but this problem is pretty widespread, and while fixing it is a necessary precondition for clustering resistance, it is not sufficient for addressing the concern, since wallet fingerprints are not the only dimensions along which clusters can be compared for similarity.

Social Graphs

Turning to the structural features, in subsequent work24 the same authors showed just a small number of confidently identified nodes suffice for matching the vertices of two anonymized social graphs. Such a matching can be obtained iteratively starting from a small seed of a few matched nodes. This builds on the sparsity exploited by the previous work. Here a "dimension" can be thought of roughly as whether some target vertex is related to some other specific vertex. In a social graph most nodes will be readily identifiable based on the identities of their neighbors. The actual result in the paper is significantly stronger, requiring only a seed of just a few matched nodes in order to iteratively propagate the matching along the graphs. Later work by Narayanan et al generalized this further to link prediction on incomplete graphs.25

Suppose the adversary starts with an incomplete clustering of Bitcoin transaction outputs. While the clusters do link together some outputs, because it's incomplete for every user there may be several clusters. So long as the adversary still hasn't collected enough evidence to conclude that several clusters belong to the same user, that user still enjoys pseudonymity.

This adversary doesn't just apply CIOH blindly. A transaction suspected to be a Payjoin, for example, will have at least 4 linked clusters, with each entity represented by at least one input-linked cluster and at least one output-linked cluster. Naive CIOH would compel merging of all of the input clusters.

Starting with the transaction graph, where coins are vertices, let clusters be represented by undirected edges connecting the linked coins. By edge contraction on these edges, a sort of graph minor26 can be obtained from a given clustering, where all of the coins of a particular cluster are fused into just one vertex representing the cluster itself. The residual edges of this now multigraph correspond to transfers of Bitcoin.

Reid and Harrigans' paper3, in its discussion of clustering, introduced the notion of a user graph, where vertices are users and edges represent the relationships between them. If the clustering is complete, and each user can be identified with just node on the contracted graph, that is the user graph. Otherwise, when the clustering is incomplete, this cluster graph is better thought of as a pseudonym graph rather than a user graph. This is a social network per Shamtikov and Narayanan's definition.27 The statistical feature distributes of clusters reduce to attributes on the vertices, and both those and the structural features can inform the edge attributes, in addition to the latter permitting.

In order to apply Narayanan and Shmatikov's algorithm more than one social graph is required, but the kinda sorta graph minor that can be obtained by contraction is just one such graph. But we could first partition the cluster-annotated coin graph, for example by leveraging the notion of time baked into the blockchain and splitting it into epochs. Contracting each subgraph separately, each corresponding graph minor would present as a somewhat different of the pseudonym graph. The more active a particular user, the more likely they are to lose pseudonymity, since more data will leak. Furthermore, the more consistent their economic activity, the more stable their representation will be in the resulting graphs, because more of their relationships will be apparent. Every pair of such graphs is then, given just a few seeds (clusterings across epochs), susceptible to deanonymization using the propagation algorithm introduced by Narayanan and Shmatikov.

Although a proliferation of pseudonyms may limit the effectiveness of this approach for any particular run of the propagation algorithm, high confidence links obtained by this method can feed into other clustering heuristics. Additionally, there are many ways of partitioning the transaction graph than just by epoch. A more optimal partitioning would be, as a very crude analogy, the "opposite" of expander decomposition: instead of identifying a sparse cut that decomposes the graph into several components, each of which is a very well connected expander, the adversary decomposes the cluster graph along a high ambiguity cut, such that each component is relatively sparser, and then matching these components.

Some clustering must already have been produced by another technique for this to work, hence the starting assumption that the clustering is partial. It supplies both the annotations that make contraction possible in the first place, and provides the seed correspondence from which graph matching can propagate.

So, even when net settlements call amount based analysis, and the number of parties complicates things further, the nature of social networks and of the transaction graph is that over time a lot of structure will inevitably revealed. There is an expression, I think it goes something like "show [the adversary] who your friends are, and [the adversary] will [successfully deanonymize you]".

Equal amount CoinJoin among arbitrary peers

Every construction so far required its participants to be doing business with one another: a payment, a batch of payments, a settlement of mutual obligations.

A CoinJoin does not have this limitation.28 The participants of an equal amount CoinJoin transaction spend their inputs, with no payment passing between them, to produce outputs of the same amount and script type.

flowchart LR
  classDef ac fill:#7993b6,stroke:#000,color:#111,rx:12,ry:12
  classDef bc fill:#e9a969,stroke:#000,color:#111,rx:12,ry:12
  classDef cc fill:#da817d,stroke:#000,color:#111,rx:12,ry:12
  classDef txn fill:#444,stroke:#000,color:#fff
  classDef amb fill:#fafafa,stroke:#777,stroke-dasharray:4 3,color:#111,rx:12,ry:12
  a1("Alice's input 0.7"):::ac --> cj["CoinJoin"]:::txn
  b1("Bob's input 0.25"):::bc --> cj
  b2("Bob's input 0.15"):::bc --> cj
  c1("Carol's input 0.55"):::cc --> cj
  cj --> e1("output 0.3"):::amb
  cj --> cb("Bob's change 0.1"):::bc
  cj --> e2("output 0.3"):::amb
  cj --> cc2("Carol's change 0.25"):::cc
  cj --> e3("output 0.3"):::amb
  cj --> ca("Alice's change 0.4"):::ac
Loading

The equal outputs are interchangeable, but the change outputs remain linked to the inputs by their amounts, and Bob's two inputs are linked to each other by the consolidation forced by a minimum denomination requirement.

Because no payments are made, the peers do not need to have any economic relationship to each other. This means they can be chosen randomly from a broader population rather than from one's counterparties. While it may seem that this already confounds social-graph analysis, this type of CoinJoin is readily identifiable. A cautious adversary can decline to apply CIOH within it, leaving additional cluster pseudonyms, while still matching the implied user graphs on either side.24 Later payments continue to expose recurring economic relationships and therefore community structure.

What it can do is introduce uncertainty about the origins of a coin, limiting the ability to perform input side clustering for any subsequent transactions as well. This slows the rate at which certainty can be gained about clustering structures, but it is not a comprehensive or robust defense, and over time such structures will generally be revealed.2930

Unfortunately, linking inputs to one another is practically inherent in the equal-amount approach.31 Users generally cannot choose incoming payment amounts or outgoing-payment change to match a CoinJoin denomination, so reaching that denomination often requires combining inputs and almost always produces change. This change is also relatively easy to link to the input clusters, as discussed in the next section.

Turning to the equal amount outputs, their order is presumed to be meaningless, so as long as they are unspent they are interchangeable. But this does not last indefinitely. Any spending transactions of these outputs will reveal more information about an outputs' ownership, through its timing, fingerprints, and other information that can be incorporated into the adversary's clustering model.

Furthermore, even though such a transaction only spends a single output, any change left from the payment will, in general, not be of a specific denomination either. In order to avoid harming the privacy of the payment transaction, this change should not be linkable to the spender's other on chain activity, but even if spent into a CoinJoin, the equal amount approach necessitates consolidating multiple such coins together, which potentially links many "anonymous" transactions to each other, and to subsequent transactions via the change from the CoinJoin.

For example, suppose Alice obtains two seemingly anonymous coins from two independent CoinJoins of the same denomination, and spends each in its own payment:

flowchart LR
  classDef rc fill:#9dc5c1,stroke:#000,color:#111,rx:12,ry:12
  classDef r2c fill:#88b279,stroke:#000,color:#111,rx:12,ry:12
  classDef txn fill:#444,stroke:#000,color:#fff
  classDef amb fill:#fafafa,stroke:#777,stroke-dasharray:4 3,color:#111,rx:12,ry:12
  cj1["CoinJoin 1"]:::txn
  cj1 --> m2("output 0.1"):::amb
  cj1 --> m3("output 0.1"):::amb
  cj1 --> m1("output 0.1"):::amb
  cj2["CoinJoin 2"]:::txn
  cj2 --> n1("output 0.1"):::amb
  cj2 --> n2("output 0.1"):::amb
  cj2 --> n3("output 0.1"):::amb
  m1 --> tx1["transaction"]:::txn
  tx1 --> p1("payment 0.03"):::rc
  tx1 --> g1("change 0.07"):::amb
  n1 --> tx2["transaction"]:::txn
  tx2 --> g2("change 0.04"):::amb
  tx2 --> p2("payment 0.06"):::r2c
Loading

Two mixed coins spent independently: each payment is ambiguous, and nothing links the payments to the same user.

Later, wanting to CoinJoin again, she consolidates the two leftover change outputs:

flowchart LR
  classDef ac fill:#7993b6,stroke:#000,color:#111,rx:12,ry:12
  classDef bc fill:#e9a969,stroke:#000,color:#111,rx:12,ry:12
  classDef rc fill:#9dc5c1,stroke:#000,color:#111,rx:12,ry:12
  classDef r2c fill:#88b279,stroke:#000,color:#111,rx:12,ry:12
  classDef txn fill:#444,stroke:#000,color:#fff
  classDef amb fill:#fafafa,stroke:#777,stroke-dasharray:4 3,color:#111,rx:12,ry:12
  classDef acg fill:#7993b6,stroke:#ffe234,stroke-width:5px,color:#111,rx:12,ry:12
  cj1["CoinJoin 1"]:::txn
  cj1 --> m2("output 0.1"):::amb
  cj1 --> m3("output 0.1"):::amb
  cj1 --> m1("Alice's output 0.1"):::acg
  cj2["CoinJoin 2"]:::txn
  cj2 --> n1("Alice's output 0.1"):::acg
  cj2 --> n2("output 0.1"):::amb
  cj2 --> n3("output 0.1"):::amb
  m1 --> tx1["transaction"]:::txn
  tx1 --> p1("payment 0.03"):::rc
  tx1 --> g1("Alice's change 0.07"):::acg
  n1 --> tx2["transaction"]:::txn
  tx2 --> g2("Alice's change 0.04"):::acg
  tx2 --> p2("payment 0.06"):::r2c
  g1 --> cj3["CoinJoin 3"]:::txn
  g2 --> cj3
  b1("Bob's input 0.13"):::bc --> cj3
  cj3 --> q1("output 0.1"):::amb
  cj3 --> q2("output 0.1"):::amb
  cj3 --> q3("Alice's change 0.01"):::acg
  cj3 --> q4("Bob's change 0.03"):::bc
  linkStyle 6,8,9,10,12,13,17 stroke:#ffe234,stroke-width:4px
Loading

The consolidating CoinJoin links the two change outputs, which links the two payments, and retroactively identifies Alice's outputs in both of the original CoinJoins.

In other words, attempting to CoinJoin in order to recover the change and mame it usable for private payments not only fails to achieve that goal, but also undoes the privacy of the previous transactions.

Arbitrary amount CoinJoin

So strict adherence to the equal amount approach leaks information about linkage after the fact. Naive batching, which we glossed over, does too, even without considering the wider context of the graph.32

The Boltzmann link probability matrix33 and Maurer et al's sub-transaction model34 are two similar models. They employ a combinatorial approach: count all the ways that the transaction could be partitioned, grouping together subsets of the inputs and outputs into sub-transactions, which are assumed to be the actions of a single party in a multiparty transaction.

What exactly makes a partition a valid sub-transaction mapping is whether or not the sub-transaction is deemed plausible. In Maurer et al, the values of the inputs and the outputs must exactly cancel out, which is not sufficiently general for real world analyses. Boltzmann casts a wider net, by allowing these to vary somewhat, accounting for fees (including JoinMarket maker fees).

In these models, even if the number of coins and therefore partitions is large, if the probabilities over the partitions are far from uniform this amounts to relatively little privacy. This is typically quantified in terms of entropy.35363738

When a sub-transaction mapping is underdetermined, i.e. there is more than one way to link inputs to outputs in separate sub-transactions, then there is ambiguity as to how to link the coins of those mutually exclusive interpretations. This can be achieved overtly by choosing output values to generate many possibilities.39

flowchart LR
  classDef ac fill:#7993b6,stroke:#000,color:#111,rx:12,ry:12
  classDef bc fill:#e9a969,stroke:#000,color:#111,rx:12,ry:12
  classDef txn fill:#444,stroke:#000,color:#fff
  b1("Bob's input 2"):::bc --> cj["Bad CoinJoin"]:::txn
  a1("Alice's input 0.3"):::ac --> cj
  b2("Bob's input 5"):::bc --> cj
  a2("Alice's input 0.1"):::ac --> cj
  cj --> o1("Alice's output 0.4"):::ac
  cj --> o2("Bob's output 7"):::bc
Loading

Carelessly chosen values are no better than a naive batch: the only sub-transaction mapping consistent with the amounts is 0.1 + 0.3 = 0.4 and 2 + 5 = 7, so a subset sum analysis fully partitions the transaction.

flowchart LR
  classDef txn fill:#444,stroke:#000,color:#fff
  classDef amb fill:#fafafa,stroke:#777,stroke-dasharray:4 3,color:#111,rx:12,ry:12
  b1("input 0.1"):::amb --> cj["CoinJoin"]:::txn
  a2("input 0.4"):::amb --> cj
  b2("input 0.2"):::amb --> cj
  b3("input 0.5"):::amb --> cj
  a1("input 0.3"):::amb --> cj
  cj --> o1("output 0.7"):::amb
  cj --> o2("output 0.8"):::amb
Loading

With values chosen to be underdetermined, three distinct mappings balance: the 0.7 output can be funded by 0.3 + 0.4, by 0.2 + 0.5, or by 0.1 + 0.2 + 0.4, with the remaining inputs funding the 0.8 output. Across these readings — and the single-owner one — no input is linked to either output.

Alternatively, the assumption of the model itself can be invalidated. This is exactly the line of reasoning from the discussion leading up to net-settlement transactions. There is no technical reason why net-settlements between economically related users should not also be possible within such a protocol that also lets strangers be parties to the same transaction. And indeed economically related users who settle in this way can also better conceal their business relationships when they do that in the context of a broader transaction.

Unfortunately, something that is omitted by both models but which should hopefully be apparent based on the above, is that the adversary can incorporate information from the surrounding context of the graph to its sub-transaction analysis, not just the input and output values. So while net-settlement can improve the resistance to analysis, as can the overt structural ambiguity of transactions with carefully chosen output values, they are no panacea.

Taking provenance into account

Enumerating the possible points of origin of the funds of a particular coin is fairly intuitive based on the graph structure.40 The entropy of the probability distribution of this candidate set can grow fairly quickly by CoinJoining, especially if an effort is made to diversify peer selection.

However, it can also decay rather quickly. Suppose the adversary successfully deanonymizes some auxiliary user, this can have two very different outcomes:

  • The coins of that user are eliminated as linking candidates when attempting to deanonymize the target user. This is an additive decay of privacy.
  • In addition, the eliminated coins form the boundary between regions of the transaction graph, fracturing it. This is a multiplicative decay of privacy, the adversary can make progress at an exponential rate when this is the case.

Merely quantifying the entropy at a point in time does not account for how it may decay over time. And even if it did, it's impossible to account for any private information the adversary may have.

For example, a chain analysis vendor with access to KYC information may have sufficiently granular data to be operating in the exponential decay regime for graphs which seemingly imply high entropy.

Robust connectivity

If the CoinJoin graph is robustly connected41, then no small cut separates any output from the mass of its candidate origin coins. Stated differently, every output is connected to its candidate origins by multiple disjoint paths.

This safety margin delays the brittle cliff-edge of exponential decay, extending the duration of the additive decay regime, by requiring the adversary to deanonymize a much larger proportion of users before divide and conquer tactics start coming into play.

But when two post-CoinJoin outputs are linked, on chain or otherwise, this makes intersection attacks4238 possible. In such attacks, we take the candidate origins of these now linked coins, and check for any overlaps.

flowchart LR
  classDef ac fill:#7993b6,stroke:#000,color:#111
  classDef bc fill:#e9a969,stroke:#000,color:#111
  classDef cc fill:#da817d,stroke:#000,color:#111
  classDef dc fill:#9dc5c1,stroke:#000,color:#111
  classDef enc fill:#88b279,stroke:#000,color:#111
  classDef txn fill:#444,stroke:#000,color:#fff
  classDef amb1 fill:#fdf6ec,stroke:#777,stroke-dasharray:4 3,color:#111,rx:12,ry:12
  classDef amb2 fill:#edf5f5,stroke:#777,stroke-dasharray:4 3,color:#111,rx:12,ry:12
  B(["Bob's cluster"]):::bc --> cj1
  C(["Carol's cluster"]):::cc --> cj1
  A(["Alice's cluster"]):::ac --> cj1["CoinJoin 1"]:::txn
  A --> cj2["CoinJoin 2"]:::txn
  D(["Dave's cluster"]):::dc --> cj2
  E(["Erin's cluster"]):::enc --> cj2
  cj1 --> x1("Alice, Bob or Carol?"):::amb1
  cj1 --> x2("Alice, Bob or Carol?"):::amb1
  cj1 --> x3("Alice, Bob or Carol?"):::amb1
  cj2 --> y1("Alice, Dave or Erin?"):::amb2
  cj2 --> y2("Alice, Dave or Erin?"):::amb2
  cj2 --> y3("Alice, Dave or Erin?"):::amb2
Loading

The adversary's view while the CoinJoin outputs remain unspent: six outputs in two equivalence classes, each output ambiguous among its CoinJoin's participants.

flowchart LR
  classDef txn fill:#444,stroke:#000,color:#fff
  classDef amb fill:#fafafa,stroke:#777,stroke-dasharray:4 3,color:#111,rx:12,ry:12
  classDef aci fill:#7993b6,stroke:#e9a969,stroke-width:8px,color:#000,font-weight:bold
  classDef bc1 fill:#e9a969,stroke:#ffe234,stroke-width:2px,color:#111
  classDef cc1 fill:#da817d,stroke:#ffe234,stroke-width:2px,color:#111
  classDef aco1 fill:#7993b6,stroke:#ffe234,stroke-width:3px,color:#111,rx:12,ry:12
  classDef dc2 fill:#9dc5c1,stroke:#ff7a76,stroke-width:2px,color:#111
  classDef enc2 fill:#88b279,stroke:#ff7a76,stroke-width:2px,color:#111
  classDef aco2 fill:#7993b6,stroke:#ff7a76,stroke-width:3px,color:#111,rx:12,ry:12
  B(["Bob's cluster"]):::bc1 --> cj1
  C(["Carol's cluster"]):::cc1 --> cj1
  A(["Alice's cluster"]):::aci --> cj1["CoinJoin 1"]:::txn
  A --> cj2["CoinJoin 2"]:::txn
  D(["Dave's cluster"]):::dc2 --> cj2
  E(["Erin's cluster"]):::enc2 --> cj2
  cj1 --> xo1("Bob or Carol?"):::amb
  cj1 --> x("Alice's output"):::aco1
  cj1 --> xo2("Bob or Carol?"):::amb
  cj2 --> y("Alice's output"):::aco2
  x --> sp["transaction"]:::txn
  y --> sp
  cj2 --> yo1("Dave or Erin?"):::amb
  cj2 --> yo2("Dave or Erin?"):::amb
  linkStyle 0,1 stroke:#ffe234,stroke-width:2px
  linkStyle 2,7,10 stroke:#ffe234,stroke-width:5px
  linkStyle 4,5 stroke:#ff7a76,stroke-width:2px
  linkStyle 3,9,11 stroke:#ff7a76,stroke-width:5px
Loading

Alice's spend links one output from each class. Tracing each input back — from spent output, through its creating CoinJoin, and to the pre-CoinJoin input clusters — gives the two antecessor sets, highlighted in yellow and red. These overlap only at Alice's cluster, highlighted as the one node belonging to both: the intersection identifies her, and by elimination also narrows the remaining outputs' candidate sets: CoinJoin 1's to Bob or Carol, CoinJoin 2's to Dave or Erin.

A successful intersection makes progress at a rate that is exponential in the information given to the adversary. If the set is of size $n$, then only $O(\log n)$ information may suffice to pin point a single element, whereas by just striking candidates off of the list in a naive process of elimination, $O(n)$ information may be required.

This holds unless the size of symmetric difference between antecessor sets is bounded by a constant, i.e. the antecessor sets of linked coins are very similar, because the size of the intersection is similar to the size of the intersected sets. This attack is powerful because in most cases that isn't the case, and the size of the intersection will usually be significantly smaller.

It's also important to bear in mind that the candidate origins are not coins, but rather clusters, so the objects being intersected needn't be connected on the transaction graph for the adversary to notice an overlap. This is particularly concerning because the clusters being intersected are the pre-CoinJoin ones, where presumably privacy is inherently weak. It's also worth emphasizing that the change outputs of equal amount CoinJoins are part of these clusters as well.

Next, consider an adversary who is also a counterparty. For example, an ATM in some remote location that sells coins to a user in exchange for cash, and later receives funds from descendents of those coins. Such a counterparty knows, from its own records, exactly which coins it paid out and can observe when descendents of those coins are sold back to it. Connectivity to a crowd of strangers over the internet does little against this, because none of those strangers are likely to have transacted with the ATM, and the CoinJoin does not sever the link from a coin to its past. By the same logic of intersection attacks, privacy loss arising from consolidation of more than one input doesn't just add up, it compounds. This is known as the Eve-Alice-Eve threat model.

Own-origin robustness

The privileged counterparty knows one endpoint because it is the origin, so the property is stated relative to that origin. It demands that a coin have many plausible, counterfactual paths back to the user's own antecedent coins.

Suppose Alice withdraws from that ATM, and that is her only means of obtaining Bitcoin. She then CoinJoins with Bob. If one of those coins is then returned to the ATM, its operator can conclude that it is likely Alice.

flowchart LR
  classDef ac fill:#7993b6,stroke:#000,color:#111,rx:12,ry:12
  classDef bc fill:#e9a969,stroke:#000,color:#111,rx:12,ry:12
  classDef ec fill:#b996b2,stroke:#000,color:#111,rx:12,ry:12
  classDef txn fill:#444,stroke:#000,color:#fff
  classDef amb fill:#fafafa,stroke:#777,stroke-dasharray:4 3,color:#111,rx:12,ry:12
  atm(("ATM")):::ec -. "withdrawal" .-> a0("Alice's coin"):::ac
  a0 --> cj["CoinJoin<br/>Alice + Bob"]:::txn
  b0("Bob's coin"):::bc --> cj
  cj --> x1("Alice or Bob?"):::amb
  cj --> x2("Alice or Bob?"):::amb
  x1 --> deposit["deposit"]:::txn
  deposit --> atmd("ATM's output"):::ec
Loading

Taken in isolation the deposited coin is ambiguous: it could be Alice's or Bob's.

flowchart LR
  classDef ac fill:#7993b6,stroke:#000,color:#111,rx:12,ry:12
  classDef bc fill:#e9a969,stroke:#000,color:#111,rx:12,ry:12
  classDef ec fill:#b996b2,stroke:#000,color:#111,rx:12,ry:12
  classDef txn fill:#444,stroke:#000,color:#fff
  classDef acg fill:#7993b6,stroke:#ffe234,stroke-width:5px,color:#111,rx:12,ry:12
  atm(("ATM")):::ec -. "withdrawal" .-> a0("Alice's coin"):::acg
  a0 --> cj["CoinJoin<br/>Alice + Bob"]:::txn
  b0("Bob's coin"):::bc --> cj
  cj --> x1("Alice's output"):::acg
  cj --> x2("Bob's output"):::bc
  x1 --> deposit["deposit"]:::txn
  deposit --> atmd("ATM's output"):::ec
  linkStyle 0,1,3,5,6 stroke:#ffe234,stroke-width:4px
Loading

But in this minimal example the ambiguity is thin. The ATM can conclude with reasonable certainty that the deposit is Alice's, because it is much more likely to get repeated business from her than it is for her to randomly interact with Bob just before Bob happens to use the ATM.

Consolidation makes the ATM's job easier still. Suppose Alice withdraws two coins, mixes each once with a different peer, and then spends the two mixed outputs together:

flowchart LR
  classDef ac fill:#7993b6,stroke:#000,color:#111,rx:12,ry:12
  classDef bc fill:#e9a969,stroke:#000,color:#111,rx:12,ry:12
  classDef cc fill:#da817d,stroke:#000,color:#111,rx:12,ry:12
  classDef ec fill:#b996b2,stroke:#000,color:#111,rx:12,ry:12
  classDef txn fill:#444,stroke:#000,color:#fff
  classDef acg fill:#7993b6,stroke:#ffe234,stroke-width:5px,color:#111,rx:12,ry:12
  atm(("ATM")):::ec -. "withdrawal" .-> a0("Alice's coin"):::acg
  atm -. "withdrawal" .-> a0b("Alice's other coin"):::acg
  b0("Bob's coin"):::bc --> cj1
  a0 --> cj1["CoinJoin 1<br/>Alice + Bob"]:::txn
  cj1 --> o2("Bob's output"):::bc
  cj1 --> o1("Alice's output"):::acg
  a0b --> cj2["CoinJoin 2<br/>Alice + Carol"]:::txn
  c0("Carol's coin"):::cc --> cj2
  cj2 --> o3("Alice's output"):::acg
  cj2 --> o4("Carol's output"):::cc
  o1 --> deposit["deposit"]:::txn
  o3 --> deposit
  deposit --> atmd("ATM's output"):::ec
  linkStyle 0,1,3,5,6,8,10,11,12 stroke:#ffe234,stroke-width:4px
Loading

Each CoinJoin output on its own is ambiguous, but only Alice is consistent with both inputs of the consolidation, so the deposit identifies her. Additional inputs amplify the ATM's observing power exponentially.

Suppose instead that Bob CoinJoins with another user, Carol, and then Carol CoinJoins with Alice.

If Carol were to use the ATM at that point, her coin has inherited Alice's provenance, which may cause the ATM to incorrectly conclude that it is Alice which is using it. By symmetry, if it was Alice who did that, she gets some degree of plausible deniability.

flowchart LR
  classDef ac fill:#7993b6,stroke:#000,color:#111,rx:12,ry:12
  classDef bc fill:#e9a969,stroke:#000,color:#111,rx:12,ry:12
  classDef cc fill:#da817d,stroke:#000,color:#111,rx:12,ry:12
  classDef ec fill:#b996b2,stroke:#000,color:#111,rx:12,ry:12
  classDef txn fill:#444,stroke:#000,color:#fff
  atm(("ATM")):::ec -. "withdrawal" .-> a0("Alice's coin"):::ac
  a0 --> cj1["CoinJoin 1<br/>Alice + Bob"]:::txn
  b0("Bob's coin"):::bc --> cj1
  cj1 --> a1("Alice's output"):::ac
  cj1 --> b1("Bob's output"):::bc
  b1 --> cj2["CoinJoin<br/>Bob + Carol"]:::txn
  c0("Carol's coin"):::cc --> cj2
  cj2 --> c1("Carol's output"):::cc
  cj2 --> b2("Bob's output"):::bc
  a1 --> cj3["CoinJoin 3<br/>Carol + Alice"]:::txn
  c1 --> cj3
  cj3 --> o1("Alice's output"):::ac
  cj3 --> o2("Carol's output"):::cc
  o2 --> deposit["deposit"]:::txn
  deposit --> atmd("ATM's output"):::ec
Loading

In this scenario it is actually Carol who deposits, and the funds was never in Alice's posession, but its history is interwoven with Alice's through the CoinJoins, which by the same logic would cause the adversary to mistakenly conclude that it is Alice.

flowchart LR
  classDef ac fill:#7993b6,stroke:#000,color:#111,rx:12,ry:12
  classDef bc fill:#e9a969,stroke:#000,color:#111,rx:12,ry:12
  classDef cc fill:#da817d,stroke:#000,color:#111,rx:12,ry:12
  classDef ec fill:#b996b2,stroke:#000,color:#111,rx:12,ry:12
  classDef txn fill:#444,stroke:#000,color:#fff
  classDef amb fill:#fafafa,stroke:#777,stroke-dasharray:4 3,color:#111,rx:12,ry:12
  atm(("ATM")):::ec -. "withdrawal" .-> a0("Alice's coin"):::ac
  a0 --> cj1["CoinJoin 1<br/>Alice + Bob"]:::txn
  b0("Bob's coin"):::bc --> cj1
  cj1 --> a1("Alice or Bob?"):::amb
  cj1 --> b1("Alice or Bob?"):::amb
  b1 --> cj2["CoinJoin<br/>Bob + Carol"]:::txn
  c0("Carol's coin"):::cc --> cj2
  cj2 --> c1("Alice, Bob or Carol?"):::amb
  cj2 --> b2("Alice, Bob or Carol?"):::amb
  a1 --> cj3["CoinJoin 3<br/>Carol or Bob + Alice"]:::txn
  c1 --> cj3
  cj3 --> o1("Alice, Bob or Carol?"):::amb
  cj3 --> o2("Alice, Bob or Carol?"):::amb
  o2 --> deposit["deposit"]:::txn
  deposit --> atmd("ATM's output"):::ec
Loading

The same transactions from the ATM's point of view: after CoinJoin 3 the two outputs are equivalent — whichever one is deposited has plausible paths back to the dispensed coin, directly through CoinJoin 3 or via Bob through CoinJoins 1 and 2 — so the ATM cannot tell Alice returning from either Bob or Carol.

Repeated CoinJoins can spread Alice's coins' provenance fingerprint to other users' coins while spreading their features to hers. This allows her to build up to a quantifiable anonymity set.

flowchart LR
  classDef ac fill:#7993b6,stroke:#000,color:#111,rx:12,ry:12
  classDef bc fill:#e9a969,stroke:#000,color:#111,rx:12,ry:12
  classDef cc fill:#da817d,stroke:#000,color:#111,rx:12,ry:12
  classDef dc fill:#9dc5c1,stroke:#000,color:#111,rx:12,ry:12
  classDef enc fill:#88b279,stroke:#000,color:#111,rx:12,ry:12
  classDef fc fill:#ecd580,stroke:#000,color:#111,rx:12,ry:12
  classDef txn fill:#444,stroke:#000,color:#fff
  classDef txni fill:#444,stroke:#5778a4,stroke-width:6px,color:#fff
  classDef ai fill:#7993b6,stroke:#5778a4,stroke-width:6px,color:#111,rx:12,ry:12
  classDef bi fill:#e9a969,stroke:#5778a4,stroke-width:6px,color:#111,rx:12,ry:12
  classDef ci fill:#da817d,stroke:#5778a4,stroke-width:6px,color:#111,rx:12,ry:12
  classDef di fill:#9dc5c1,stroke:#5778a4,stroke-width:6px,color:#111,rx:12,ry:12
  a0("Alice's coin"):::ac --> cj1["CoinJoin<br/>Alice + Bob"]:::txni
  b0("Bob's coin"):::bc --> cj1
  cj1 --> a1("Alice's output"):::ai
  cj1 --> b1("Bob's output"):::bi
  b1 --> cj2["CoinJoin<br/>Bob + Carol"]:::txni
  c0("Carol's coin"):::cc --> cj2
  cj2 --> b2("Bob's output"):::bi
  cj2 --> c1("Carol's output"):::ci
  d0("Dave's coin"):::dc --> cjx1["CoinJoin<br/>Dave + Erin"]:::txn
  e0("Erin's coin"):::enc --> cjx1
  cjx1 --> d1("Dave's output"):::dc
  cjx1 --> e1("Erin's output"):::enc
  c1 --> cj3["CoinJoin<br/>Carol + Dave"]:::txni
  d1 --> cj3
  cj3 --> c2("Carol's output"):::ci
  cj3 --> d2("Dave's output"):::di
  e1 --> cjx2["CoinJoin<br/>Erin + Frank"]:::txn
  f0("Frank's coin"):::fc --> cjx2
  cjx2 --> e2("Erin's output"):::enc
  cjx2 --> f1("Frank's output"):::fc
  linkStyle 0,2,3,4,6,7,12,14,15 stroke:#5778a4,stroke-width:3px
Loading

Coins are colored by their true owner; a thick blue border marks the coins descending from Alice's. Dave's coin picks up the fingerprint when mixing with Carol, for example.

As this set grows, Alice's chances of CoinJoining her actual funds with such a coin increase with time. If Alice participates in enough CoinJoins so that eventually inputs to the next CoinJoin are already descendents of her prior coin and many such counterfactual paths exist, that can provide privacy even against an adversarial counterparty, which by finding its own coins in the intersection is able to cluster much more reliably than a 3rd party observer relying on heuristics.

flowchart LR
  classDef ac fill:#7993b6,stroke:#000,color:#111,rx:12,ry:12
  classDef bc fill:#e9a969,stroke:#000,color:#111,rx:12,ry:12
  classDef cc fill:#da817d,stroke:#000,color:#111,rx:12,ry:12
  classDef dc fill:#9dc5c1,stroke:#000,color:#111,rx:12,ry:12
  classDef enc fill:#88b279,stroke:#000,color:#111,rx:12,ry:12
  classDef fc fill:#ecd580,stroke:#000,color:#111,rx:12,ry:12
  classDef gc fill:#b9a0d8,stroke:#000,color:#111,rx:12,ry:12
  classDef txn fill:#444,stroke:#000,color:#fff
  classDef txni fill:#444,stroke:#5778a4,stroke-width:6px,color:#fff
  classDef ai fill:#7993b6,stroke:#5778a4,stroke-width:6px,color:#111,rx:12,ry:12
  classDef bi fill:#e9a969,stroke:#5778a4,stroke-width:6px,color:#111,rx:12,ry:12
  classDef ci fill:#da817d,stroke:#5778a4,stroke-width:6px,color:#111,rx:12,ry:12
  classDef di fill:#9dc5c1,stroke:#5778a4,stroke-width:6px,color:#111,rx:12,ry:12
  classDef ei fill:#88b279,stroke:#5778a4,stroke-width:6px,color:#111,rx:12,ry:12
  classDef fi fill:#ecd580,stroke:#5778a4,stroke-width:6px,color:#111,rx:12,ry:12
  classDef gi fill:#b9a0d8,stroke:#5778a4,stroke-width:6px,color:#111,rx:12,ry:12

  a0("Alice"):::ac --> cj1[" "]:::txni
  b0("Bob"):::bc --> cj1
  cj1 --> a1("Alice"):::ai
  cj1 --> b1("Bob"):::bi
  a1 --> cj6[" "]:::txni

  b1 --> cj2[" "]:::txni
  c0("Carol"):::cc --> cj2
  cj2 --> c1("Carol"):::ci
  cj2 --> b2("Bob"):::bi
  d0("Dave"):::dc --> cjx1[" "]:::txn
  e0("Erin"):::enc --> cjx1
  cjx1 --> d1("Dave"):::dc
  cjx1 --> e1("Erin"):::enc
  c1 --> cj3[" "]:::txni
  d1 --> cj3
  cj3 --> d2("Dave"):::di
  cj3 --> c2("Carol"):::ci
  e1 --> cjx2[" "]:::txn
  f0("Frank"):::fc --> cjx2
  cjx2 --> e2("Erin"):::enc
  cjx2 --> f1("Frank"):::fc

  b2 --> cj4[" "]:::txni
  e2 --> cj4
  cj4 --> b3("Bob"):::bi
  cj4 --> e3("Erin"):::ei

  d2 --> cj5[" "]:::txni
  f1 --> cj5
  cj5 --> d3("Dave"):::di
  cj5 --> f2("Frank"):::fi

  b3 --> cj6
  d3 --> cj6
  g0("Grace"):::gc --> cj6
  cj6 --> a2("Alice"):::ai
  cj6 --> b4("Bob"):::bi
  cj6 --> d4("Dave"):::di
  cj6 --> g1("Grace"):::gi

  linkStyle 0,2,3,4,5,7,8,13,15,16,21,23,24,25,27,28,29,30,32,33,34,35 stroke:#5778a4,stroke-width:4px
Loading

Continuing the preceding graph, Bob and Dave spend marked outputs along separate branches. As more marked outputs circulate, more branches can spread Alice's deep feature. In Alice's next four-input CoinJoin, Bob's and Dave's inputs also descend from her original coin; only Grace's does not.

Let's call provenance specific structural features deep features. When spending more than one coin, if the coins have similarly distributed deep features then they will have similar antecessor sets.

In order to quantify an anonymity set size, Alice can count the inputs belonging to other users that already share her coins' deep features,43, i.e. inputs that counterfactually trace back to her origin coins. Many such counterfactual paths imply that intersected candidate sets shrink only a linear rate, because Alice's coins would also be in the intersections arising from post CoinJoin linking of coins that have nothing to do with her.

This is the most conservative notion of on chain privacy described in this document. CoinJoin transactions that have the structural properties discussed, where peers are chosen so as to satisfy these robustness properties, are required in order to frustrate a real world deanonymization adversary, because without them the information required to deanonymize is often revealed in the transaction graph itself, which already is very harmful for privacy, fungibility and censorship resistance, but perhaps catastrophically it is augmented by many other privacy loss vectors, such as blockchain indexing services for light clients, KYC information, leaks from data breaches, transport level information like IP addresses, etc.

Note however that this still requires that the other ATM users also CoinJoin; otherwise Alice's history will still be unique. Even if she tries to obscure this using "normal looking" transactions, the ATM could still distinguish Alice based on the fact that these transactions descend from the CoinJoin subgraph, simply because those of the other users do not. Determining whether she is the only such user is something that can be judged based on evidence like temporal patterns (correlations or periodic activity). Such patterns are more easily discernable when there are clearly delineated CoinJoin-using and non-CoinJoin-using populations in the transaction graph as a whole.

Censorship resistance vs. privacy tradeoffs and complementarity

The constructions discussed here form a spectrum: unilateral transactions, two-party payjoins, many senders and receivers, net-settlement, and market-based coinjoin among strangers, constructed with increasing degrees of connectivity robustness.

One end offers censorship resistance and generally weak privacy; the other offers stronger privacy guarantees at the cost of an overt fingerprint.

Note that this does not refer to on-chain censorship resistance specifically, we must take for granted that at least some miners will include CoinJoin transactions for any of this to work. The relevant risk is censorship by businesses: an exchange or custodian that refuses, freezes, or discounts coins by their history, and in particular if their history uses privacy enhanced transactions.44

This is not zero sum. The two ends then reinforce each other. The censorship resistant end gains fungibility and can better resist clustering from being interwoven with the well-connected structure. The robust anonymity end gains censorship resistance by not being an insular sub-economy that rarely touches ordinary transactions.

Multiparty transactions that don't overtly optimize for an underdetermined sub-transaction mapping can still provide ambiguity, with their susceptibility to censorship based on public information largely reduced to their proximity to the overtly privacy enhancing ones.

If this structural feature spectrum is realized on chain, the distribution of deep features labeling a user's output has a better chance of matching the deep feature distributions of more superficially similar but unrelated coins. Superficially similar coins are those with which it shares statistical features (like value distribution or wallet software fingerprints) or more general structural features (e.g. size of the containing transaction).

Recall that sparsity is requirement for Narayanan and Shmatikov's results. In today's transaction graph, cluster features, both statistical and more importantly structural, are very likely to be unique because there doesn't exist any wallet software that deliberately blends the structural features.

Sufficiently ambiguous collaborative transaction would instead produce coins that share the sum of their inputs' deep feature vectors, blending together their fingerprints. The more widely this spreads, the less sparse the feature vectors of all coins become. For the transaction graph structure to actually be connected in this way requires deliberate effort, but there is no fundamental barrier.

This seems to suggest that some of today's often misleading, but widely held beliefs or intuitions about on chain privacy and fungibility would be more accurate if such mixing of deep features were pervasive, and the boundary between such transactions and "normal" transactions was obscured by a network of covert transactions, none of them completely disconnected from the CoinJoin graph.

In other words, in a world where some users CoinJoin, and take care to construct transaction graphs with sufficiently many disjoint counterfactual paths as discussed above, and where some users engage in net settlement transactions, or 2 party PayJoins, and crucially where these subgraphs are all intertwined then the combinatorial explosion of graph based features, which today can reveal a lot of information to the adversary, will be rendered mostly inert. By construction, this would no longer satisfy the conditions for Narayanan and Shmatikov's algorithms.

Until such time, be skeptical of claims that PayJoin or even CoinJoin provide privacy against a competent adversary. Especially if it is well funded and is widely known to have access to a trove of high quality auxiliary information. And even if this does become a reality, please remember that it wouldn't address the auxiliary information concern, nor thwart any as-of-yet undiscovered analyses that perhaps exploit some subtler leaks.

Footnotes

  1. The common-input-ownership observation appears in passing in the Bitcoin whitepaper: S. Nakamoto, Bitcoin: A Peer-to-Peer Electronic Cash System

  2. For an informal introduction to wallet clustering, see The Scroll #2, Wallet Clustering Basics

  3. F. Reid and M. Harrigan, An Analysis of Anonymity in the Bitcoin System 2

  4. D. Ron and A. Shamir, Quantitative Analysis of the Full Bitcoin Transaction Graph

  5. E. Androulaki, G. Karame, M. Roeschlin, T. Scherer, S. Capkun, Evaluating User Privacy in Bitcoin

  6. S. Meiklejohn, M. Pomarole, G. Jordan, K. Levchenko, D. McCoy, G. M. Voelker, S. Savage, A Fistful of Bitcoins: Characterizing Payments Among Men with No Names

  7. J. Nick, Data-Driven De-Anonymization in Bitcoin

  8. M. Harrigan and C. Fretter, The Unreasonable Effectiveness of Address Clustering

  9. For a narrative history of clustering techniques, see The Scroll #3, A Brief History of Wallet Clustering

  10. The two-party case is not exhaustive; the space of collaborative transaction forms is larger, in particular lightning is probably the most widespread type of two-party transaction. See The Scroll #5, The Many Faces of CoinJoins

  11. Pay-to-EndPoint (P2EP)

  12. R. Havar, BIP 79: Bustapay :: a practical coinjoin protocol

  13. N. Dorier, BIP 78: A Simple Payjoin Proposal

  14. D. Gould and Y. Kogman, BIP 77: Async Payjoin

  15. AdamISZ, payjoin / unnecessary-input-heuristic writeup 2

  16. S. Ghesmati, A. Kern, A. Judmayer, N. Stifter, and E. Weippl, Unnecessary Input Heuristics & PayJoin Transactions 2

  17. Roughly 1.58 bits is the best possible case for a two-input, two-output transaction, which is a rather typical shape. Counterintuitively while additional inputs seem to suggest linear growth in entropy because of the combinatorics, that can input consolidation may result in linear reduction entropy due to consolidation as a function of the in-degree of the consolidation sub-transaction. See the discussion about intersection attacks below.

  18. rust-payjoin, PR #923 (removal of the experimental NS1R implementation, with status discussion)

  19. A. Narayanan and V. Shmatikov, Robust De-anonymization of Large Sparse Datasets (S&P 2008; the linked arXiv version is titled How To Break Anonymity of the Netflix Prize Dataset)

  20. A. Narayanan and V. Shmatikov, Robust De-anonymization of Large Sparse Datasets: a Decade Later

  21. M. Möser and A. Narayanan, Resurrecting Address Clustering in Bitcoin

  22. G. Kappos, H. Yousaf, R. Stütz, S. Rollet, B. Haslhofer, S. Meiklejohn, How to Peel a Million: Validating and Expanding Bitcoin Clusters

  23. A. Sabouri, How Wallet Fingerprints Damage Payjoin Privacy

  24. A. Narayanan and V. Shmatikov, De-anonymizing Social Networks 2

  25. A. Narayanan, E. Shi, B. I. P. Rubinstein, Link Prediction by De-anonymization

  26. This is not really a graph minor since there are directed edges. In fact it's not really a graph because it's got both directed and undirected edges.

  27. Strictly speaking their model involves a directed graph, not a multigraph, but edges are labeled with arbitrary attributes so the information about the various payments can be collected into a rich set of attributes on a single edge representing the relationship between two clusters.

  28. G. Maxwell, CoinJoin: Bitcoin privacy for the real world

  29. G. Danezis, Statistical Disclosure Attacks

  30. C. Troncoso, B. Gierlichs, B. Preneel, I. Verbauwhede, Perfect Matching Disclosure Attacks

  31. This gets worse if coins of a specific denomination are prepared before the CoinJoin, because that setup transaction is highly fingerprintable as a unilateral transaction.

  32. K. Atlas, CoinJoin Sudoku

  33. LaurentMT, Boltzmann, specifically the link-probability matrix.

  34. F. K. Maurer, T. Neudecker, M. Florian, Anonymous CoinJoin Transactions with Arbitrary Values

  35. C. Diaz, S. Seys, J. Claessens, B. Preneel, Towards Measuring Anonymity

  36. A. Serjantov and G. Danezis, Towards an Information Theoretic Metric for Anonymity

  37. P. Syverson, Why I'm not an entropist

  38. For an accessible treatment of entropy and intersection attacks on coinjoin anonymity sets, see The Scroll #4, Intersection Attacks 2

  39. https://colab.research.google.com/drive/1We_FvfX_Ob9BapFW3X_By9vTtxUrt3pm

  40. D. M. Kelen and I. A. Seres, Towards Measuring the Traceability of Cryptocurrencies

  41. Note that being connected doesn't just mean a path can be traced along the graph, but that the flow of funds along that path appears plausible.

  42. S. Goldfeder, H. Kalodner, D. Reisman, A. Narayanan, When the Cookie Meets the Blockchain

  43. Not exactly the same, since both temporal patterns and the number of hops make a difference. This can be addressed by ensuring the length of the actual path taken is consistent with the distribution of lengths of the counterfactual paths.

  44. Unsurprisingly, the most aggressive compliance posturing has historically often come from businesses that later collapsed, namely FTX, whose executives were convicted of fraud, and BlockFi, which settled SEC charges that included materially false statements before going under in FTX's wake.

@yuval-block

Copy link
Copy Markdown

Although it's captures by "at least" I think it's worth mentioning coinjoin. It can be a spend, but generally isn't, making it distinct from a PayJoin that generally is a spend. You also point that out in the section on coin-joins.

the first one was intended to convey coinjoinness (self-spend coinjoins are a special case of balanced sub-txn batching, typically with some additional constraints)

rephrased to clarify

In the naive case of one party receiving many transactions from multiple payers, e.g. a merchant, there's a peel chain that makes it clear which output is which.

"with only this transaction to go by" is meant to imply that there isn't a peel chain, i rephrased as "looking at just this transaction in isolation"

Is that still the same user? Do you mean that all ATM users are known (or at least the ATM knows it's the same person as before, e.g. comparing photos or just phone numbers)? Or just that the users are likely in a small geographic area, so joining a global coin-join and then coming back to the same ATM doesn't help?

not necessarily, but certainty increases exponentially in the number of inputs used. i forgot to mention this, added, and also clarified that the example assumes cash

all typos fixed, thanks!

@arminsabouri

Copy link
Copy Markdown

Because no payments are made, the peers do not need to have any economic relationship to each other.
...
Consequently, it cannot address the broader systemic problem on its own, since the payment transactions will still reveal the community structures.

Wdym by payment transactions in this context? Subsequent transactions?

@bc1cindy

bc1cindy commented Jul 6, 2026

Copy link
Copy Markdown

thanks for sharing this!

Wdym by payment transactions in this context? Subsequent transactions?

"the ordinary payments between counterparties elsewhere in the graph, which the CoinJoin itself does not encompass" sounds better?

typos:

  • "A batched transaction or a CoinJoin is two independent and balanced sub-transactions..."
  • "the the typical uncertainty is on the order of a a few bits"
  • "Each additional consolidated input multiplies..."
  • "the number of inputs of other users to which that fingerprint has spread"
  • "and in subsequent ones she proceeds to blend into it"
  • "Note that being connected doesn't just mean a path can be traced along the graph, but that the flow of funds along that path appears plausible"

@nothingmuch

Copy link
Copy Markdown
Author

Wdym by payment transactions in this context? Subsequent transactions?

Any transaction which seems to move value from one apparent cluster to another

@nothingmuch

Copy link
Copy Markdown
Author

"the ordinary payments between counterparties elsewhere in the graph, which the CoinJoin itself does not encompass" sounds better?

not just ordinary ones any kind of observable flow between clusters

typos:

thanks, fixed all the nothingmuch-slop except

  • "Each additional consolidated input multiplies..."

which was unadulterated claude slop that snuck in (somehow i clearly edited it manually but did not make it make sense, anyway now it does)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment