2. Entropy
2.1 The dice rule
The rule is the one nullroute publishes. Quoted from docs/ENTROPY.md, section
"The canonical encoding" (lines 91-96):
- Each roll is recorded as a single ASCII digit in
1-6(bytes0x31through0x36).- Rolls are concatenated in the order they were entered, with no separators, no whitespace, no line breaks, and no trailing newline.
- The result is exactly
Nbytes forNrolls.- The entropy is
SHA-256of those bytes, which is exactly 32 bytes.
Quoted from packages/core/src/entropy/dice.spec.yaml (core.entropy.dice,
algorithm, lines 35-39):
Each roll is one ASCII digit, 0x31 through 0x36. Rolls are concatenated in entry order with no separators and no trailing newline, giving exactly N bytes for N rolls. The entropy is SHA-256 of those bytes.
entropy = SHA-256( ascii(roll_1) || ... || ascii(roll_N) )
The 32 bytes are then BIP-39 entropy with ENT = 256, which BIP-39 turns into a
24-word mnemonic (bip-0039.mediawiki: "The allowed size of ENT is 128-256
bits", checksum length CS = ENT / 32).
SP-ENT-1. A signer that offers dice entry MUST encode each roll as one byte,
the ASCII digit 0x31 to 0x36 for faces one to six, and MUST concatenate the
rolls in entry order with no separator, whitespace, line break or terminator.
Check: vector dice-to-entropy.json (positive cases, including the worked
example in 2.5); nullroute INV-DICE-3 (entropy.dice.test.ts::published-worked-example).
SP-ENT-2. The signer MUST compute the entropy as SHA-256 of exactly those
bytes and MUST use the 32-byte digest, unmodified, as BIP-39 entropy for a
24-word mnemonic. No other value MAY enter the derivation in this mode. Mixing
dice with machine entropy is a different mode and MUST be labelled as one.
Check: vector dice-to-entropy.json (digest, mnemonic and BIP-39 seed for each
case); nullroute INV-DICE-4, INV-DICE-7.
SP-ENT-3. The signer MUST refuse to derive a seed from fewer than 100 rolls
and MUST accept any count of 100 or more. Check: vector dice-to-entropy.json
(a 99-roll case that MUST be refused, a 100-roll and a 150-roll case that MUST
succeed); nullroute INV-DICE-1, INV-UI-7.
SP-ENT-4. Input that is not canonical MUST be refused, not normalised. That
includes a trailing newline, any whitespace or separator, and any character
other than 1 to 6 (so 0 and 7 are refused, and a d6 read as 0 to 5 is
not accepted by relabelling). Check: vector dice-to-entropy.json (negative
cases); nullroute INV-DICE-2.
SP-ENT-5. Any running count of collected entropy MUST be floor(n * log2(6))
or lower, so the display never claims more than has been collected. Check:
vector dice-accounting.json; nullroute INV-DICE-5.
SP-ENT-6. The rolls MUST remain visible, in entry order, before the seed is derived, so the user can compare them with what was rolled and with what they will hash by hand. Check: nullroute INV-UI-9; manual: enter 100 rolls, confirm every roll is shown in order before confirmation.
SP-ENT-7. The signer MAY warn about suspicious patterns. It MUST NOT discard, reorder or alter a roll, and a warning MUST NOT block derivation. Check: nullroute INV-DICE-6, INV-UI-8.
SP-ENT-8. If the signer offers to roll on the user's behalf, it MUST draw each face uniformly (for example by rejection sampling, not by reduction modulo 6), MUST hash the result by SP-ENT-1 and SP-ENT-2, and MUST state on screen how many rolls it chose. Check: nullroute INV-ENTMODE-1 for the sampling; manual for the on-screen count.
SP-ENT-9. A seed from machine entropy alone MUST NOT be produced without an explicit acknowledgement that the result cannot be checked by the user. Check: nullroute INV-ENTMODE-2.
SP-ENT-10. The implementer MUST publish the rule in this section with at
least one worked example (rolls, digest, mnemonic) and MUST keep its
implementation reproducing that example. Check: vector dice-to-entropy.json;
manual: the procedure in 2.5.
SP-ENT-11. The signer and its documentation MUST NOT state or imply that it has assessed whether the dice were fair or whether the rolls were observed. Check: manual review of on-screen copy and documentation. The underlying property, that the dice were fair and unobserved, is UNTESTABLE by the signer and is a precondition on the user (section 8).
2.2 Why 100 rolls: the arithmetic
A fair six-sided die yields log2(6) bits per roll.
log2(6) = ln(6) / ln(2) = 1.791759469228055 / 0.693147180559945
= 2.584962500721156 bits per roll
The count needed for 256 bits:
256 / 2.584962500721156 = 99.034318652042...
ceil(99.0343...) = 100
What 99 and 100 rolls give:
99 x 2.584962500721156 = 255.911287571394... short of 256 by 0.089
100 x 2.584962500721156 = 258.496250072115... over 256 by 2.496
The same result without floating point, since 6^N possible roll strings carry 256 bits only if 6^N is at least 2^256:
6^99 < 2^256 (99 rolls: fewer roll strings than 256-bit values)
6^100 > 2^256 (100 rolls: more)
These four values were computed for this document (Python math.log2, and
exact integer comparison of 6**99, 6**100 and 2**256). They agree with
docs/ENTROPY.md lines 63-78 and dice.spec.yaml lines 44-46.
Hashing does not add entropy. SHA-256 of a 258.496-bit input is a 256-bit
value; the profile treats SHA-256 as a random oracle here, as
dice.spec.yaml line 125 does, so the output is taken to carry the full 256
bits and no more.
2.3 Bias
The count in 2.2 assumes a fair die. It is not a margin against a biased one, and the profile does not claim 256 bits for dice it cannot see.
For an attacker guessing the seed, what matters is min-entropy: if the likeliest
face comes up with probability p, each roll gives at most -log2(p) bits
against the best guesser. For 100 rolls to reach 256 bits of min-entropy:
100 x -log2(p) >= 256
-log2(p) >= 2.56
p <= 2^-2.56 = 0.169576
A fair die has p = 1/6 = 0.166667. So 100 rolls stay at or above 256 bits only while the likeliest face is within about 0.3 percentage points of fair.
An example of a die that fails this: one face at p = 0.20, the other five at 0.16 each.
min-entropy per roll -log2(0.20) = 2.321928 bits
100 rolls 100 x 2.321928 = 232.19 bits
rolls for 256 bits 256 / 2.321928 = 110.25 -> 111
Shannon entropy per roll -(0.2 log2 0.2 + 5 x 0.16 log2 0.16) = 2.579471 bits
Consequences written into the profile:
- SP-ENT-3 requires accepting more than 100 rolls, so a user who doubts their dice can roll more under the same rule.
- SP-ENT-7 keeps pattern warnings advisory. nullroute's chi-squared band fires
on about 1 in 1000 fair sequences by design (
dice.spec.yamllines 119-123) and its own spec calls the detector "a courtesy, not a security control". No test over 100 rolls can certify a die, and SP-ENT-11 forbids implying one did.
2.4 Agreement with other projects
No BIP defines a dice-to-seed method (research/00-current-state.md, section
4.1, "Dice"). The comparison below is taken from that document's section 4.3,
which quotes each project's source at the revision recorded in
research/handoff-A.md.
| Project or document | Encoding hashed | Count for 24 words | Agrees with SP-ENT-1 and SP-ENT-2 | Differs |
|---|---|---|---|---|
| Coldcard, dice-only path | SHA-256 over the ASCII digits as keyed (shared/seed.py:455, :481, :533) |
threshold = 99 (shared/seed.py:448) |
Yes | 99 rolls accepted. Its path that mixes dice with the hardware RNG uses a domain-separated hash (shared/seed.py:694) and does not agree |
| SeedSigner 0.8.7 | hashlib.sha256(roll_data.encode()) over "1" to "6" |
DICE__NUM_ROLLS__24WORD = 99, exactly |
Yes | Takes exactly 99, so a 100-roll seed cannot be re-entered on it |
| Krux | "".join(self.rolls) for d6, then SHA-256 |
D6_24W_MIN_ROLLS = 99, a minimum |
Yes, for d6 | 99 accepted; d20 rolls are hyphen-joined; 12 words by truncating the digest |
| BCR-2020-001 (Blockchain Commons, research proposal) | Bytes 0x01 to 0x06, SHA-256, then used to seed an HKDF-based generator |
Not stated | No | Different bytes and a further derivation step. The paper says "Existing third-party tools do not use this proposed system" |
What follows for a user cross-checking on a second device:
- A seed made from 100 or more rolls under this profile can be reproduced on Coldcard's dice-only path and on Krux, whose counts are minimums.
- It cannot be reproduced on SeedSigner 0.8.7, which accepts exactly 99.
- A 99-roll seed from any of the three cannot be reproduced on a conforming signer, because SP-ENT-3 refuses it.
- The profile defines 24 words only. Coldcard, SeedSigner and Krux also offer
12 words from 50 rolls; whether a later version of this profile should define
12-word truncation is an open question (
research/handoff-C.md).
2.5 Hand verification
Worked example, from docs/ENTROPY.md lines 131-175. Do not use this seed.
It is public.
Rolls: 123456 repeated to 100 digits.
1234561234561234561234561234561234561234561234561234561234561234561234561234561234561234561234561234
Procedure, for a user checking a signer against their own machine:
- Write down the rolls the signer shows (SP-ENT-6), in order.
- On a separate computer, check the length. The count MUST be the number of
rolls:
$ printf '%s' '<rolls>' | wc -c - Hash the string with
printf, neverecho, which appends a newline:
On macOS use$ printf '%s' '<rolls>' | sha256sumshasum -a 256. For the example the output ise56403e8522ddeae1b44a1e8148b1ba4d3b4c626ccf20980056eedcc7e0c0f35. - Convert that hex value to a mnemonic with any offline BIP-39 tool, as entropy, not as a seed.
- Compare the 24 words with the words the signer showed. For the example:
tornado cactus wheel picture target finish home neither trend picture shoulder endless deputy glide open oxygen another ability forum swear side alcohol devote random. - If they differ, stop and do not use the signer. Check for a mistyped roll first, then the software identity (section 5).
The digest, the 24 words and the BIP-39 seed with an empty passphrase
(e50a4299...98ab8011, printed in full in docs/ENTROPY.md lines 173-174) were
recomputed for this document: the digest with shasum -a 256, the mnemonic and
seed with @scure/bip39. All three match what docs/ENTROPY.md publishes.
2.6 nullroute conformance
| Id | Status | Evidence |
|---|---|---|
| SP-ENT-1 | Met | INV-DICE-2, INV-DICE-3 |
| SP-ENT-2 | Met | INV-DICE-3, INV-DICE-4, INV-DICE-7; Mode B is a separate mode (docs/ENTROPY.md, Modes) |
| SP-ENT-3 | Met | INV-DICE-1, INV-UI-7; no upper bound in packages/core/src/entropy/dice.ts |
| SP-ENT-4 | Met | INV-DICE-2 |
| SP-ENT-5 | Met | INV-DICE-5 |
| SP-ENT-6 | Met | INV-UI-9 |
| SP-ENT-7 | Met | INV-DICE-6, INV-UI-8 |
| SP-ENT-8 | Met for sampling; screen copy manual | INV-ENTMODE-1 |
| SP-ENT-9 | Met | INV-ENTMODE-2 |
| SP-ENT-10 | Met | docs/ENTROPY.md worked example, INV-DICE-3 |
| SP-ENT-11 | Manual | docs/ENTROPY.md, "What the device does not know" |
Rendered from spec/signer-profile.md, the same bytes that ship with the code. SHA-256 4138440d8a2b925d07a9b6d1d96d4189a74c399b5fc717ae27a79d65c0065817