Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Small Hamming Weight Denominations for CoinJoins

Standardizing on a small set of denominations has advantages, among other use cases, for privacy in CoinJoin transactions. Instead of creating a single output for an arbitrary amount, a wallet can approximate the amount from below by a sum of standard denominations, and create one output for each denomination in the sum. Each of these outputs is less linkable to the input cluster that funded it.

The fewer denominations there are to choose from, the more likely it is that more than one user chooses any given denomination, which improves privacy. The cost is that more outputs are needed to represent a given balance.

With too few denominations, the approximations also get worse. This notebook quantifies that trade-off. It takes denominations with Hamming weight 1, meaning values with a single nonzero digit in some base, such as powers of 2. It then measures the gaps between consecutive sums of up to such denominations. There are only such values up to sats.

Sub-transaction model and implications for privacy

In the sub-transaction model (1, 2), for every repeated value with multiplicity in the inputs or the outputs of a sub-transaction mapping as a whole, equivalent mappings can be obtained by permutation. Permuting coins of equal value does not change the sum of any sub-transaction, so it preserves the defining property of a sub-transaction: that its inputs minus its outputs sum to (approximately) zero. Permutations within a single sub-transaction do not change which sub-transaction any coin is assigned to, so they yield no new mappings.

In base 2, a value with multiplicity 2 within a single sub-transaction is exchangeable with the next denomination up from some other sub-transaction. Base 3 and 10 have somewhat more complex arithmetic relationships which imply yet more permutations of subsets of coins.

If an arbitrary output value (i.e. with large Hamming weight) can be constructed – or more generally than in Maurer et al, approximated – by summing together distinct denominations, and each of which has multiplicity in the outputs of the sub-transaction mapping as a whole, then the number of equivalent mappings is . This counts permutations over sets of subsets of outputs, specifically when a subset of size is exchanged with a subset of size .

Although this preserves the nominal sum of the sub-transaction, its weight, and therefore its implied fee rate, will differ. A high variance in the implied fee rates of sub-transactions is less likely, so unlike 1:1 exchanges, k:1 exchanges are not perfectly symmetric. Here a sub-transaction’s implied fee rate is the nominal sum of its inputs minus that of its outputs, divided by the weight of the sub-transaction. Users who attempt to recover every last sat are more susceptible to such analysis.

A partition of the inputs and outputs is only a valid sub-transaction mapping if each part’s outputs balance its inputs. When the coins form a dense subset sum instance, almost any group of inputs can be balanced by many groups of outputs, and vice versa. With multiplicities high enough that no single sub-transaction uses every copy of a denomination, the remaining coins stay dense after any one sub-transaction is set aside, so the same holds for the rest of the transaction. Every input and output therefore appears in many distinct sub-transactions across all non-derived mappings.

Radix CoinJoin

It follows that a logarithmic (in the range of values) number of small Hamming weight values with non-trivial multiplicity, taken from a geometric progression, can provide cover for any number of arbitrary valued inputs and outputs. Larger Hamming weight values with multiplicity 1 can of course contribute to subset sum density, but a geometric progression with high multiplicity is much more robust and trivial to analyze.

Ignoring any hypothetical UTXO sharing, the anonymity set size of a CoinJoin transaction is constrained by the number of inputs or the number of outputs (the smaller of the two). Even if the estimated number of distinct sub-transactions in which a coin appears is very large, this only means that the sub-transaction mappings will not be the limiting constraint on anonymity set size for this coin. Therefore even loose lower bounds on this value are sufficient in practice.

Mixed bases and preferred values

If using only a single base, when a small fee cost is subtracted from a single denomination this will result in a remaining balance with a large Hamming weight in that base. To get around this to some extent, values from multiple bases can be used. Additionally, not all Hamming weight 1 values are required, a preferred value series can approximate values well with a smaller number of denominations.

Currency conversions often result in excessively precise bitcoin amounts. In some base , a value will have expected weight . Values which have a small weight in one base usually have a large weight in another, in particular bases 2 and 3. The result of subtracting a value with weight 1 in base 3 from a value with a large weight in base 2 can often have a weight that is smaller by more than 1 (but it may also be higher).

Concretely, the 1-2-5 series in base 10 is useful for representing values with a decimal bias. Base 2 and 3 are suitable for decomposing arbitrary values with a high decimal Hamming weight. They are also complementary to each other: the 1-2 series in base 3 has a high Hamming weight in base 2 and powers of 2 have a high Hamming weight in base 3. These additional values also improve the overall density, reducing the sizes of combinations required for good approximations. Beyond this, additional denominations provide diminishing returns.

As we will see in detail, allowing more outputs improves the density of representable values. Coverage is denser in the interior of the range and becomes sparser towards the upper end, where combinations are limited by the largest available denomination. Raising this denomination limit can improve coverage in the upper region, but does not fill existing gaps between smaller values. With the parameters used here, some gaps still exceed the dust threshold even with six outputs.

This python implementation is optimized for simplicity, not performance or memory use, and the nature of this problem lends itself to precomputation. In an optimized implementation it’s possible to exhaustively enumerate all possibilities even as part of an online computation. This is worth noting because for privacy choosing uniformly at random between an unbiased selection of acceptable choices is better than always picking the best choice according to the cost function. A black box optimization approach may introduce such a bias depending on the cost function.

Parameters

The minimal feerate can be controlled in order to vary the dust threshold.

# bitcoin network parameters
max_sats = 2099999997690000
dust_feerate = 3.0
expected_feerate = 10.0
min_vbytes = 98 # 294 sats @ 3 sat/vb for p2wpkh per https://bitcoin.stackexchange.com/a/41082
dust = int(min_vbytes * dust_feerate)

We will generate denominations from the dust threshold to some maximum value. Setting a large max_denom smooths the gap distribution and emphasizes the gaps between the larger magnitude values, which tend to have smaller relative differences. max_combination_value cuts the sums off at a multiple of max_denom, because larger sums are less informative. A sum above times max_denom needs more than terms, since no term exceeds max_denom, so it effectively has fewer than free terms and the sumset is sparse there.

# denomination parameters
min_denom = dust
max_denom = int(1e8) # reducing this reveals some interesting structure in the plots below
max_combination_size = 6 # 7 is slow but doable, and consumes lots of ram for larger values of max_denom with this naive implementation. results are not very interesting, median gap size is 1 sat.
max_combination_value = max_denom * 2

The final parameters set a cutoff for the smallest gaps. The smallest possible gap is of size 1. Setting a higher cutoff omits negligible gaps (e.g. below dust) improving readability of the individual histograms with regard to larger gaps. Low values aid in comparing between sumset degrees.

# analysis parameters
min_diff = 2 # setting to dust hides small gaps, focusing on more significant ones
min_diff_ratio = 0 # 1e-4 is a basis point

Generate denominations

Denominations are based on preferred value series in base 2, 3, and 10.

# helper functions
def powers(b: int) -> set[int]:
  """
  Generates the powers of the given base up to the maximum denomination.
  Smaller powers are kept, since a multiple of one may still be large enough.
  """
  return { b**i for i in range(int(log(max_denom)/log(b))+1) }

def multiples(vs: set[int], cs: set[int]) -> set[int]:
  """
  Computes the products of pairs of values from two sets.
  """
  return { c*v for (c,v) in product(cs,vs) if c*v in range(min_denom, max_denom+1) }
# denominations of different bases
binary  = multiples(powers(2),  {1})
ternary = multiples(powers(3),  {1,2})
decimal = multiples(powers(10), {1,2,5})
# these additional Hamming weight 2 values plug the proportionally largest gaps
# between the denominations. this has some impact on the decomposition of smaller
# values (max_denom <= 1e6 or so) and so seem appropriate for a future with a
# lower dust feerate, but marginal benefit today

#binary = binary | multiples({3}, { 2**i for i in [ 10, 13, 15, 18 ]})

# the addition of 3 * 2^n arguably over-represents base 2 values
# https://en.wikipedia.org/wiki/Preferred_number#Computer_engineering

#binary  = multiples(powers(2), {1,3})

The combined set of denominations is just a union of these Hamming weight 1 values in these 3 useful bases.

# combined denomination set
combined = binary | ternary | decimal
set value binary (little endian) popcount weight (base 3) weight (base 10)
0 ternary 0.00000486 011001111 6 1 3
1 decimal 0.00000500 001011111 6 4 1
2 binary 0.00000512 0000000001 1 4 3
3 ternary 0.00000729 1001101101 6 1 3
4 decimal 0.00001000 0001011111 6 4 1
5 binary 0.00001024 00000000001 1 6 3
6 ternary 0.00001458 01001101101 6 1 4
7 decimal 0.00002000 00001011111 6 4 1
8 binary 0.00002048 000000000001 1 6 3
9 ternary 0.00002187 110100010001 5 1 4
10 binary 0.00004096 0000000000001 1 7 3
11 ternary 0.00004374 0110100010001 5 1 4
12 decimal 0.00005000 0001000111001 5 6 1
13 ternary 0.00006561 1000010110011 6 1 4
14 binary 0.00008192 00000000000001 1 5 4
15 decimal 0.00010000 00001000111001 5 7 1
16 ternary 0.00013122 01000010110011 6 1 5
17 binary 0.00016384 000000000000001 1 8 5
18 ternary 0.00019683 110001110011001 8 1 5
19 decimal 0.00020000 000001000111001 5 5 1
20 binary 0.00032768 0000000000000001 1 10 5
21 ternary 0.00039366 0110001110011001 8 1 5
22 decimal 0.00050000 0000101011000011 6 9 1
23 ternary 0.00059049 1001010101100111 9 1 4
24 binary 0.00065536 00000000000000001 1 7 5
25 decimal 0.00100000 00000101011000011 6 7 1
26 ternary 0.00118098 01001010101100111 9 1 5
27 binary 0.00131072 000000000000000001 1 9 5
28 ternary 0.00177147 110111111100110101 13 1 6
29 decimal 0.00200000 000000101011000011 6 7 1
30 binary 0.00262144 0000000000000000001 1 9 6
31 ternary 0.00354294 0110111111100110101 13 1 6
32 decimal 0.00500000 0000010010000101111 7 11 1
33 binary 0.00524288 00000000000000000001 1 9 6
34 ternary 0.00531441 10001111110110000001 10 1 6
35 decimal 0.01000000 00000010010000101111 7 9 1
36 binary 0.01048576 000000000000000000001 1 10 6
37 ternary 0.01062882 010001111110110000001 10 1 6
38 ternary 0.01594323 110010111100101000011 11 1 7
39 decimal 0.02000000 000000010010000101111 7 10 1
40 binary 0.02097152 0000000000000000000001 1 11 6
41 ternary 0.03188646 0110010111100101000011 11 1 7
42 binary 0.04194304 00000000000000000000001 1 11 6
43 ternary 0.04782969 10011110110111110001001 14 1 7
44 decimal 0.05000000 00000010110100100011001 8 7 1
45 binary 0.08388608 000000000000000000000001 1 8 6
46 ternary 0.09565938 010011110110111110001001 14 1 7
47 decimal 0.10000000 000000010110100100011001 8 9 1
48 ternary 0.14348907 110101100100111101011011 15 1 7
49 binary 0.16777216 0000000000000000000000001 1 9 8
50 decimal 0.20000000 0000000010110100100011001 8 12 1
51 ternary 0.28697814 0110101100100111101011011 15 1 8
52 binary 0.33554432 00000000000000000000000001 1 7 8
53 ternary 0.43046721 10000010111010110000100101 11 1 7
54 decimal 0.50000000 00000001000011110101111101 12 11 1
55 binary 0.67108864 000000000000000000000000001 1 12 7
56 ternary 0.86093442 010000010111010110000100101 11 1 7
57 decimal 1.00000000 000000001000011110101111101 12 13 1

Generate sumsets

{0} is unioned with the denominations in order to obtain the sumsets from combinations of size as opposed to .

# compute sumsets naively

sumsets = []
for k in range(1, max_combination_size+1):
  subsets = combinations_with_replacement({0} | combined, k)
  sumset = { s for s in map(sum, subsets) if s in range(1,max_combination_value+1) }
  sumsets.append(np.array(sorted(sumset)))
# compute gaps efficiently
gaps = []
for s in sumsets:
  diffs = np.diff(s)
  gaps.append(diffs[diffs >= min_diff])

For the relative gap size, omit gaps below the dust threshold. These gaps are negligible and unrecoverable (since an additional output has a cost), and are therefore noise. To improve clarity of the plots they are filtered out of the relative differences.

# compute normalized gaps efficiently

relative_gaps = []
for s in sumsets:
    diffs = np.diff(s)
    relative_diffs = diffs / s[1:]
    mask = (diffs >= dust) & (relative_diffs >= min_diff_ratio)
    relative_gaps.append(relative_diffs[mask])

Plots

Gaps

Count and sizes

Here we compare the gaps (ignoring gaps of size 1) of the sumsets of different degrees. As larger combinations are permitted the number of distinct sums grows and with them the number of gaps between these sums. Because these values are all in a fixed range, the sizes of the more numerous gaps of sumsets of higher degrees are of course smaller.

Note that for the scatterplots are a little misleading. For there aren’t that many distinct values, so even though these values are smoothly distributed the quantile values aren’t. For several quantiles of the sumset are entirely gaps of size 1. The plot omits those gaps, so its lowest several quantiles are 2 and the next several are 3. The spacings of these in the scatter plot are a visual artifact arising from plotting integer values.

Gap sizes distribution

The next two plots show the gaps in more detail, both their sizes and their sizes in relation to the next larger value. When decomposing into low Hamming weight values, the total balance being decomposed may fall in one of these gaps. The distribution of their sizes bounds the loss from the decomposition. Here too small integer values create visual artifacts, the empty bins for are empty because the populated bins interspersing them correspond to successive integer values.

Relative gap size

The next plot displays gaps larger than the dust threshold as a fraction of the larger value. No quantization artifacts arise in this plot, but there is a cutoff imposed by dividing by values no larger than the maximum combination value.

Sums and combinations

Density of distinct sums

The next plot shows the density of distinct sums in the integers. The previous plots show the gap sizes over the entire range, but not all balances decompose as readily, and towards the end of the range the density tapers off (see max_combination_value parameter). The initial ramp up is determined by the size of the smallest denomination (see also dust_feerate parameter).

This plot gives a clearer indication of the expected loss as a function of the balance being decomposed into a sum of standard values. The distinct constructible sums are assigned into bins (ranges of natural numbers). To plot each bin, this number of distinct values is divided by the size of the bin (number of distinct values, or difference between smallest and largest values).

A density of 1 implies every possible integer value in the bin can be constructed exactly, and a density means every value can be approximated from below with error smaller than the dust threshold.

More intuitively, anything above the dashed line means values can be well approximated often enough that the loss is on average negligible (the cost of an additional output will be higher even at the minimum relay fee). The difference in costs will be linear in , smaller combinations are more efficient representations of a given value but concrete efficiency depends on the feerate, and at higher feerates.

Distinct combinations vs. distinct sums

The following plot displays the possible combinations (as opposed to the distinct sums of the previous plot), which gives an indication for how many ways of constructing a possible sum there are as the combination size increases.