Skip to the content
nullroute
Browse the docs

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):

  1. Each roll is recorded as a single ASCII digit in 1-6 (bytes 0x31 through 0x36).
  2. Rolls are concatenated in the order they were entered, with no separators, no whitespace, no line breaks, and no trailing newline.
  3. The result is exactly N bytes for N rolls.
  4. The entropy is SHA-256 of 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.yaml lines 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:

  1. Write down the rolls the signer shows (SP-ENT-6), in order.
  2. On a separate computer, check the length. The count MUST be the number of rolls:
    $ printf '%s' '<rolls>' | wc -c
    
  3. Hash the string with printf, never echo, which appends a newline:
    $ printf '%s' '<rolls>' | sha256sum
    
    On macOS use shasum -a 256. For the example the output is e56403e8522ddeae1b44a1e8148b1ba4d3b4c626ccf20980056eedcc7e0c0f35.
  4. Convert that hex value to a mnemonic with any offline BIP-39 tool, as entropy, not as a seed.
  5. 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.
  6. 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