# NoNoncense
Generate globally unique nonces (number-only-used-once) in distributed Elixir.
The nonces are guaranteed to be unique if:
- machine IDs are unique for each node (`NoNoncense.MachineId.ConflictGuard` can help there)
- individual machines maintain a somewhat accurate clock (specifically, the UTC clock has to have progressed between node restarts)
## Installation
The package is hosted on [hex.pm](https://hex.pm/packages/no_noncense) and can be installed by adding `:no_noncense` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:no_noncense, "~> 0.0.2"}
]
end
```
## Docs
Documentation can be found on [hexdocs.pm](https://hexdocs.pm/no_noncense/).
## Usage
Note that `NoNoncense` is not a GenServer. Instead, it stores its initial state using `m::persistent_term` and its internal counter using `m::atomics`. Because `m::persistent_term` triggers a garbage collection cycle on writes, it is recommended to initialize your `NoNoncense` instance(s) at application start, when there is hardly any garbage to collect.
```elixir
# lib/my_app/application.ex
# generate a machine ID, start conflict guard and initialize a NoNoncense instance
defmodule MyApp.Application do
use Application
def start(_type, _args) do
machine_id = NoNoncense.MachineId.id!(node_list: [:"myapp@127.0.0.1"])
:ok = NoNoncense.init(machine_id: machine_id)
children =
[
# optional but recommended
{NoNoncense.MachineId.ConflictGuard, [machine_id: machine_id]}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
```
Then you can generate plain and encrypted nonces.
```elixir
# generate nonces
iex> <<_::64>> = NoNoncense.nonce(64)
iex> <<_::96>> = NoNoncense.nonce(96)
iex> <<_::128>> = NoNoncense.nonce(128)
# generate encrypted nonces
# be sure to read the NoNoncense docs before using 64/96 bits encrypted nonces
iex> <<_::64>> = NoNoncense.encrypted_nonce(64, :crypto.strong_rand_bytes(24))
iex> <<_::96>> = NoNoncense.encrypted_nonce(96, :crypto.strong_rand_bytes(24))
iex> <<_::128>> = NoNoncense.encrypted_nonce(128, :crypto.strong_rand_bytes(32))
```
## How it works
The first 42 bits are a millisecond-precision timestamp of the initialization time (allows for ~139 years of operation), relative to the NoNoncense epoch (2025-01-01 00:00:00 UTC) by default. The next 9 bits are the machine ID (allows for 512 machines). The remaining bits are a per-machine counter.
A counter overflow will trigger a timestamp increase by 1ms (the timestamp effectively functions as a cycle counter after initialization). The theoretical maximum sustained rate is 2^counter-bits nonces per millisecond per machine. For 64-bit nonces (with a 13-bit counter), that means 8192 nonces per millisecond per machine. Because the timestamp can't exceed the actual time (that would break the uniqueness guarantee), new nonce generation throttles if the nonce timestamp/counter catches up to the actual time. In practice, that will probably never happen, and nonces will be generated at a higher rate. For example, if the first nonce is generated 10 seconds after initialization, 10K milliseconds have been "saved up" to generate 80M 64-bit nonces at virtually unlimited rate. Benchmarking shows rates around 20M/s are attainable.
The design is inspired by Twitter's Snowflake IDs, although there are some differences, most notably in the timestamp which is _not_ a message timestamp. Unlike Snowflake IDs, nonces are meant to be opaque, and not used for sorting.
## Benchmarks
```
On Debian Bookworm, AMD 9700X (8C 16T), 32GB, 990 Pro, generating 100M nonces
NoNonce.nonce(64) single 22_306_381 ops/s
NoNonce.nonce(96) single 40_686_097 ops/s
NoNonce.nonce(128) single 41_549_756 ops/s
NoNonce.encrypted_nonce(64) single 1_205_063 ops/s
NoNonce.encrypted_nonce(96) single 1_140_176 ops/s
NoNonce.encrypted_nonce(128) single 2_421_775 ops/s
:crypto.strong_rand_bytes(8) single 2_650_561 ops/s
:crypto.strong_rand_bytes(12) single 2_660_745 ops/s
:crypto.strong_rand_bytes(126) single 2_654_720 ops/s
NoNonce.nonce(64) multi 39_031_122 ops/s
NoNonce.nonce(96) multi 38_306_529 ops/s
NoNonce.nonce(128) multi 39_012_560 ops/s
NoNonce.encrypted_nonce(64) multi 10_033_706 ops/s
NoNonce.encrypted_nonce(96) multi 9_584_873 ops/s
NoNonce.encrypted_nonce(128) multi 13_515_584 ops/s
:crypto.strong_rand_bytes(8) multi 4_751_390 ops/s
:crypto.strong_rand_bytes(12) multi 4_766_312 ops/s
:crypto.strong_rand_bytes(126) multi 4_749_853 ops/s
```
Some things of note:
- NoNoncense wins! :p
- All methods are quick enough to handle very high peak loads.
- The plain nonce generation rate is hardly influenced by multithreading and seems to hit a bottleneck of some kind, probably to do with `:persistent_term` or `:atomics`. Still, it hits a really high rate and is almost as quick as calling a plain getter.
- Encrypting the nonce exacts a very hefty penalty, but parallellization scales well to alleviate the issue.
- Triple DES sucks.