The file format
What every emission declares, field by field.
spec/ β the published specification
What is needed for anyone to reproduce the numbers without our code:
- The inputs:
seed,round.randomness,drand_round - The derivation context:
version|variant|round, plus|bNper block - The stream:
info = contextβ"|"βbytes_already_generated, 64-byte blocks via HKDF-SHA256 - The unbiased integer:
cut = (space/range)*range, discard and retry - The 6 shapes, with their exact rule
- The version convention:
0.xis a test, an integer is history
πͺπΈ En espanol:
README.es.mdΒ· Signed notices:notices.md
Why this is published and not the emitter
A verifier was written in Python from scratch, using only the file and the document, and it
reproduced 8,262 of 8,262 results. If a third party reaches the same numbers with that, there is
nothing else to hand over.
The six global rules
Ambiguity here is what makes an honest emission look like a lie. R2, R5 and R6 are the three that
make a badly written verifier accuse a legitimate file.
| Rule | |
|---|---|
| R1 | Hex is lowercase, with no prefix |
| R2 | Hashes are computed over the decoded bytes, never over the hex text |
| R3 | Times are UTC, RFC 3339, with Z |
| R4 | A SHA-256 is always 64 hex characters |
| R5 | The signature is verified over the raw text. Re-serializing the JSON changes the bytes and breaks a signature that is perfectly valid |
| R6 | The file travels as binary and is not touched. No re-encoding, no trailing newline, no editor |
Which drand round belongs to a turn
The target round is not chosen. It is derived, and this is what makes it impossible for us to
name a round that suits us:
The target round of a turn is the one whose nominal time is exactly the turn's time.
round = (turn β genesis_time) / period + 1
genesis_timeandperiodcome from the drand chain, which is pinned inside every file as
drand_chain_hashanddrand_public_key.
Anyone can recompute it and check that the file names the round it had to name. A file whose
drand_round is not that one is invalid, no matter how well everything else verifies.
This also fixes when the number comes into existence: the round is born at the turn, so the
result cannot exist before its own turn.
The cut-off is the turn
The result is published a few seconds after the turn β that is the time it takes to read the
round, derive and push. Never before: publishing early would mean deriving from an earlier round,
and then the number would exist before its turn.
Which leads to the rule anyone building on this must follow:
Nothing that depends on an emission may accept entries after its turn.
Anyone who publishes anything knows it before the reader does, for as long as publishing takes.
Closing at the turn makes that interval worth nothing.
Missing turns, and how to check that each one is explained
A turn can be missing from the history. That is expected, and every gap must carry its
explanation in the turn right before it. This is what makes the rule checkable instead of
something you have to take our word for.
The rule: while a turn is not closed, no new turn starts. A commitment that was published
must end in an -emission or a -failure; until one of those exists, the emitter does not begin
another turn.
So a gap is read backwards:
2026-08-22/0505 missing
2026-08-22/0500 -emission with "recovery": { "code": "emitted_after_expiry", ... }
The 05:00 turn was closed late. While it was open, 05:05 could not start. That is the whole
explanation, and it is in a signed file.
What counts as explained. A gap is explained when the previous turn's -emission carries a
recovery note, or when that turn ended in a -failure. Both are signed and both say why.
π And the reverse is the guarantee: a gap with a clean turn before it β an
-emissionwith no
recoverynote β is an anomaly with no explanation, and you should treat it as one. Anyone can
walk the history and check that every gap has its reason next to it. We cannot hide a skipped turn
behind a normal-looking one.
Why a late emission is published at all. The result is a deterministic function of a seed
committed beforehand and a drand round named beforehand. Publishing it late gives us no freedom
whatsoever β the number was already fixed. Voiding the turn instead would have given us freedom:
an operator who can cancel a turn by stalling can cancel the ones it dislikes. So the number always
goes out, and the delay is declared.
The three details that break a reimplementation
These are not edge cases. Every reimplementation so far has hit all three.
1. For simple variants, the mold is the group name, not a field.
Only composite carries an explicit blocks array. Everything else carries its parameters flat,
and the mold is the key of the group it sits in:
"discrete_uniform": { "u-0-9": { "min": 0, "max": 9, "result": 2 } }
"composite": { "s-5-of-50-and-2-of-12": { "blocks": [ β¦ ], "result": [[β¦],[β¦]] } }
2. random_matrix carries rows and cols, not count. The number of values to draw is
rows Γ cols, and the result is published as a list of rows.
3. truncated_continuous publishes min and max already multiplied by scale. Do not
multiply again. With min: 1000, max: 1000000, scale: 1000, the span is read straight off the
file:
value = min + integer(max β min + 1)
The shape of result depends on the mold
| Mold | result |
|---|---|
discrete_uniform |
a scalar β 2 |
sample_without_replacement |
a sorted list β [3, 11, 24] |
uniform_vector_with_replacement |
a list, order matters β [1, 0, 1] |
permutation |
a list, the order is the result β [8, 5, 4, β¦] |
truncated_continuous |
a scalar, read it with scale β 833590 |
random_matrix |
a list of rows β [[1,4,7],[2,9,0]] |
composite |
one list per block β [[7,12,30],[2]] |
This text comes from the permanent repository: spec/README.md β
GitHub Β· GitLab