Why use fixed denominations in a digital currency when your payments could use arbitrary amounts? After all, we’re talking numbers here, not bits of metal and scraps of paper. The answer is privacy. If you transact 4589234127 SNT, that figure is highly likely to be unique and therefore traceable.
However, even if we accept denominations as a way of pooling anonymity, there is also the question of efficiency. A small number of possible denominations (say 1 and 10) would make transactions extremely hard to link and would maximize fungibility, but at the cost of making unacceptable extra work for the network.
@mav’s study of bitcoin transactions shows that a granularity of 8 significant decimal places is commonplace, presumably because Btc payments are calculated based on fiat currencies and subject to whatever the rate of exchange happens to be. This will be the same for SNT, so we cannot expect perfectly even amounts.
The team has come up with a happy medium approach which allows for massive divisibility, accords with realworld transactions, and should not place undue strain on the mints.
That’s a summary. For those wanting to dig deeper @danda lays out our current thinking in all its mathematical splendour below.
General progress
@chriso has been working hard to get the api and cli merged. Codewise it’s all in, but the merging of these repos has meant we’re having to adapt our continuous integration and version bumping flows. Acceptable solutions have been found, which should mean that every safe_network
release will have all crates attached to it, but with their own distinct versions (our old flow would have meant only one version, and a lot of likely confusion there). So now he’s working through implementing these changes.
@Chris.Connelly has continued poking into qp2p and confirmed that sending many messages between two nodes is much faster over a single connection than over many connections. This follows from QUIC’s use of TLS, meaning each connection must perform a handshake protocol. Thankfully QUIC also supports many logically independent streams over a single connection, so there’s no real downside here – less connections, less problems. This will inform the last stages of removing connection pooling from qp2p, where we’ll have to take some care to ensure we’re still being efficient with connections.
@Lionel.Faber has been working on improving the process for deploying testnets to Digital Ocean, with approval now required for every step in deploying and taking down the testnet, allowing us to restart each step or use the testnet externally instead of it being automatically destroyed.
We’ve also been refining node/client startup. With @yogesh implementing writing the network prefixmap to disk, such that it can be shared, and so clients and nodes can start off with a modicum of network knowledge. And we’ve been adding and improving some local log inspection to be able to more easily identify and track down issues.
Good news from the DBC labs. Spentproofs are now working in the test environment and should be ready to merge soon.
Denominations Background
A DBC Mint utilising blind signatures is unable to see the contents of an output DBC before signing it, yet it must verify that sum(inputs) == sum(outputs). The mint cannot trust the reissuing party to specify an amount with the output because it could be a lie. Commitment schemes are possible but the resulting reissues become linkable, defeating the purpose of the blind signatures.
A solution then, is for the mint to treat every DBC signed with a given key as being equal to every other DBC signed with the same key. Extending this, we can define a set of fixed denominations, each with a unique signing key. Key derivation enables us to have a single mint key, but deterministically derive a denomination key using the denomination itself as the derivation index. This means that we could go so far as to have a unique denomination for every possible value of a u128!
But it turns out this would be very bad for privacy and fungibility. Unique amounts like 4589234127 make it possible to link one reissue with another. Think about cash for a moment. With USD, we pay using paper denominations of 1,2,5,10,50,100. And also coins: .01, .05, .10, .25, .50. When you buy something with a price of $365.23 you could pay with three 100, one 50, one 10, one 5, two .20, and three .01. Each of those bills or coins is very hard to link with other transactions because so many other people are using the exact same amounts. Yet if you could somehow produce a paper bill worth 365.23, well that is pretty unique and makes your bill really stand out.
Therefore, we can say that each denomination represents an anonymity set, or pool for your transaction to hide in. The more denominations (or pools) we have, the smaller each pool is. So in terms of privacy/fungibility, we would aim for the smallest possible number of pools, which is actually 1, i.e. the very smallest available unit.
However, we must also consider efficiency. This is measured by the number of “coins” (bills or coins) required to make change for a given amount. Using our USD example price of $365.23 we needed 11 individual coins. Plus, consider that in a typical DBC reissue, there will be two logical output amounts: 1: the recipient’s payment, and 2: change for the sender. Creating, signing, and validating DBC coins represents work for the mint nodes and there is also significant network traffic involved spending each DBC. So we must try to minimise the number of change coins required for any transactable amount.
So it becomes clear that there is a tension between optimizing for fungibility and optimizing for efficiency. The optimal fungibility would be to have a single denomination only. The optimal efficiency would be to have a unique denomination for every possible amount.
Transactable amounts vs denominations
When thinking about denominations, we must be careful to distinguish between transactable amounts
and denomination amounts
.
A transactable amount
is a price or payment amount that is being transacted. One or more denomination amounts can be combined/added to make a transactable amount
.
Going forward, we will refer to denomination amount
as simply denomination
.
We will use the term coin
to refer to a particular instance of a denomination.
Our first denominations approach
note: code is here.
Initially we defined a transactable amount
to be a 128bit integer (u128).
We then defined a series of denominations based on powers of ten, eg:
enum {
One, Two, Three, Four, Five, Six, Seven, Eight, Nine,
Ten, Twenty, Thirty, Forty, Fifty, Sixty, Seventy, Eight, Ninety,
Hundred, TwoHundred, ThreeHundred, FourHundred, FiveHundred, SixHundred, SevenHundred, EightHundred, NineHundred,
Thousand, TwoThousand, ThreeThousand, FourThousand, FiveThousand, SixThousand,SevenThousand, EightThousand, NineThousand,
TenThousand, TwentyThousand, ThirtyThousand, FortyThousand, FiftyThousand, SixtyThousand, SeventyThousand, EightyThousand, NinetyThousand
}
And so on, up to 10^38, which approaches the limit of a u128. The total number of denominations was around 340.
We also came up with short names for each power of ten, based on large number names.
tau → taus
mil → mils
bil → bils
tril → trils
quad → quads
quint → quints
sic → sics
set → sets
ott → otts
non → nons
det → dets
unt → unts
The basic idea is that One
represents the smallest unit, for example, the equivalent in bitcoin is called a Satoshi. We would not define an (arbitrary) decimal point location at all, but rather the market would decide what is the right set of units to be using for everyday transactions soon after launch, and over time as “market cap” increases, it would progress from large units eg sets to smaller units eg quads.
Anyway, a decimal point is only a userfacing (display) thing as the value is represented as a u128. So we won’t discuss it any further here.
This approach has some nice properties. By defining 19 for each power of ten, we can represent every digit of a transactable amount
with a single coin, or less if there are any zeros.
For example, if we have the transactable amount
55034, we can represent that with: 1 FiftyTau, 1 FiveTau, 1 Thirty, and 1 Four. So the amount has 5 digits, one of them is zero, and we make change for it with just 4 coins.
This approach also has a couple of drawbacks.

The first is somewhat minor. The code to implement an enum with 340+ variants and mapped names is huge and hard to maintain. We ended up writing a script to generate the denominations.rs source code file. While workable, this was a clumsy, awkward solution.

huge number of change coins. In our tests, the average number of coins required to make change for randomly generated u128
transactable amounts
was 3839. And that’s for a single logical output. Remember that a typical reissue will have two logical outputs, maybe more. So on average, we could be looking at 76+ output DBCs per reissue and comparable number of inputs. That’s a lot of work for the mint and the network, and it also puts the Blind Sig implementation at a severe efficiency disadvantage compared to the Amount Hiding (but traceable) implementation.
For example, let’s use u128::MAX:
$ calc 2^128
340282366920938463463374607431768211456
We have a denomination variant for every [0…9] of every power of ten. There are 40 digits in that number. Two digits are zero, we can ignore them. So we need 38 “coins” to represent the number. Eg:
3*10^38
4*10^37
2*10^35
8*10^34
…and so on…
What if we use u64 instead of u128? Well, that’s a bit better.
$ calc 2^64
18446744073709551616
But it’s still 20 digits. And now we have only 19 powers of ten to work with, so our potential divisibility is more limited.
Now, one could argue that in practice users would choose to send more whole number amounts with lots of zeros. However, that seems not to be true. @mav performed an analysis of the entire bitcoin blockchain and found that most transaction amounts used 8 digits of precision with mostly random numbers. We believe this is because people are still mostly making payments based on fiat (e.g. USD) amounts and calculating the equivalent BTC amount, which becomes a number with many significant digits. The same behavior would seem to apply for our DBCs.
In any event, the system should be designed such that it performs adequately well/efficiently even for the worst case.
So we had to find a better way.
Our second (current) approach
We came up with a simpler, more powerful representation of Denominations. We encode the poweroften exponent in the Denomination enum as a signed integer. So instead of the huge enum with 300+ variants, we have this:
pub type PowerOfTen = i8;
pub enum Denomination {
One(PowerOfTen),
Two(PowerOfTen),
Three(PowerOfTen),
Four(PowerOfTen),
Five(PowerOfTen),
Six(PowerOfTen),
Seven(PowerOfTen),
Eight(PowerOfTen),
Nine(PowerOfTen),
}
So with an i8, we can represent 9*10^128…9*10^127. Remember that the u128, which is already considered huge, gave us only 10^38, in terms of divisibility. So for practical purposes, this representation is nearly limitless. (We could easily go even further by using an i16 or i32 instead of an i8, but it seems like overkill.)
note: The following is not meant as a discussion of SNT monetary policy plans. It is purely hypothetical for illustrative purposes.
This approach to denominations does not make the One
denomination equal to the smallest possible amount as our first approach did. Instead One
can represent our best starting guess at for example 1 DBC = 1 USD. We could aim for One
to have some useful buying power in terms of real world goods, but not a lot. eg, One
should buy a can of soda or perhaps a gumball, not a house.
It is also interesting to think about what our total money supply could be. Just as a hypothetical starting point, let’s say we come up with the Genesis DBC amount by taking the current SNT market cap and calculating a Genesis amount such that 1 DBC = 1 USD. I haven’t calculated what the Genesis amount would be by this method, but let’s just pretend it is 50 million. Ok, so we launch with denom One
(1*10^0) = ~1 USD and the Genesis DBC = 50 million. cool. We will ignore distribution/farming effects for now, except to say they could temporarily inflate/devalue the currency.
Ok, so this is a deflationary (fixed money supply) currency and as time goes by each unit becomes worth more in terms of real world value (or else the project probably fails). As this happens, users will gradually shift to using more 10^1 denoms. then 10^2, and so on. We can accomodate 128 such 10x expansions. We can also accommodate sending 3 million SNT in one dbc, eg by using the denomination 3*10^6. Or much, much, much more than that, if we eg define the Genesis amount to be a lot higher, all the way up to 9*10^127.
Naming
We previously had come up with some names for large numbers but they were a bit awkward and unfamiliar. We found ourselves shortening and altering them so they could be pronounceable. Also they did not cover any negative exponents. But not to worry, the SI scale has us covered, with short names people are already mostly familiar with.
The SI scale names 10^24 … 10^24. Well, not every power of ten, but every 10^3, which is good enough. When we plug these into code, we can generate:
10^24  1 yocto
10^23  10 yocto
10^22  100 yocto
10^21  1 zepto
10^20  10 zepto
10^19  100 zepto
10^18  1 atto
10^17  10 atto
10^16  100 atto
10^15  1 femto
10^14  10 femto
10^13  100 femto
10^12  1 pico
10^11  10 pico
10^10  100 pico
10^9  1 nano
10^8  10 nano
10^7  100 nano
10^6  1 micro
10^5  10 micro
10^4  100 micro
10^3  1 milli
10^2  1 centi
10^1  1 deci
1  1
10^1  1 deka
10^2  1 hecto
10^3  1 kilo
10^4  10 kilo
10^5  100 kilo
10^6  1 mega
10^7  10 mega
10^8  100 mega
10^9  1 giga
10^10  10 giga
10^11  100 giga
10^12  1 tera
10^13  10 tera
10^14  100 tera
10^15  1 peta
10^16  10 peta
10^17  100 peta
10^18  1 exa
10^19  10 exa
10^20  100 exa
10^21  1 zetta
10^22  10 zetta
10^23  100 zetta
10^24  1 yotta
10^25  10 yotta
10^26  100 yotta
If we ever reach denominations smaller than yocto we can figure out names for them. That is for a distant future. For now, there is an Amount::to_si_string() func that just spits out the exponent representation for unnamed values.
Representing transactable amounts
Previously we used a u128 to represent transactable amounts
. But how do we deal with amounts when our new Denomination enum allows values much larger than u128 allows (10^38), and also they can be negative exponents, i.e. tenths, hundredths, thousandths, etc?
Ok, so this is where things get a bit tricky.
What we’ve done is to change sn_dbc::Amount from a u128 alias to:
pub type PowerOfTen = i8;
pub type AmountCounter = u32;
pub struct Amount {
pub count: AmountCounter,
pub unit: PowerOfTen,
}
note: This may be renamed to TransactableAmount
in the future.
With this structure, we can represent monetary amounts as a power of ten (representing a single denomination or coin) plus a count, representing the number of those coins.
AmountCounter is a u32, so we can represent well over a billion count of any denomination. Presently we limit it to exactly 1billion (10^9).
We arrived at this number by thinking about typical transactions today. Individuals regularly make cash payments of up to a few thousand dollars, purchase a house for a million dollars, etc. But 100 million tx are pretty rare. And billion dollar tx are very rare – we are talking huge corporations and governments. We wanted to avoid forcing people to change their (mental) pricing unit for regular transactions. But by the time we get to a 10^9 difference in units, we are sort of in a different financial league.
Another way of thinking about it is that if Sally is sending 1 billion USD, she probably doesn’t care very much if she can’t use 1 dollar increments. Sally could reissue 1 billion + 10, but she could not reissue 1 billion + 1 (or 2, 3, 4…). Sally could probably live with that. Whereas if we set the limit at say 100, and she could only reissue 110, 120, but not 101, 102, that might be more of a problem for her… and most people.
Now remember that the number of digits in the amount defines the max number of change coins required. So by using 1 billion as our limit, we have dropped from 40 change coins to 9 (max). Pretty good!
We could drop this further. 1 million = 10^6, or six coins. That could be a reasonable choice. 1 million is still a pretty high number for everyday tx. We could keep going lower, with the tradeoff being that people must start specifying amounts/prices using higher units.
So this counter_limit() function is a knob we can twist to balance between system efficiency and the granularity of transactable amounts
. Performance testing can/will guide us here as we get a bit further along.
Calculating with Amount
It is important to note up front that Amount still uses only integer math. No floating points are involved.
The mint needs to verify that sum(inputs) == sum(outputs). We have implemented some mathematical operators: checked_sub(), checked_add(), and checked_sum(). These work by converting Amounts to a common base unit and then adding or subtracting the count of each. The result of each operation is either an Amount or Error::AmountIncompatible. Amounts are incompatible if the units are too far apart for the count to be represented within Amount::counter_max() (1 billion).
Amount does not expose the regular unchecked Add, Sub, Sum operators at all, so it is impossible to perform unchecked operations. Also the return value is a Result, not an Option as with built in types.
The Mint code now calls Amount::checked_sum() instead of sum(). With this simple change, the Mint now enforces that all inputs and outputs must be compatible else the reissue fails.
It is also possible to convert an Amount into a Rational number, and back. This has been implemented, but will go into a separate crate, as it is not necessary for Mint or client operations.
Implications for using inputs
The 10^9 “nearness limit” for Amounts applies to both inputs and outputs in a reissue.
However inputs and outputs are summed separately. Thus it could be that an individual input and output would not be compatible by themselves, however the sum of inputs and sum of outputs still equal each other. Here is an example of this:
inputs:
9*10^8, 1*10^8, 9*10^0, 1*10^0
outputs:
1*10^9,1*10^1
$ calc 9*10^8+1*10^8+9*10^0+1*10^0
1000000010
$ calc 1*10^9+1*10^1
1000000010
So that reissue would succeed.
Even still, the nearness limit constrains that inputs and outputs cannot be too far apart, especially if we impose a limit on the number of inputs and outputs. Thus it can be thought of as similar to the (fuzzy) dust limit in bitcoin, except that it is always relative to the other amounts in the reissue, so there is no fixed limit anywhere, which is pleasing.
This nearness limit should incentivise people to transact with others using the most commonly used denominations. Because the mint would not allow me to pay you for a Soda with a One DBC plus a 1 Pico DBC (10^12). We do not presently enforce any maximum number of input DBCs to a reissue, but that would be something to consider also, which would make it impossible to pay with hundreds or thousands of tiny coins, instead of a few reasonably sized coins.
Concluding thoughts
The fact that our original denomination approach requires a large number of coins to represent arbitrary amounts was a serious drawback that was necessary to address in order for a blind sig mint to have a chance at being anywhere near as efficient as an unblinded mint.
This new design is an improvement on the original denomination system, in terms of efficiency, clarity, and (arguably) code simplicity.
While the TransactableAmount concept may at first seem unusual, it should be mostly transparent to users. Initially all transaction amounts under 1 billion DBC would simply be expressible as an integer (using base 10^0). Only if one goes over that amount or needs to drop below 1 DBC, then one must choose a different unit to express the amount in, and wallet software could do this automatically.
Deep divisibility is a nice property which enables tiny micropayments, and something that this design delivers as a sort of happy accident. 128 digits of divisibility is huge, and we could go much much further with a i16 if we wished. By comparison, bitcoin has 8 decimal digits of divisibility and most cryptocurrencies today are similar. 12 decimal places is considered large.
The nearness limit is a novel concept that should help minimise dust and increase fungibility without requiring any fixed size dust limit. It is also a knob we can easily turn to tune between efficiency and transactableamount granularity.
Anyone interested in further details can read the source code. amount.rs contains a lengthy comment/explanation.
code:
Useful Links
Feel free to reply below with links to translations of this dev update and moderators will add them here:
Russian ; German ; Spanish ; French; Bulgarian
As an open source project, we’re always looking for feedback, comments and community contributions  so don’t be shy, join in and let’s create the Safe Network together!