# UUID256

A 256-bit (32-byte), purely random unique identifier, formatted `16-8-8-8-24` — the standard UUID's `8-4-4-4-12` with every group doubled.

```
128-bit UUID:  f47ac10b-58cc-4372-a567-0e02b2c3d479                                   (36 chars)
UUID256:       0001020304050607-08090a0b-4c0d0e0f-90111213-1415161718191a1b1c1d1e1f  (68 chars)
                                        ^ version 4       ^ variant 8..b
```

The normative **[specification](#specification)** (text format, bit layout, generation, parsing, storage, test vectors) is in the second half of this file; the first half covers the reference implementations and the bulk duplicate-check results.

- [The algorithm (pseudocode)](#the-algorithm-pseudocode)
- [The algorithm (in plain English)](#the-algorithm-in-plain-english)
- [Reference implementations](#reference-implementations)
- [How the large runs stay exact without 32 GB of RAM](#how-the-large-runs-stay-exact-without-32-gb-of-ram)
- [Results so far](#results-so-far-apple-m4-pro-14-cores-24-gb)
- [Specification](#specification) — §1 Purpose · §2 Comparison with UUID · §3 Text format · §4 Binary layout · §5 Generation · §6 Parsing · §7 Storage & ordering · §8 Special values · §9 Alternate encodings · §10 Security · §11 Test vectors · §12 Reference implementation · §13 Summary

## The algorithm (pseudocode)

Everything an implementation has to do, in five short procedures. Bytes are indexed from 0; byte 0 is the most significant and comes first in the text. Every implementation in this repository is a transliteration of exactly this.

```
constant SIZE     = 32                      ; bytes in an id
constant TEXT_LEN = 68                      ; 64 hex digits + 4 hyphens
constant VERSION  = 4
constant HEX      = "0123456789abcdef"

procedure apply_version_variant(b[0..31]):          ; §4 — the only two bytes ever touched
    b[12] := (b[12] AND 0x0F) OR 0x40                ; high nibble of byte 12 := 4      → hex digit 24 is '4'
    b[16] := (b[16] AND 0x3F) OR 0x80                ; top two bits of byte 16 := 10    → hex digit 32 is 8, 9, a or b

function generate() -> b[0..31]:                     ; §5.1
    b := 32 bytes from the operating system's cryptographically secure random source
    apply_version_variant(b)                         ; (never a non-cryptographic PRNG; never a reused or user-supplied seed)
    return b

function is_strict(b) -> boolean:                    ; §6
    return length(b) = 32 AND (b[12] >> 4) = VERSION AND (b[16] >> 6) = 0b10

function to_canonical(b[0..31]) -> text[0..67]:      ; §3.1  "16-8-8-8-24", lowercase
    out := ""
    for i in 0..31:
        out += HEX[b[i] >> 4] + HEX[b[i] AND 0x0F]
        if i in {7, 11, 15, 19}: out += "-"          ; hyphens after hex digits 16, 24, 32, 40
    return out                                       ; hyphens land at string offsets 16, 25, 34, 43

function parse(text, strict) -> b[0..31] or error:   ; §6   accepts canonical (68) or compact (64), any letter case
    if length(text) = 68:
        if text[16] ≠ '-' or text[25] ≠ '-' or text[34] ≠ '-' or text[43] ≠ '-': return error HYPHEN
        hyphenated := true
    else if length(text) = 64:
        hyphenated := false
    else:
        return error LENGTH                          ; no whitespace, braces, urn: prefixes or trailing newlines
    b := 32 zero bytes; nibbles := 0
    for i in 0..length(text)-1:
        if hyphenated and i in {16, 25, 34, 43}: continue
        v := value of text[i] as a hex digit ('0'..'9', 'a'..'f', 'A'..'F' only)
        if none: return error CHAR                   ; also catches a stray '-' anywhere else
        b[nibbles / 2] := (b[nibbles / 2] << 4) OR v ; big-endian, high nibble first
        nibbles += 1
    if strict and not is_strict(b): return error VERSION
    return b
```

Two properties fall out of this and are worth stating: numeric order of the 256-bit value, byte order of the 32-byte encoding, and code-point order of the canonical text are all the same ordering (§7.1); and `parse(to_canonical(b)) = b` for every strict `b`.

## The algorithm (in plain English)

**Making an id.** Ask the operating system for 32 cryptographically random bytes — `/dev/urandom`, `getrandom(2)`, `getentropy(2)`, `SecureRandom`, `crypto.getRandomValues`, or whatever your platform calls its real CSPRNG; never a general-purpose random-number generator. Then overwrite six bits so the id carries a signature: force the top half of byte 12 to the value 4 (this is the *version*, and it makes the 25th hex digit always a `4`), and force the top two bits of byte 16 to `10` (this is the *variant*, and it makes the 33rd hex digit always one of `8 9 a b`). The other 250 bits stay exactly as the random source produced them. That is the whole generator — no clock, no counter, no machine identifier, nothing to coordinate; two machines can generate ids independently forever, and the chance that any two of the first 10¹⁰ collide is about 2⁻¹⁸⁵.

**Writing it down.** Print the 32 bytes as 64 lowercase hex digits, most significant byte first, and insert a hyphen after the 16th, 24th, 32nd and 40th digit. That gives the five groups `16-8-8-8-24` — 68 characters — which is just the familiar `8-4-4-4-12` UUID shape with every group doubled, so the version digit still sits at the start of the third group and the variant digit at the start of the fourth, where a UUID reader already expects them.

**Reading it back.** Take the text and, if it is 68 characters long, insist that positions 16, 25, 34 and 43 (counting from 0) are hyphens and ignore them; if it is 64 characters long, treat it as the compact form with no hyphens; any other length — including a stray space, a trailing newline, or surrounding braces — is rejected. Every remaining character must be a hex digit in either case; letters are folded to lowercase, and any other character (including a hyphen in the wrong place) is rejected. Pack the digits two per byte, high digit first, into 32 bytes. Finally, in *strict* mode, check the signature: byte 12's top half must be 4 and byte 16's top two bits must be `10`, otherwise the value is not a UUID256 of this specification and is rejected. *Lenient* mode skips that last check so that the all-zero *nil* value, the all-`f` *max* value, and ids from any future layout can still be carried around.

**Storing and comparing.** Keep the 32 raw bytes when you can (`BYTEA`, `BINARY(32)`, `BLOB`) and the 68-character text when you must; either way, sorting the stored form is the same as sorting the numbers, so ranges and indexes behave. Because the ids are uniformly random, new keys land in random places in a B-tree — the same trade-off as UUIDv4 — so keep a separate timestamp column if you need creation order.

## Reference implementations

All thirty-nine implement the same API — `generate`, `to_canonical`, `parse` (strict/lenient), `is_strict` — and all self-check against the spec's [§11 test vectors](#11-test-vectors) on start-up. All sources live flat in `reference/` (one file per language, plus the Verilog testbench, the wasm host and the emulator harness); only sources are kept — the build/run commands below (run from inside each file's directory) produce the binaries, the .NET/Go/Zig ones into caches outside the tree.

| File | Language | Build / run | Bulk duplicate check |
|---|---|---|---|
| `reference/uuid256.c` | C11 + pthreads | `cc -O3 -pthread -o uuid256 uuid256.c && ./uuid256` | exact, sharded, replayable DRBG — **1e9 in 12 s (4.5 s in one 8 GB pass), 1e10 in 3.9 min** on an M4 Pro |
| `reference/uuid256.rs` | Rust (no crates) | `rustc --edition 2021 -O -o uuid256-rs uuid256.rs && ./uuid256-rs` | same design as C |
| `reference/uuid256.py` | Python 3, stdlib | `python3 uuid256.py` | exact, in-memory hash — default 1e7 |
| `reference/uuid256.js` | Node ≥ 18 (≥ 22 for `-n` > 1.3e8), no deps | `node uuid256.js` | exact, in-memory bucket sort — default 1e8 |
| `reference/uuid256.cs` | C# (.NET 10 file-based app, no project) | `dotnet run uuid256.cs` · `dotnet run uuid256.cs -- -g` prints one id | exact, in-memory bucket sort — default 1e8; `Uuid256` is a `readonly struct` you can drop into any .NET 8+ project |
| `reference/uuid256.fsx` | F# script (any .NET SDK) | `dotnet fsi --optimize+ uuid256.fsx` · `… -- -g` prints one id | same algorithm as C#; `Uuid256` module with `Result`-returning `parse` |
| `reference/uuid256.go` | Go 1.21+, stdlib | `go run uuid256.go` · `… -g` prints one id | exact, in-memory bucket sort — default 1e8; `UUID` type implements `TextMarshaler` so it JSON-encodes as canonical text |
| `reference/uuid256.java` | Java 17+, no deps | `java uuid256.java` (source-file mode) · `… -g` prints one id; `java -Xmx6g …` for the 1e8 default | exact, in-memory bucket sort; `Uuid256` is an immutable `Comparable` value class |
| `reference/uuid256.cpp` | C++20, stdlib | `c++ -std=c++20 -O3 -o /tmp/uuid256-cpp uuid256.cpp && /tmp/uuid256-cpp` · `-g` prints one id | exact, in-memory bucket sort — default 1e8; `uuid256` value class with `<=>`, `parse()` returning a result struct |
| `reference/uuid256.swift` | Swift 5.9+, no packages | `swift uuid256.swift -g` (immediate mode) · `swiftc -O -o /tmp/uuid256-swift uuid256.swift` for bulk | exact, in-memory bucket sort — default 1e8; `UUID256` struct is Hashable/Comparable/Codable/LosslessStringConvertible |
| `reference/uuid256.rb` | Ruby 2.6+, stdlib | `ruby uuid256.rb` · `-g` prints one id | exact, in-memory sort — default 1e7 |
| `reference/uuid256.pl` | Perl 5, core modules | `perl uuid256.pl` · `-g` prints one id | exact, in-memory sort — default 1e7; randomness from `/dev/urandom` |
| `reference/uuid256.sh` | Bash 3.2+ / POSIX tools | `./uuid256.sh` · `-g` prints one id · `source uuid256.sh` for the functions | exact via `xxd \| awk \| sort \| uniq -d` — default 1e6 |
| `reference/uuid256.sqlite.sql` | SQLite ≥ 3.41 (pure SQL) | `sqlite3 :memory: < uuid256.sql` | no `CREATE FUNCTION` in SQLite, so: documented expressions + a table whose `DEFAULT` mints ids, a `CHECK` enforcing §6, text/parse views; exact check via `GROUP BY … HAVING count(*) > 1` — 1e6 |
| `reference/uuid256.ts` | TypeScript (Node ≥ 22.6 runs it directly; `tsc --strict` clean) | `node uuid256.ts` · `-g` prints one id | exact, in-memory bucket sort — default 1e8; `Uuid256` class uses only Web Crypto + Uint8Array (runs in Deno/Bun/browsers too) |
| `reference/uuid256.kt` | Kotlin/JVM 2.x | `kotlinc uuid256.kt -include-runtime -d /tmp/uuid256.jar && java -Xmx6g -jar /tmp/uuid256.jar` · `-g` prints one id | exact, in-memory bucket sort — default 1e8; `Uuid256` value class with `parse`/`parseOrNull` |
| `reference/uuid256.zig` | Zig 0.16, std only | `zig run -OReleaseFast uuid256.zig` · `-- -g` prints one id | exact, in-memory bucket sort — default 1e8; `Uuid256` struct formats with `{f}` |
| `reference/uuid256.ps1` | PowerShell 7+ | `pwsh uuid256.ps1` · `-g` prints one id · `. ./uuid256.ps1 -Library` for the functions | exact via .NET `Dictionary` — default 1e6; `New-Uuid256` / `ConvertFrom-Uuid256` / `Format-Uuid256` / `Test-Uuid256` |
| `reference/uuid256.wat` + `reference/uuid256-wasm-host.js` | WebAssembly (hand-written WAT; Node host, needs wabt) | `node uuid256-wasm-host.js` · `-g` prints one id | all §3–§6 logic runs inside wasm (host supplies only CSPRNG bytes + I/O); exact bucket sort over ids generated *in wasm memory* — default 1e7 |
| `reference/uuid256.postgres.sql` | PostgreSQL 13+ (pgcrypto) | `psql -v ON_ERROR_STOP=1 -d db -f uuid256.postgres.sql` | `uuid256_generate/text/parse/is_strict` functions + `DOMAIN uuid256`; built-in 3-planted proof + 1e6 clean run via `GROUP BY … HAVING` |
| `reference/uuid256.mysql.sql` | MySQL 8.0.13+ | `mysql -N -B db < uuid256.mysql.sql` | stored functions (`SIGNAL` errno 45001–45004 for parse errors); table idiom inlines the built-in expression since MySQL bars stored functions from `DEFAULT`/`CHECK`; 3-planted proof + 1e6 |
| `reference/uuid256.mssql.sql` | SQL Server 2017+ / Azure SQL (T-SQL) | `sqlcmd -b -S host -d db -i uuid256.mssql.sql` | scalar functions (`uuid256_parse` returns `NULL` on failure, `uuid256_parse_kind` reports why — T-SQL functions can't `RAISERROR`/`THROW`); `uuid256_generate` is a procedure since functions can't call `CRYPT_GEN_RANDOM`; `CHECK`-constrained table; 3-planted proof + 1e6 |
| `reference/uuid256.jl` | Julia 1.9+ | `julia uuid256.jl` · `-g` prints one id | `RandomDevice()`; exact bucket sort — default 1e7 |
| `reference/uuid256.R` | R 4.x (base) | `Rscript uuid256.R` · `-g` prints one id | `/dev/urandom` (base R has no CSPRNG); vectorised §4, `duplicated()` — default 1e6 |
| `reference/uuid256.hs` | Haskell (GHC 9, base+bytestring+containers) | `ghc -O2 -outputdir /tmp/x -o /tmp/uuid256-hs uuid256.hs` · `-g` prints one id | `newtype` over ByteString, `Either ParseError`; `Data.Set` dedup — default 1e6 |
| `reference/uuid256.ml` | OCaml 4.14+/5 | `ocaml uuid256.ml -g` (toplevel) · `ocamlopt -O3` for bulk | `result`-returning `parse`; Hashtbl dedup — default 1e6 |
| `reference/uuid256.exs` | Elixir 1.14+ / OTP | `elixir uuid256.exs` · `-g` prints one id | `:crypto.strong_rand_bytes`; §4 and `strict?` by binary pattern matching; Map dedup — default 1e6 |
| `reference/uuid256.clj` | Clojure 1.11+ | `clojure -M uuid256.clj` · `-g` prints one id | `SecureRandom`; `ex-info` parse errors; HashMap keyed by `ByteBuffer` — default 1e6 |
| `reference/uuid256.php` | PHP 8.1+ | `php uuid256.php` · `-g` prints one id | `random_bytes`; binary-string array keys — default 1e6 |
| `reference/uuid256.dart` | Dart 3 | `dart run uuid256.dart` · `-g` prints one id | `Random.secure()`; `Comparable` value class; bucket sort — default 1e6 |
| `reference/uuid256.scala` | Scala 3 (scala-cli runner) | `scala run --workspace /tmp/x uuid256.scala -- -g` | `Either[ParseError, Uuid256]`, `Ordered`; HashMap dedup — default 1e6 |
| `reference/uuid256.lua` | Lua 5.3+ | `lua uuid256.lua` · `-g` prints one id · `require("uuid256")` | `/dev/urandom`; `parse` returns bytes or `nil, kind`; table dedup — default 1e6 |
| `reference/uuid256.f90` | Fortran 2008 (gfortran) | `gfortran -O2 -o /tmp/uuid256-f90 uuid256.f90 -J /tmp` · `-g` prints one id | `/dev/urandom` stream read; module `uuid256_mod` with `parse` returning an error code; bucket sort — default 1e6 |
| `reference/uuid256.cob` | COBOL (GnuCOBOL 3, free format) | `cobc -x -O2 -o /tmp/uuid256-cob uuid256.cob` · `-g` prints one id | `CALL "getentropy"` straight into libc; no bit operators in COBOL, so §4 is `MOD(b,16)+64` / `MOD(b,64)+128`; table `SORT` dedup — default 2e5 (1e6 in 1.8 s) |
| `reference/uuid256.v` + `reference/uuid256_tb.v` | Verilog-2005 (Icarus) | `iverilog -o /tmp/uuid256.vvp uuid256.v uuid256_tb.v && vvp /tmp/uuid256.vvp +selftest` · `+gen=N` prints ids | synthesizable `uuid256_apply` (pure wiring), `is_strict`, `format`, `parse` modules; the testbench feeds real entropy from `/dev/urandom` via `$fread`; 100k simulated ids validated by the Python parser |
| `reference/uuid256.aarch64.s` | AArch64 assembly (macOS) | `cc -o uuid256-asm uuid256.s && ./uuid256-asm 5` | generation + formatting only; `./uuid256-asm -` is a test mode (32 raw bytes on stdin → canonical) |
| `reference/uuid256.x86_64.S` | x86-64 assembly (macOS via libc calls, Linux via raw syscalls — one `.S`) | `cc -arch x86_64 -o /tmp/uuid256-x64 uuid256.x86_64.S && /tmp/uuid256-x64 5` (runs natively under Rosetta 2 on Apple silicon) · Linux: `zig cc -target x86_64-linux-musl -nostdlib -static …` | generation + formatting; `-` test mode; both variants verified (1M ids validated by the Python strict parser) |
| `reference/uuid256.i386.s` | 32-bit x86 assembly (Linux, `int 0x80`, no libc) | `zig cc -target x86-linux-musl -nostdlib -static uuid256.i386.s -o /tmp/uuid256-i386` · `python3 emulate.py /tmp/uuid256-i386 5` | macOS can't execute i386 at all, so it is run for real under **`reference/emulate.py`** (Unicorn CPU emulator + a 4-syscall Linux shim); 100k emulated ids validated |
| `reference/uuid256.riscv64.s` | RISC-V 64 assembly (Linux, `ecall`, no libc) | `zig cc -target riscv64-linux-musl -nostdlib -static uuid256.riscv64.s -o /tmp/uuid256-rv64` · `python3 emulate.py /tmp/uuid256-rv64 5` | same harness; 100k emulated ids validated |

Every CLI program (all but the assembly and Verilog ones) speaks the same three modes: `-g [count]` prints ids; `-n <count> [-i <k>]` runs the self-tests and the exact bulk duplicate check, planting *k* known duplicates to prove the detector fires; `-p <text>` / `-P <text>` parse strictly / leniently and print `ok <hex64>` or `error <length|hyphen|char|version>` (PowerShell: `-p <text> [-Lenient]`; Verilog: `+parse=<text> [+lenient]`). C/Rust add `-s <shards>` (RAM ≈ 2 × 16 B × n / shards), `-t <threads>`, `-k <seed>` to replay a run, `-r first:count` to process a subset of shards (partial coverage, reported as such). The SQLite/PostgreSQL/MySQL scripts have both a planted-duplicate proof and a clean run built in.

**Conformance suite:** `python3 tests/conformance.py` drives every implementation whose toolchain is present through 27 parse edge cases (canonical/compact, any case, hyphen slots, version/variant, decoration, non-ASCII) × strict/lenient, generation validity/uniqueness, tiny-`n` bulk runs and bad-argument handling; `tests/conformance.py go rust php` runs a subset. Set `UUID256_PSQL` / `UUID256_MYSQL` to client commands to include the database engines, `UUID256_PYTHON` to a Python with `unicorn` for the emulated assembly targets.

## How the large runs stay exact without 32 GB of RAM

The C/Rust harness keys a ChaCha20 DRBG once from the OS CSPRNG (the same construction the OS RNGs use internally, so §5.2 still holds; the block function runs four blocks per call in SIMD lanes and is checked bit-for-bit against the scalar RFC 8439 vector), which makes the id stream **replayable and seekable**. It then makes one pass per shard over all *n* ids and, for each id whose top bits match the shard, writes **one 8-byte entry** — the id's 40-bit index plus the 24 prefix bits that follow the shard and region bits — straight into one of 4096 regions selected by 12 more prefix bits (threads claim 64-slot chunks of a region with a single atomic add; a region that fills beyond its 6-sigma capacity spills to a small side list that is folded back in). Each region is then radix-sorted by key and every equal-key pair is regenerated by index and compared over all 32 bytes. Full duplicates always share the prefix, so there are no false negatives; regeneration removes the false positives (there are about n²/(2³⁷·shards) of them, all cheap).

Cost model: **RAM ≈ 8.3 B × n / shards, time ≈ shards × (n / ~350 M ids/s) + a few seconds per shard**. That is why 1e10 is a 4-minute job here (8 shards, ~10 GB). 1e12 is still not: it needs 8.3 TB of storage, or with `-s 512` (16 GB per pass) 512 full passes of 1e12 ids (~50 min each here, ~35 min with `-R 8`) ≈ 2–3 weeks.

## Results so far (Apple M4 Pro, 14 cores, 24 GB)

| Implementation | ids checked (exact) | duplicates | wall time |
|---|---|---|---|
| C | 1,000,000,000 | **0** | 11.8 s (4 shards, 2.1 GB) · 4.5 s (`-s 1`, 8.1 GB) |
| C | **10,000,000,000** | **0** | 231.9 s (3.9 min, 8 shards, 10.1 GB; was 20 min with the old 16 B/entry design) |
| Rust | 1,000,000,000 | **0** | 10.9 s (4 shards, 2.1 GB) |
| C# | 100,000,000 | **0** | ~5 s |
| F# | 100,000,000 | **0** | ~6 s |
| Go | 100,000,000 | **0** | ~5 s |
| Java | 100,000,000 | **0** | ~19 s (SecureRandom bulk fill dominates) |
| C++ | 100,000,000 | **0** | 3.9 s |
| Swift | 100,000,000 | **0** | 4.0 s |
| Ruby | 10,000,000 | **0** | 22 s |
| Perl | 10,000,000 | **0** | 45 s |
| Bash | 1,000,000 | **0** | 7 s |
| SQLite | 1,000,000 | **0** | 2 s (+3/3 planted found in the built-in proof) |
| TypeScript | 100,000,000 | **0** | ~35 s |
| Kotlin | 100,000,000 | **0** | ~18 s (SecureRandom bulk fill dominates) |
| Zig | 100,000,000 | **0** | ~4 s |
| PowerShell | 1,000,000 | **0** | 2 s |
| WebAssembly | 10,000,000 | **0** | ~6 s |
| PostgreSQL | 1,000,000 | **0** | 1.2 s (+3/3 planted proof) |
| MySQL | 1,000,000 | **0** | 3.5 s (+3/3 planted proof) |
| SQL Server | 1,000,000 | **0** | 17.3 s (+3/3 planted proof; measured in Docker, linux/amd64 emulated on the M4 Pro — not a native comparison like the rows above) |
| Julia | 10,000,000 | **0** | 1.3 s |
| R | 1,000,000 | **0** | 3 s |
| Haskell | 1,000,000 | **0** | ~3 s |
| OCaml | 1,000,000 | **0** | 0.2 s |
| Elixir | 1,000,000 | **0** | 1 s |
| Clojure | 1,000,000 | **0** | 0.3 s |
| PHP | 1,000,000 | **0** | 0.1 s |
| Dart | 1,000,000 | **0** | 1 s |
| Scala | 1,000,000 | **0** | 0.3 s |
| Lua | 1,000,000 | **0** | 1.1 s |
| JavaScript | 100,000,000 | **0** | ~40 s |
| Python | 10,000,000 | **0** | ~5 s |
| Fortran | 1,000,000 | **0** | ~1 s |
| COBOL | 1,000,000 | **0** | 1.8 s |
| Verilog (Icarus sim) | 100,000 (Python strict parser) | **0** | 6 s |
| Assembly (AArch64, native) | 1,000,000 (piped through the Python strict parser) | **0** | 0.17 s |
| Assembly (x86-64, Rosetta 2) | 1,000,000 (Python strict parser) | **0** | ~3 s |
| Assembly (i386, emulated) | 100,000 (Python strict parser) | **0** | ~40 s under Unicorn |
| Assembly (RISC-V 64, emulated) | 100,000 (Python strict parser) | **0** | ~40 s under Unicorn |

Every run also reported 0 version/variant violations and 0 failed text round-trips, and every detector (all 39) was proven live by planting duplicates (`-i k`) and seeing exactly *k* reported. (Run logs were not kept; re-run any row with the commands above.)

Expected number of collisions for *n* = 1e10 random 250-bit values: n²/2²⁵¹ ≈ 2⁻¹⁸⁵. Any duplicate would indicate a broken random source, not bad luck.

**About 1e12:** requested, and not run. An exact check must remember ~8.3 B per id → 8.3 TB, or with replay-sharding `-s 512` (16 GB per pass) 512 full passes of 1e12 ids (~50 min each here) ≈ 2–3 weeks. The harness *can* express it — `./uuid256 -n 1000000000000 -s 4096 -r 0:8` runs 8 of 4096 shards (2 GB each) and reports the resulting 0.2 % coverage honestly — but a full 1e12 needs a cluster with ~10 TB, not a laptop.

---

## Specification

| | |
|---|---|
| **Status** | Draft v1.0 |
| **Date** | 2026-08-17 |
| **Scope** | Binary layout, canonical text format, generation, parsing, storage, and comparison rules for a 256-bit random unique key |

> **Terminology.** The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are to be interpreted as described in RFC 2119.
>
> **Size note.** UUID256 is **256 bits = 32 bytes = 64 hexadecimal digits**. This is exactly double the standard 128-bit UUID (16 bytes, 32 hex digits). Throughout, "UUID" without a qualifier means the 128-bit RFC 9562 UUID.

---

### 1. Purpose

UUID256 is a fixed-size, globally unique, **purely random** identifier intended for use as a primary/unique key in databases, message systems, and APIs. It is the 256-bit analogue of UUIDv4:

- **250 bits of cryptographically random data** (vs. 122 for UUIDv4), making collisions unreachable in practice at any generation rate, with no coordination, clocks, or node identifiers.
- **Nothing embedded, nothing leaked**: no timestamp, sequence, or machine identity can be recovered from an id.
- **A text format that is visually kin to the standard UUID** so operators immediately recognise it, yet cannot be mistaken for one.

### 2. Comparison with the standard 128-bit UUID

| Property | UUID (RFC 9562) | **UUID256 (this spec)** |
|---|---|---|
| Size | 128 bits / 16 bytes | **256 bits / 32 bytes** |
| Hex digits | 32 | **64** |
| Canonical text | `8-4-4-4-12` (5 groups) | **`16-8-8-8-24` (5 groups)** |
| Canonical length | 36 chars | **68 chars** |
| Example | `f47ac10b-58cc-4372-a567-0e02b2c3d479` | `0001020304050607-08090a0b-4c0d0e0f-90111213-1415161718191a1b1c1d1e1f` |
| Version digit | 1st hex digit of group 3 | **1st hex digit of group 3** (same position) |
| Variant digit | 1st hex digit of group 4 | **1st hex digit of group 4** (same position) |
| Content | v4: random | **random** |
| Random bits | v4: 122 | **250** |

The design rule is simple: **every field of `8-4-4-4-12` is doubled**, giving `16-8-8-8-24`. This keeps the same five-group silhouette, the same relative field proportions, and keeps the version and variant digits at the same *relative* positions ("first character of the third group" / "first character of the fourth group") that people already know from standard UUIDs.

### 3. Canonical text format

#### 3.1 Definition

A UUID256 in canonical text form is the 64 hexadecimal digits of the 256-bit value (most-significant nibble first), split into five hyphen-separated groups of **16, 8, 8, 8, and 24** digits:

```
xxxxxxxxxxxxxxxx-xxxxxxxx-Vxxxxxxx-Rxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx
|<---- 16 ---->| |<- 8 ->| |<- 8 ->| |<- 8 ->| |<-------- 24 -------->|
   group 1        group 2   group 3   group 4          group 5
   bits 0–63     bits 64–95 bits 96–127 bits 128–159   bits 160–255
```

- `V` = version digit (see §4), always `4` for this specification.
- `R` = variant digit (see §4), always one of `8`, `9`, `a`, `b`.
- Every other digit is random.
- Total length: **68 characters** (64 hex digits + 4 hyphens).
- Hyphens are at 0-based string offsets **16, 25, 34, 43**.

#### 3.2 Rules

1. Output MUST use **lowercase** hexadecimal digits (`0-9`, `a-f`).
2. Output MUST include exactly the four hyphens at the positions above.
3. Parsers MUST accept uppercase and mixed-case input and MUST normalise to lowercase.
4. Parsers SHOULD accept the **compact form** (64 hex digits, no hyphens) as input; the compact form MUST NOT be emitted where the canonical form is expected.
5. Braces `{…}`, `urn:` prefixes, whitespace, and any other decoration are NOT part of the format and MUST be rejected by strict parsers.

#### 3.3 Grammar (ABNF)

```abnf
uuid256    = group1 "-" group2 "-" group3 "-" group4 "-" group5
group1     = 16HEXDIG
group2     = 8HEXDIG
group3     = 8HEXDIG          ; first digit is the version (4)
group4     = 8HEXDIG          ; first digit is the variant (8/9/a/b)
group5     = 24HEXDIG
HEXDIG     = DIGIT / "a" / "b" / "c" / "d" / "e" / "f"   ; lowercase on output;
                                                         ; A-F also accepted on input
compact    = 64HEXDIG         ; input-only alternate form
```

#### 3.4 Regular expressions

Structural (any version/variant):

```
^[0-9a-f]{16}-[0-9a-f]{8}-[0-9a-f]{8}-[0-9a-f]{8}-[0-9a-f]{24}$
```

Strict — this specification's version (`4`) and variant (`8`–`b`):

```
^[0-9a-f]{16}-[0-9a-f]{8}-4[0-9a-f]{7}-[89ab][0-9a-f]{7}-[0-9a-f]{24}$
```

Add the `i` flag (or use `[0-9a-fA-F]`) when validating input; never when validating output.

#### 3.5 Formats considered and rejected

| Layout | Length | Why not |
|---|---|---|
| **`16-8-8-8-24`** (chosen) | 68 | Proportional double of `8-4-4-4-12`; version/variant keep their familiar positions; 5 groups scan easily. |
| `8-4-4-4-12-8-4-4-4-12` (two UUIDs) | 73 | Ten groups; reads as "two UUIDs glued together"; the second half would carry a meaningless fake version/variant. |
| `8-4-4-4-12` + separator + `8-4-4-4-12` (e.g. `…::…`) | 73–74 | Same problems; also invites tooling to split it into two 128-bit values. |
| `8-8-8-8-8-8-8-8` | 71 | Uniform but eight groups is harder to scan and no group has semantic meaning. |
| `16-16-16-16` | 67 | Groups too long to eyeball; loses the UUID silhouette. |
| 64 hex, no hyphens | 64 | Compact but painful to read/compare by eye. Retained as the **compact input form** only. |
| Crockford base32 | 52 | Shorter, but not hex, no visual kinship with UUIDs, and needs its own alphabet handling. |
| base64url | 43 | Shortest, but case-sensitive and its lexical order does not match numeric order. Suitable only as an opaque transport encoding (see §9). |

### 4. Binary layout

The value is a 256-bit unsigned integer, serialised **big-endian** (network byte order): byte 0 is the most significant byte and corresponds to hex digits 0–1 of the text form. Bit 0 is the most significant bit.

| Field | Bits | Width | Bytes | Hex digits | Text group | Description |
|---|---|---|---|---|---|---|
| `rand_a` | 0–95 | 96 | 0–11 | 0–23 | 1–2 | Cryptographically random. |
| `ver` | 96–99 | 4 | 12 (high nibble) | 24 | 3 | Version. `0100` (`4`) for this layout. |
| `rand_b` | 100–127 | 28 | 12 (low nibble)–15 | 25–31 | 3 | Cryptographically random. |
| `var` | 128–129 | 2 | 16 (top 2 bits) | 32 (partial) | 4 | Variant. `10` — hex digit 32 is therefore `8`, `9`, `a`, or `b`. |
| `rand_c` | 130–255 | 126 | 16 (low 6 bits)–31 | 32 (partial)–63 | 4–5 | Cryptographically random. |

Totals: 4 version + 2 variant + **250 random** = 256 bits.

```
 byte:  0  1  2  3  4  5  6  7  8  9 10 11 |12 13 14 15 |16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
        [--------------- rand_a ---------------] [V][ rand_b ] [R][------------------- rand_c -------------------]
 text:  group 1 (16 hex)        group 2 (8)   group 3 (8)   group 4 (8)   group 5 (24)
```

Only two bytes are ever touched after drawing randomness: the high nibble of **byte 12** is forced to `4`, and the top two bits of **byte 16** are forced to `10`.

#### 4.1 Version and variant

- **Version** (`ver`, 4 bits) identifies the field layout. This document defines version `4` — *random*. The value was chosen to mirror UUIDv4's meaning so the digit reads the same way in both formats. All other values are reserved for future layouts and MUST NOT be emitted.
- **Variant** (`var`, 2 bits) is a family discriminator, mirroring the RFC 9562 convention. This document defines `10`. Values `00`, `01`, `11` are reserved.
- Both fields sit at the same relative positions as in the 128-bit UUID (first hex digit of group 3 and group 4), so a human can read "version 4, variant 8-b" straight from the text.

### 5. Generation

#### 5.1 Algorithm

Generation is stateless: no clock, counter, node id, or coordination is required.

```
function generate():
    b = 32 bytes from a CSPRNG
    b[12] = (b[12] & 0x0F) | 0x40          # ver = 4  → hex digit 24 is '4'
    b[16] = (b[16] & 0x3F) | 0x80          # var = 10 → hex digit 32 is 8/9/a/b
    return b
```

#### 5.2 Requirements

1. All 32 bytes MUST be drawn from a cryptographically secure random source (`/dev/urandom`, `getrandom(2)`, `crypto.getRandomValues`, `SecureRandom`, `os.urandom`, etc.). Non-cryptographic PRNGs (Mersenne Twister, xorshift, `rand()`, `Math.random`) MUST NOT be used.
2. Random bytes MUST NOT be reused across ids, derived from user input, or generated from a seed that could repeat (e.g. a forked process inheriting a userspace RNG state — implementations SHOULD prefer the OS entropy source directly, or reseed after `fork()`).
3. Generators MUST NOT emit values with any version or variant other than those in §4.1.
4. Because generation is stateless, any number of generators (threads, processes, machines) MAY run concurrently without coordination.

#### 5.3 Uniqueness argument

Every id carries 250 independent, uniformly random bits. For *n* ids generated anywhere, ever, the probability of at least one collision is ≈ *n*² / 2²⁵¹.

| Ids generated | P(any collision) |
|---|---|
| 2⁶⁴ (≈1.8 × 10¹⁹) | ≈ 2⁻¹²³ |
| 2⁸⁰ | ≈ 2⁻⁹¹ |
| 2¹⁰⁰ | ≈ 2⁻⁵¹ |
| 2¹²⁵ | ≈ 50 % (birthday bound) |

For comparison, UUIDv4 has 122 random bits: 2⁶⁴ ids give ≈ 2⁻⁵ chance of a collision, and the 50 % birthday bound is at ≈ 2⁶¹ ids. UUID256 moves the birthday bound to ≈ 2¹²⁵.

### 6. Parsing and validation

A conforming parser:

1. Trims nothing — surrounding whitespace is an error in strict mode (lenient mode MAY trim).
2. Accepts exactly 68 characters with hyphens at offsets 16, 25, 34, 43, **or** exactly 64 characters with no hyphens.
3. Accepts hex digits in either case; rejects any other character.
4. Decodes to 32 bytes.
5. In **strict** mode additionally checks `ver == 4` and `var == 10`; in **lenient** mode accepts any version/variant so that future layouts and the special values (§8) can be transported.

Errors MUST distinguish at least: wrong length, hyphen at wrong position, invalid character, unsupported version/variant.

### 7. Storage, comparison, and ordering

#### 7.1 Ordering

All of the following orderings are **identical** by construction:

- numeric order of the 256-bit unsigned integer,
- byte-wise (`memcmp`) order of the 32-byte big-endian encoding,
- byte-wise / code-point order of the canonical lowercase text.

Uppercase or mixed-case text would break the text-order property, which is why lowercase is mandatory on output.

Because the value is uniformly random, ordering carries **no meaning**: it is not creation order and reveals nothing. Consequently:

- New keys land at uniformly random positions in a B-tree / clustered index (same characteristic as UUIDv4). For write-heavy tables where this matters, keep a separate `created_at` column for chronological queries, or use a non-clustered / hash index on the id.
- Range scans over the id space are only useful for sharding/partitioning by prefix, which random ids do evenly.

#### 7.2 Recommended storage types

| System | Type | Notes |
|---|---|---|
| PostgreSQL | `BYTEA` (32 bytes) | The native `uuid` type is 128-bit and cannot hold a UUID256. Add `CHECK (octet_length(id) = 32)`. |
| MySQL / MariaDB | `BINARY(32)` | |
| SQLite | `BLOB` | |
| SQL Server | `BINARY(32)` | |
| Cassandra / ScyllaDB | `blob` | |
| Any RDBMS, text column | `CHAR(68)` / `VARCHAR(68)` | Store canonical lowercase; use a binary/case-sensitive collation. |
| JSON / protobuf `string` / HTTP / logs | canonical text | 68 chars. |
| protobuf `bytes`, msgpack bin, etc. | 32 raw bytes | Big-endian as defined in §4. |

Binary storage is preferred (32 vs 68 bytes, faster comparisons). Text storage is acceptable where readability or tooling matters.

#### 7.3 Prefix use

Any fixed-length prefix of a UUID256 (e.g. the first 8 hex digits) is itself uniformly random and MAY be used for sharding, bucketing, or as a short display handle — with the usual collision math for the shortened width. Prefixes are never unique on their own and MUST NOT be used as keys.

### 8. Special values

| Name | Canonical text | Meaning |
|---|---|---|
| Nil | `0000000000000000-00000000-00000000-00000000-000000000000000000000000` | "No id" sentinel. Never generated. |
| Max | `ffffffffffffffff-ffffffff-ffffffff-ffffffff-ffffffffffffffffffffffff` | Upper bound sentinel (e.g. range scans). Never generated. |

Both are structurally valid, fail strict version/variant checks (by design), and MUST NOT be used as real keys.

### 9. Alternate encodings (non-normative)

Where the 68-character canonical form is too long (URLs, QR codes, tokens), a system MAY transport the same 32 bytes as:

| Encoding | Length | Notes |
|---|---|---|
| Compact hex | 64 | Case-insensitive; preserves sort order if lowercase. |
| Crockford base32 (no padding) | 52 | Case-insensitive, URL-safe, preserves sort order. |
| base64url (no padding) | 43 | Shortest; case-sensitive; does **not** preserve sort order. |

These are transport encodings only. Interfaces MUST document which encoding they accept, and the canonical form is always the interchange default.

### 10. Security and privacy considerations

- A UUID256 embeds **no** timestamp, counter, or machine identity; nothing about when, where, or how fast ids were generated can be recovered from the value.
- With a CSPRNG, all 250 random bits are unpredictable, so an id cannot be guessed or enumerated. It is nonetheless an *identifier*, not a *credential*: authorisation MUST NOT rest on the secrecy of an id, because ids routinely appear in logs, URLs, and foreign keys. Use purpose-built tokens for secrets.
- The security of the uniqueness guarantee is exactly the security of the random source. A weak or repeating RNG (§5.2) is the only realistic way to produce a collision.

### 11. Test vectors

Generated with the algorithm in §5.1 from fixed "random" input, showing exactly which bits change (byte 12 high nibble → `4`, byte 16 top two bits → `10`).

**Vector 1** — input bytes `00 01 02 … 1f`:

| | |
|---|---|
| Input (hex) | `000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f` |
| Output (hex) | `000102030405060708090a0b4c0d0e0f901112131415161718191a1b1c1d1e1f` |
| Canonical text | `0001020304050607-08090a0b-4c0d0e0f-90111213-1415161718191a1b1c1d1e1f` |
| Changed bytes | byte 12: `0c` → `4c` (version); byte 16: `10` → `90` (variant `10` + low bits `010000`) |

**Vector 2** — input bytes `ff fe fd … e0`:

| | |
|---|---|
| Input (hex) | `fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7e6e5e4e3e2e1e0` |
| Output (hex) | `fffefdfcfbfaf9f8f7f6f5f443f2f1f0afeeedecebeae9e8e7e6e5e4e3e2e1e0` |
| Canonical text | `fffefdfcfbfaf9f8-f7f6f5f4-43f2f1f0-afeeedec-ebeae9e8e7e6e5e4e3e2e1e0` |
| Changed bytes | byte 12: `f3` → `43` (version); byte 16: `ef` → `af` (top bits `11` → `10`) |

Decoded fields of vector 1:

| Field | Value |
|---|---|
| `rand_a` | `0x000102030405060708090a0b` |
| `ver` | `4` |
| `rand_b` | `0xc0d0e0f` |
| `var` | `10` (hex digit `9`) |
| `rand_c` | `0x101112131415161718191a1b1c1d1e1f` (byte 16 = `0x90` with the top two bits cleared → `0x10`) |

Live examples from a real generator (note the `4` at the head of group 3 and `8`–`b` at the head of group 4; everything else is random):

```
c7959992fb0cd8bd-d05c7030-40de491e-88ea97ac-829bb01dfcb89f76bd13b06c
765122f3f03e9ab3-80d6c24e-4828ab37-82cd399b-1f9b8096377d81129215b4a4
683062f8f59f7c20-c97a1d85-40d83331-881b54e5-02d5666e7e1d3e06dca57453
```

### 12. Reference implementation (Python, non-normative)

> Full implementations with self-tests and bulk duplicate checks accompany this spec — 39 of them, grouped in `native/`, `jvm/`, `dotnet/`, `web/`, `scripting/`, `functional/`, `data/`, `database/`, `assembly/`. See [Reference implementations](#reference-implementations) above.

```python
import os, re

_RX = re.compile(r'^[0-9a-f]{16}-[0-9a-f]{8}-4[0-9a-f]{7}-[89ab][0-9a-f]{7}-[0-9a-f]{24}$')

def generate() -> bytes:
    b = bytearray(os.urandom(32))
    b[12] = (b[12] & 0x0F) | 0x40   # version 4
    b[16] = (b[16] & 0x3F) | 0x80   # variant 10
    return bytes(b)

def to_canonical(b: bytes) -> str:
    h = b.hex()
    return f"{h[0:16]}-{h[16:24]}-{h[24:32]}-{h[32:40]}-{h[40:64]}"

def parse(s: str, strict: bool = True) -> bytes:
    s = s.lower()
    if len(s) == 68 and s[16] == s[25] == s[34] == s[43] == '-':
        s = s.replace('-', '')
    if len(s) != 64 or not re.fullmatch(r'[0-9a-f]{64}', s):
        raise ValueError("malformed UUID256")
    if strict and not _RX.match(to_canonical(bytes.fromhex(s))):
        raise ValueError("unsupported UUID256 version/variant")
    return bytes.fromhex(s)
```

### 13. Summary of decisions

- **Width:** 256 bits / 32 bytes / 64 hex digits.
- **Text format:** `16-8-8-8-24`, lowercase, 68 characters — the standard `8-4-4-4-12` with every group doubled.
- **Layout:** purely random — 250 CSPRNG bits, with a version nibble (`4`) and variant bits (`10`) at the same relative positions as a standard UUID.
- **Generation:** stateless; no clock, counter, or coordination.
- **Ordering:** binary and text order agree but carry no meaning; index inserts are uniformly distributed (as with UUIDv4).
- **Storage:** 32-byte binary preferred; `CHAR(68)` acceptable.
