The 445 BTC gridchain case

For those time-constrained or non-technical, it may make sense to read only the Summary section of this article. It goes without saying that the details do matter, and reading the other sections will give you a much better overall picture.


Background - what is the "gridchain case"?

Toxic change and peeling chains

Change outputs in a Joinmarket context

The toxic recall attack

The size factor

Joinmarket sudoku

Reminder on the maker-taker tradeoff

Address reuse

Summary; lessons learned; advice to users

Already implemented improvements

Still needed improvements

Recommendations for users

Background - what is the "gridchain case"?

This is a reflection on a case of reported theft as outlined here on reddit in early 2017 by user 'gridchain'.

What I won't do here is discuss the practical details of the case; things like, whether it was a hack or an inside job, nor anything like network level metadata, all of which is extremely important in an actual criminal investigation. But here I'm only focusing on the role played by Joinmarket specifically and blockchain level activity of the coins, generally.

The reason for this blog post was this recent report by OXT Research - specifically by analyst ErgoBTC (they require an email for signup to read the full report, otherwise you only see the summary).

A short note of thanks here to ErgoBTC and LaurentMT and others involved, since this kind of detailed analysis is badly needed, I hope will we see more, specifically in public, over time (we cannot hope for such from the deeply unethical blockchain analysis companies).

I'm [not]{style="text-decoration: underline;"} going to assume here that you've read that report in full, but I am going to be referring to its main set of conclusions, and analyzing them. Obviously if you want to properly assess my statements, it's rather difficult - you'd need full knowledge of Joinmarket's operation and full details of the OXT Research analysis - and even then, like me, you will still have some significant uncertainties.

So the case starts with the claimed theft in 2 parts: 45 BTC in this txn (note I will use for my tx links because I find their presentation easiest for single txs specifically; note that 's research tool is of course a vastly superior way to see a large network of txs, which plays a crucial role in this analysis), and a consolidation of 400BTC in this other txn .

We'll assume that both of these utxos are under the control of a single actor/thief, henceforth just A.

Setting aside the (in some ways remarkable) timing - that A did not move the coins for about 2 years - let's outline roughly what happened, and what the report tells us:

  • The 400BTC went into joinmarket as a maker, and did a bunch (11 to be precise) of transactions that effectively "peeled down" (more on this later) that 400 to perhaps 335 BTC (with the difference going into coinjoins).
  • A then switched to a taker role for a while, focusing on higher denominations, ranging from \~ 6BTC to as high as \~58BTC. Many of these coinjoins had very low counterparty numbers (say 3-5 being typical).
  • At some point some maker activity is seen again in this same "peeling chain"; the report terms this phase as "alternating", but it's hard to say for sure whether some particular script is running, whether A is just randomly switching roles, or what.

Be aware that this simplified narrative suggests to the careless reader that one can easily trace all the coins through all the coinjoins, which of course is not true at all - each subsequent transaction moves some portion into a "mixed state", but (a) we'll see later that just "moved into mixed state" is not the end of the story for some of those coins and (b) while this narrative is misleading for Joinmarket in general, it is not as misleading in this particular case.

The distinction between the "second" and "third" phase as listed in those bullet points is pretty much arbitrary, but what is not in doubt as important is: that second phase marks a clear jump in coinjoin amount average size (this could be read as impatience on A's part - but that's just speculation), and this resulted in small anonymity sets in some txs - 4 and 3 in two txs, in particular. Let's continue:

  • Within the second regime, the OXT analysis narrows in on those small anon set, large denomination txs - can they figure out which equal sized output belongs to A here? The "toxic replay attack" (explained below) allows them to identify one coinjoin output unambiguously  - but that goes into another coinjoin. But in a second case it allows them to reduce the anonymity set (of the equal sized coinjoin outputs) to 2, and they trace forwards both of those outputs.
  • One of those 2 coinjoin outputs (this txn , output index 2) pays, after several hops, into a Poloniex deposit address in this txn ). Although this is several hops, and although it does not deposit all of that \~58BTC into Poloniex (only about half of it), nevertheless this can be (and is) treated as a significant lead.
  • So the next step was to trace back from that specific Poloniex deposit address, which it turned out had a bunch of activity on it. See 16vBEuZD54NzqnnSStPYxFF2aktGhhuaf1 . Indeed several other deposits to that single address are connected to the same Joinmarket cluster, and specifically connected to those smaller-anon set taker-side coinjoins. In total around 270BTC is eventually linked from A's joinmarket coinjoins to that deposit address. Even though some of those connections are ambiguous, due to address reuse the evidence of co-ownership appears very strong.
  • Some further evidence is provided (though I am still fuzzy on the details, largely just because of the time needed to go through it all) linking more of the coins to final destinations, including some from the 45BTC original chunk. The claim is that 380BTC is linked at final destinations to the original 445BTC set. In the remainder I'll focus on what is already seen with this 270BTC set and only peripherally mention the rest - there is already a lot to chew on!

Toxic change and peeling chains

The general idea of a "peeling chain" on the Bitcoin blockchain isn't too hard to understand. Given 100 BTC in a single utxo, if I have to make a monthly payment of 1 BTC and never use my wallet otherwise, then clearly the tx sequence is (using (input1, input2..):(output1, output2..)) as a rudimentary format): ((100):(1,99), (99):(1, 98), (98:(1, 97)...). Ignoring fees of course. What matters here is that I just always have a single utxo and that on the blockchain my utxos may be linked as (100-99-88-97...) based on a change heuristic such as "round amount for payment". To whatever extent change heuristics work, then to that extent ownership can be traced through simple payments (especially and mostly if transactions have exactly two outputs, so that the very idea of change, let alone a change heuristic, applies straightforwardly).

peeling chain simple
example{width="418" height="296"}

In peeling chains, sometimes, the primary heuristic is the size of the output. If you start with 1000 btc and you peel 0.1 btc hundreds of times, it's obvious from the "size pattern" what the change is (and indeed it's this case that gives rise to the name peel chain because "peel" refers to taking off a small part of something, usually its surface). The above diagram is more similar (but not the same, exactly) as the initial flow in the gridchain case, with one very large utxo gradually getting peeled off.

In some cases timing may factor in; sometimes hackers will do hundreds of such peels off a main originating utxo in a short time.

You can think of a peeling chain as the lowest effort ownership obfuscation out there. Notice how literally any, even the simplest, Bitcoin wallet, has to offer the feature required to carry this out - just make a vanilla payment, for which there is (almost always, but not always) a change output, back to your wallet.

So in Bitcoin's history, this technique has very often been seen used - by hackers/thieves moving coins "away" from the original site of the theft (I remember the case of Sheep Market for example). Each "peel" raises additional uncertainty; the non-change output is going somewhere, but who owns that? But the change outputs represent a link allowing someone, in theory, to keep tracing the activity of the original actor. Notice here how we talk about one branch (our ((100):(1,99), (99):(1, 98), (98:(1, 97)...) example illustrates it); but one could keep tracing the payment outputs (the '1's in that flow) and see if they themselves form other peel chains, leading to a tree.

We mentioned a 'change heuristic' element to this - which is the "main branch" if we're not sure which output is the change?

Change outputs in a Joinmarket context

A reader should from this point probably be familiar with the basics of Joinmarket's design. Apart from the README and usage guide of the main Joinmarket code repo, the diagrams showing the main Joinmarket transaction types here may be useful as a refresher or a reference point for the following.

We have: \(N\) equal outputs and \(N\) or \(N-1\) non-equal change outputs, where \(N-1\) happens when the taker does a "sweep", emptying the mixdepth (= account; joinmarket wallets have 5 accounts by default) without a change output. [This last feature is specific to Joinmarket, and specific to the taker role: there's no other coinjoin out there that provides the facility to sweep an arbitrary amount of coins out to an equal-sized output, with no change.]{style="text-decoration: underline;"} (I am emphasizing this not for marketing, but because it's crucial to this topic, and not widely understood I think).

As an example of why it's important, here is one line from the OXT Research article:

Fees taken directly in a mix transaction result in deterministic links ("unmixed change").

This is false as an absolute statement; fees can be paid by a taker, inside the transaction, with no unmixed change for the taker (this is the Joinmarket 'sweep'). Deterministic links between inputs and change outputs do result from change, and fees do create an additional flag that can help make those linkages, in cases where there would be more ambiguity. But a zero fee coinjoin with change outputs still has deterministic links, usually.

Why does the OXT Research article heavily focus on toxic unmixed change as a concept and as a key weakness of such protocols as Joinmarket, and why do I disagree?

As we discussed peeling chains offer a low quality of obfuscation, and to unpack that: the problem is that if you have any relatively viable change heuristic (it doesn't have to be large amounts as discussed), it can let you keep knowledge of ownership of a whole chain of transactions. That basically gives the blockchain analyst (we'll call B) a very large attack surface. He can look at all the information flowing out of, or associated with, a whole chain of transactions. Any later recombination of outputs from that "large attack surface" is either a coinjoin or a "smoking gun" that different outward paths were actually under the control of one owner (this comes back to that central heuristic - common input ownership, and all the nuance around that).

In Joinmarket or any other coinjoin protocol that does allow change outputs, "change heuristic" doesn't really apply, it kind of morphs into something else: it's very obvious which outputs are change, but it is only in some cases easy to disentangle which change outputs are associated to which inputs, and that's actually what you need to know if you want to trace via the change (as per "peeling chains" description above). In high anonymity sets, it starts to get difficult to do that disentangling, but more on that ("sudoku") later.

The analysis done in the OXT Research report smartly combines a long peeling chain with other specific weaknesses in the way A acted, which we will discuss in the next section.. So all this is very valid in my view.

[But I think going from the above to the conclusion "coinjoins which have unmixed change are fundamentally inferior and not viable, compared to coinjoins without unmixed change" is just flat out wrong]{style="text-decoration: underline;"}. Consider yourself in the position of A. You have let's say 400BTC in a single utxo. If you run a coinjoin protocol that insists on no change always, and without a market mechanism, you are forced to use a fixed denomination, say 0.1 BTC (an example that seems common), now across thousands of transactions. In order to create these fixed denomination utxos you are faced with the same problem of trying to avoid a trivial peeling chain. By insisting on no deterministic links within the coinjoin, you simply move the problem to an earlier step, you do not remove it.

Fixed denomination does not solve the problem of having an unusually large amount to mix compared to your peers.

Having said that, fixed denomination with no change at all, does create other advantages - I certainly don't mean to disparage that model! Without going into detail here, consider that a large set or network of all-equal-in all-equal-out coinjoins can create similar effects to a single, much larger, coinjoin (but this is a topic for another article).

The toxic recall attack

Earlier we explained that one of the steps of the OXT Research analysis was to identify a low liquidity regime where A was acting as taker, and we mentioned the "toxic recall attack" was used to reduce the anonymity sets of the coinjoin outputs, during this, to a level low enough that simple enumeration could find good candidates for final destinations of those coins.

Embedded in this was a crucial piece of reasoning, and I think this was a both excellent, and very important idea:

  • Joinmarket does not allow co-spending of utxos from different accounts
  • That means that if a coinjoin output X is spent along with a utxo from the "peeling chain" (i.e. they are both inputs to the same tx), then X is not owned by A (assuming correct identification of A's peeling chain)
  • Every time such an event occurs, that X can be crossed off the list of coinjoin outputs that A might own, thus reducing the anonymity set of that earlier coinjoin by 1.

The reasoning is not perfectly watertight:

First, as the report observes: the first assumption behind it is "A user can only run one mixing client at a time." This is clearly not literally true, but like many things here, a good-enough guess is fine, if it eventually leads to outcomes that further strengthen the case. And that is definitely true here: while a smart operator probably would be running more than one instance of Joinmarket code, it is not default behaviour and requires both a little coding and some careful thought. Most people would not do this.

(Second, nothing stops a user from making a coinjoin to an address in the same mixdepth (at least in the current software). It's just that (a) that is heavily discouraged and (b) it's not easy to see a good reason why someone would try to do that. Still it is possible as a mistake. But I don't think this is a reason to doubt the effectiveness of the "toxic recall attack", just, it should be noted.)

So overall the bolded sentence is the most interesting - Joinmarket's intention is to prevent co-spending outputs which would ruin the effect of any single coinjoin - i.e. it tries (caveat: above parenthetical) to prevent you using both a coinjoin output and the change output (or any other utxo in the same account as the change output and the original inputs) together. And this small element of 'rigidity' in how coins are selected for spending is actually another 'bit' of information that B can use to make deductions, at least some of the time.

The following diagram tries to illustrate how these conditions lead to the possibility of the attack, to reduce the anonymity set of coinjoin outputs:

Toxic recall attack
illustration{width="692" height="490"}

So in summary we see 4 really important factors leading to the attack's viability:

  1. Joinmarket's strict account separation
  2. Linkability via change - as we'll describe in the next section "Joinmarket sudoku", this is usually but not always possible, so while (1) was 99% valid this is more like 75% valid (entirely vague figures of course).
  3. Reusing the same peers in different coinjoin transactions
  4. Low number of peers

Of course, 3 and 4 are closely tied together; reuse of peers happened a lot precisely because there were so few peers available for large coinjoin sizes (to remind you, it was between 6 and 58 BTC, and the average was around 27, and there are/were few Joinmarket peers actually offering above say 10BTC).

The size factor

This is a thread that's run through the above, but let's be clear about it: in practice, typical Joinmarket coinjoins run from 0.1 to 10 BTC, which is unsurprising. There are a fair number of much smaller transactions, many just functioning as tests, while really small amounts are not very viable due to the fees paid by the taker to the bitcoin network. Larger than 10 BTC are certainly seen, including up to 50 BTC and even beyond, but they appear to be quite rare.

The actions of A in this regard were clearly suboptimal. They started by taking 4 x 100 BTC outputs and consolidating them into 1 output of 400 BTC. This was not helpful, if anything the opposite should have been done.

Second, as a consequence, they placed the entirety of this (I'm ignoring the 45 BTC output for now as it's not that crucial) in one mixdepth. For smaller amounts where a user is just casually offering coins for joining, one output is fine, and will rapidly be split up anyway, but here this very large size [led to most of the large-ish joining events forming part of one long peeling chain.]{style="text-decoration: underline;"} This part probably isn't clear so let me illustrate. A yield generator/maker usually splits up its coins into random chunks pretty quickly, and while as a maker they do not get the crucial "sweep, no change" type of transaction mentioned above, they nevertheless do get fragmentation:

Initial deposit --> After 1 tx --> After 2 txs --> After many txs

0: 1BTC         --> 0.800 BTC  --> 0.800 BTC   --> 0.236 BTC

1: 0 BTC        --> 0.205 BTC  --> 0.110 BTC   --> 0.001 BTC

2: 0 BTC        --> 0.000 BTC  --> 0.100 BTC   --> 0.555 BTC

3: 0 BTC        --> 0.000 BTC  --> 0.000 BTC   --> 0.129 BTC

4: 0 BTC        --> 0.000 BTC  --> 0.000 BTC   --> 0.107 BTC

(Final total is a bit more than 1BTC due to fees; the reason it gets jumbled, with no ordering, is: each tx moves coinjoin output to next mixdepth, mod 5 (ie it wraps), but when a new tx request comes in it might be for any arbitrary size, so the mixdepth used as source of coins for that next transaction, could be any of them. This is illustrated in the 'after 2 txs' case: the second mixdepth was chosen as input to the second tx, not the first mixdepth).

This dynamic does not remove the "peeling chain" or "toxic change" dynamic emphasized in OXT Research's report - because every tx done by the maker still has its change, [precisely because as maker you don't have the privilege of choosing the amount]{style="text-decoration: underline;"}.

But it does result in more so to speak "parallelisation" of the mixing activity, instead of the largest chunk all being in one long chain.

A question remains, if we imagine that we use much smaller amounts - can the analyst always follow the "peeling chain of each mixdepth" (to coin a phrase which at this point hopefully makes sense)?

I think actually the answer is more 'no' than you might at first think. The next section will illustrate.

Joinmarket sudoku.

This concept including its origination is covered in some detail in my earlier article here. Essentially we are talking about making unambiguous linkages between change outputs and the corresponding inputs in any given Joinmarket coinjoin. I reproduce one transaction diagram from that article here to help the reader keep the right idea in mind:

canonical{width="550" height="389"}

So to effect this "sudoku" or disentangling, let's suppose you don't have any sophistication. You're just going to iterate over every possible subset of the inputs (they're randomly ordered, of course) and see if it matches any particular change output (you assume that there is exactly one change output per participant). In case it wasn't obvious, "matches" here means "that change output, plus the coinjoin size (3 btc in the diagram above), equals the sum of the subset of inputs".

Now none of them will actually match because there are fees of two types being paid out of (and into) the change - the bitcoin network fees and the coinjoin fees (which add to most and subtract from one, at least usually). So since you don't know the exact values of those fees, only a general range, you have to include a "tolerance" parameter, which really complicates the issue.

This gist is a quick and dirty (in the sense it's barely a 'program' since i just hardcoded the values of the transaction) example of doing such a Joinmarket sudoku for one of the transactions in the OXT Research analysis of flows for this case. The pythonistas out there might find of interest particularly this code snippet for finding the "power set" (the set of all subsets):

def power_set(l):\ iil = range(len(l))\ return list(chain.from_iterable(combinations(iil, r) for r in range(len(iil)+1)))

As per a very beautiful piece of mathematical reasoning, the power set of a set of size \(N\) is \(2\^{N}\) (every member of set is either in, or not in, each subset - think about it!). So this matters because it illustrates, crudely, how we have here an exponential blowup.

That particular transaction had 24 inputs, so the power set's cardinality would be \(2\^{24}\) - but the beginning of the analysis is to take a subset, of size 4, you already conclude to be linked, thus reducing the size of the search space by a factor of 16. Now, there's a lot more to it, but, here's what's interesting: depending on the tolerance you choose, you will often find there are multiple sudoku solutions if the size of the set of inputs is reasonably large (let's say 20 and up, but it isn't possible to fix a specific number of course). In the first couple of attempts of finding the solution for that transaction, I found between 3 and 7 different possible ways the inputs and outputs could connect; some of them involve the pre-grouped 4 inputs acting as taker (i.e. paying fees) and some involve them acting as maker.

Now, if this ambiguity isn't enough, there's another significant source of ambiguity in these sudokus: previous equal-sized coinjoin outputs. For example take this txn:

tx{width="849" height="617"}

There are 21 inputs, which is already in the "problematic" zone for sudoku-ing, as discussed, in that it will tend to lead to multiple possible solutions, with a reasonable tolerance parameter. But in this case a full sudoku is fundamentally impossible: notice that inputs index 7 and 21 (counting from 0) both have amount 6.1212 . This means that any subset that includes the first is identical to a subset that includes the second. Those two outputs are, unsurprisingly, from the same previous Joinmarket coinjoin (they don't have to be, though).

In any long "peeling chain" these ambiguities will degrade, perhaps destroy, the signal over time - unless there is some very strong watermark effect - such as huge size, which is precisely what we see with A.

To summarize, we these key points about the Sudoku concept for identifying chains of ownership:

  • As long as you don't sweep, a Joinmarket account, thus not emptied, will keep creating this chain of ownership via change - though the size of that linked amount dwindles over time.
  • Thus makers (who cannot sweep) have no guarantee of not having that specific ownership trace persist, for each of their 5 accounts (but not across them - the 5 accounts will not be connected on chain, at least not in a trivial way).
  • If you use a very large size then this acts as a strong enough watermark that such tracing is pretty much guaranteed to work (i.e the Sudoku works much more reliably if you put in a 400BTC utxo and everyone else in the coinjoin only uses 10BTC at max).
  • Otherwise, and in general, such tracing is a bit unreliable, and over a long series of transactions it becomes very unreliable (but again - this is no kind of privacy guarantee! - we just observe that there will be increasing uncertainty over a long chain, including really fundamental ambiguities like the transaction above).
  • Whenever you do sweep, you create what I called in the previous article a "completed mixdepth closure"; there is no change for you as taker, and so an end to that "chain". This only exists for takers. (you can of course sweep without coinjoin at all, also).

Reminder on the maker-taker tradeoff

This illustrates another aspect of the more general phenomenon - Joinmarket almost by definition exists to serve takers. They pay for these advantages:

  • As coordinator, they do not reveal linkages to their counterparties. Makers must accept that the taker in each individual coinjoin does know their linkages (the maker's), even if they're OK with that over a long period because there are many disparate takers; that's a weakness.
  • They choose the time when the coinjoin happens (within a minute or so, it's done, if all goes well)
  • They choose the amount of the coinjoin, so can have a payment as a coinjoin outpoint.
  • Corollary of the above: they can control the size of their change, in particular, reducing it to zero via a "sweep"
  • Since they run only when they want to coinjoin, they have a smaller time footprint for attackers (makers have an "always on hot wallet" which responds to requests rather than initiates them , so it's more like a server than a client, which is by definition difficult to keep properly secure).

These 4+ advantages are what the Taker pays for, and it's interesting that in practice that the coinjoin fee has fallen to near-zero except for larger sizes .I will point to my earlier thoughts on low fees here to avoid further sidetrack.

Therefore the cool sounding idea "oh I have this bunch of bitcoin sitting around, I'll just passively mix it for a while and actually get paid to do it!" (I have noticed people mostly get interested in Joinmarket from this perspective) is more limited than it seems.

Address reuse

This will probably be the shortest section because it's so obvious.

The fact that 270BTC of the 445 BTC going "into" Joinmarket ended up at 16vBEuZD54NzqnnSStPYxFF2aktGhhuaf1is kind of a big facepalm moment; I don't think anyone reading this blog would have trouble understanding that.

I don't dismiss or ignore that such things happen for a reason, and that reason is mainly actions of centralized exchanges to deliberately reduce the privacy of their customers ("KYC/AML"). Sometimes, of course, sheer incompetence is involved. But it's the exception rather than the rule, since even the most basic consumer wallets do not generally reuse addresses nowadays. I'll consider these real world factors out-of-scope of this article, although they will matter in your practical real life decisions about keeping your privacy (consider not using such exchanges).

What has to be said though: 270 does not equal 445 (or 400); it is not impossible to imagine that such a set of deposits to one address may not be traced/connected to the original deposit of 400 (+) into Joinmarket (although it would really help if that total wasn't so very large that there are only a few Joinmarket participants in that range anyway). And indeed, my own examination of the evidence tells me that the connections of each individual final deposit to `16vBEuZD54NzqnnSStPYxFF2aktGhhuaf1```back to that original 445 is not unambiguous. The problem is of course the compounding effect of evidence, as we will discuss in the next, final section.

Summary; lessons learned; advice to users

So we've looked into details, can we summarize what went wrong for A? Albeit we don't actually know with certainty how much of the attributions in the OXT Research are correct, they appear to be broadly correct.

  1. 400 BTC is a very large amount to move through a system of perhaps at best a couple hundred users (100 makers on the offer at once is typical), most of whom are not operating with more than 10 BTC.
  2. One large chunk of 400 is therefore a way worse idea than say 10 chunks of 40 across 10 Joinmarket wallets (just common sense really, although starting with 400 is not in itself a disaster, it just makes it harder, and slower). This would have been more hassle, and more fees, but would have helped an awful lot.
  3. Running passively as a maker proved too slow for A (this is an assumption that the report makes and that I agree with, but not of course a 'fact'). This is Joinmarket's failing if anything; there are just not enough people using it, which relates to the next point:
  4. When switching to a taker mode (which in itself was a very good idea), A decided to start doing much larger transaction sizes, but found themselves unable to get more than a few counterparties in some cases. This should have been a sign that the effect they were looking for might not be strong enough, but it's very understandable that they didn't grok the next point:
  5. The "toxic replay attack" very heavily compounds the low anonymity set problem mentioned above - reuse of the same counterparties in successive transactions reduced the anonymity set from "bad" to "disastrously low" (even down to 1 in one case).
  6. Even with the above failings, all needn't really be lost; repeated rounds are used and the '1' anonymity set mentioned output was sent to another coinjoin anyway. The first chunk of coins identified to be sent to Poloniex address (first to be identified, not first in time) was in an amount of about 28 BTC via several hops, then part of the 76 BTC in this txn, and even the first hop only had a 50% likelihood assigned. So it's a combination of (a) the address being marked as in the POLONIEX cluster, the size of the deposit and then the reuse allowing tracing back to other transactions, that caused a "high-likelihood assignment of ownership", which leads into ...
  7. Address reuse as discussed in the previous section is the biggest failing here. If all the deposits here were to different exchange addresses, these heuristics would not have led to any clear outcomes. A few guesses here and there would exist, but they would remain guesses, with other possibilities also being reasonable.
  8. Circling back to the beginning, notice how making educated guesses about deposits on exchanges a few hops away from Joinmarket might already be enough to get some decent guesses at ownership, if the sizes are large enough compared to the rest of the Joinmarket usage.

So overall the post mortem is: a combination of at least three different things leads to a bad outcome for A : large (much bigger than typical JM volume) size not split up, heavy address reuse (on a centralized exchange) and a small anonymity set portion of the sequence.

This issue of "combination of factors" leading to a much worse than expected privacy loss is explained well on the bitcoin wiki Privacy page here.

Already implemented improvements

When running as a taker and using the so-called tumbler algorithm users should note that in 2019 a fairly meaningful change to the algorithm was implemented - one part was to start each run with a sweep transaction out of each mixdepth containing coins as the first step (with longer randomized waits). This makes a peeling chain direct from a deposit not possible (you can always try to guess which coinjoin output to hop to next of course, with the concomitant difficulties). Additionally average requested anonymity sets are increased, which, as an important byproduct tends to create larger input sets which are harder to sudoku (and more likely to have substantial ambiguity). There are several other minor changes like rounding amounts, see Chris Belcher's document on it for more details.

Still needed improvements

Clearly the toxic recall attack concept matters - it is going to matter more, statistically, as the anonymity set (i.e. the number of coinjoin counterparties) is reduced, but it matters per se in any context - reusing the same counterparties in a sequence of coinjoins from the same mixdepth closure reduces the anonymity set. Notice there are a couple of ways that situation could be remediated:

  1. Reduce the number of coinjoin transactions within the same mixdepth closure - but this is not clear. If I do 1 coinjoin transaction with 10 counterparties and it's a sweep, closing the mixdepth closure, is that better than doing 2 coinjoin transactions from it, each of which has 6 counterparties, if there is a 10% chance of randomly choosing the same counterparty and thus reducing the anonymity set of the second coinjoin by 1? That is pretty profoundly unclear and seems to just "depend". 1 transaction with 12 counterparties is clearly better, but very large sets like that are very difficult to achieve in Joinmarket today (particularly if your coinjoin amount is large).
  2. Actively try to prevent reusing the same counterparty for multiple transactions in the same mixdepth closure (obviously this is for takers; makers are not choosing, they are offering). Identification of bots is problematic, so probably the best way to do this is simply for a taker to keep track of its earlier txs (especially within a tumbler run, say) and decide to not include makers when they provide utxos that are recognized as in that set. This is still a bit tricky in practice; makers don't want their utxos queried all the time, but takers for optimal outcomes would like full transparent vision into those utxo sets - see earlier discussion of PoDLE and here on this blog for the tricky points around this.

(2) is an example of ideas that were discussed by Joinmarket developers years ago, but never really went anywhere. Takers probably should expand the query power given them by the PoDLE tokens to have a larger set of options to choose from, to gauge the "quality" of what their counterparties propose as join inputs, but it's a delicate balancing act, as mentioned.

Recommendations for users

For the final section, some practical advice. Joinmarket can be a powerful tool - but it's unfortunately not very easy to understand what you should do, precisely because there is a lot of flexibility.

  1. The taker role, and in particular the tumbler role, are designed to be used to actively improve your privacy. We explain above that it gives certain advantages over the maker role. So: use it! - with at least the default settings for counterparty numbers and transaction numbers, and long waits - in fact, increase these factors above the defaults. Note that tumbles can be safely restarted, so do it for a day, shut it down and then restart it a few days later - that's fine. See the docs for more on that. Be sensitive to bitcoin network fees - these transactions are very large so they'll be more palatable at times when the network is clearing 1-5 sats/byte. However ...
  2. ... mixing roles definitely has advantages. The more people mix roles the more unsafe it is to make deductions about which coinjoin output belonged to the taker, after it gets spent (consider what you can deduce about a coinjoin output which is then spent via an ordinary wallet, say to a t-shirt merchant).
  3. The maker role isn't useless for privacy, it's rather best to think of it as (a) limited and (b) taking a long time to have an effect. It's most suitable if your threat model is "I don't want a clear history of my coins over the long term". It also costs nothing monetarily and brings in some very small income if your size is large (if small, it's likely not worth mentioning) - but in that case, take your security seriously.
  4. Consider sizing when acting as a taker. We as a project should perhaps create more transparency around this, but you can gauge from your success in arranging big size coinjoins: if you can't easily find 6+ counterparties to do a coinjoin at a particular size, it may not be a good idea to rely on the outcomes, as you may be mixing in too small of a crowd (whether that's at 10 BTC or 20 BTC or 50+ BTC just depends on market condition).
  5. Make good use of (a) the accounts (mixdepths) feature, (b) the coin freeze feature and (c) the sweep feature (taker only). These three things allow you to better isolate coins going to different destinations - your cold wallet, your mobile spending wallet, an exchange etc etc. Accounts let you have the assurance that coins in one aren't linked with coins in another; you can't accidentally co-spend them. The freeze feature (see the "Coins" tab on Qt) lets you spend individual utxos, where that's important to you for some reason, without connection to others. And the sweep feature lets you make a coinjoin without any change, breaking a link to future transactions.

We soon (in 0.7.0; the code is basically already done) hope to have more helpful features, in particular Payjoin as defined in BIP 78, along with very basic PSBT support.