Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

ccdice.py — Coldcard dice-roll verifier

Independent verifier for the dice-roll entropy of a COLDCARD Mkx / Q, matching the firmware roll by roll.

Third-party dice tools stop matching the Coldcard as soon as Add some dice rolls into the mix is used — same digits, different seed. The reason is that key 4 re-seeds the hasher with the current entropy instead of concatenating the raw rolls. Nothing is broken and no entropy is lost, but the scheme is undocumented, so no external tool reproduces it.

ccdice.py implements both formulas and refreshes the hash after every single roll, so you can compare against the Coldcard screen key press by key press and pinpoint exactly where any divergence appears. Single file, no dependencies, no network, self-testing against five publicly reproducible vectors.

python3 ccdice.py --selftest
python3 ccdice.py 24

⚠️ Warning

This script is meant only to verify that the COLDCARD (Mk4 / Q) firmware correctly implements the Dice Roll and Add some dice rolls into the mix functions.

It must never be used to generate a seed that will hold funds. Typing the rolls of a real seed into a computer defeats the entire purpose of a hardware signer. Only enter throwaway test sequences, to be destroyed after verification.


1. What the script does

It reimplements the Coldcard firmware mechanics exactly (shared/seed.py, functions add_dice_rolls / approve_word_list):

Stage Formula applied
Initial session (Dice Roll) SHA256(rolls_ascii)
Later session (key 4) SHA256(current_entropy ‖ rolls_ascii)

The entropy is then truncated to 16 bytes for 12 words, or kept at 32 bytes for 24 words, then converted into BIP39 words.

What other tools miss

The official rolls.py and third-party web tools only implement SHA256(all rolls concatenated). They do not model the re-seeding of the hasher with the current entropy when key 4 is pressed, and therefore diverge as soon as that function is used.

This is neither a bug nor a lost checksum: it is simply a chained mixing scheme that Coinkite does not document.

Two undocumented behaviors

Both stem from the key 4 handler in the firmware's shared/seed.py.

1. Randomness checks are disabled on added rolls.

The firmware validates dice rolls only during the initial generation session: it warns when a face exceeds 30% of the throws, and enforces a minimum roll count. The key 4 path calls the same routine with those checks turned off.

After pressing 4, the Coldcard performs no distribution check and enforces no minimum. You could add twenty rolls of all 1s and it would stay silent.

ccdice.py applies both checks to every block.

2. Truncation differs between 12 and 24-word seeds.

On confirmation, the entropy is truncated to 16 bytes for 12-word seeds and kept at the full 32 bytes for 24-word seeds. This carries over into the re-seeding value shown when key 4 is pressed:

Seed length Re-seeding value
12 words SHA256(16-byte entropy)
24 words SHA256(32-byte entropy)

The rolls themselves hash identically in both modes, so a 12/24 mismatch between the script and the device stays invisible during input and only diverges at confirmation.


2. Requirements

  • Python 3.6 or later
  • No external dependencies, no network access, no external files
  • A Unix terminal for key-by-key reading (automatic fallback otherwise)

Wordlist integrity check

The 2048 BIP39 words are embedded in the script. On startup, check_wordlist() rebuilds the canonical form of the reference file english.txt from the bitcoin/bips repository — one word per line, each ending with \n — and compares its digest against:

2f5eed53a4727b4bf8880d8f3f199efc90e58503646d9ff8eff3a2ed3b24dbda

This is an equivalence check against the official reference, not a file read. If a word were altered, missing or out of order in the script, the digest would diverge and execution would stop.

⚠️ Do not confuse this with entropy. This digest concerns the wordlist and nothing else. The starting entropy of a dice session — the value the Coldcard displays before the first roll — is an entirely separate value: e3b0c442…7852b855, the SHA256 of the empty string.


3. Built-in self-test

python3 ccdice.py --selftest

Run this before any use. The vectors come from observations made on real hardware, published publicly, and are reproducible by anyone on their own Coldcard.

All of them start from the same series of 60 rolls:

1111111111 2222222222 3333333333 4444444444 5555555555 6666666666

The five vectors in detail

1 — SHA256 of empty string (starting entropy)

Checks the initial state of the pool, before any roll.

e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

This is the SHA256 of the empty string. This value must match the one shown at the bottom of the Coldcard screen before the first roll.

2 — 12 words / 60 rolls

Validates the core mechanics: hashing of the ASCII string, truncation to 16 bytes, BIP39 conversion.

SHA256 entropy   : 76098bd44297998c1fd26314d49fbd560db54797e045d1e1773286f35e54f8b9
Truncated 16 B   : 76098bd44297998c1fd26314d49fbd56
Expected words   : invite era vital lunch kangaroo ship leg erase below pilot urban protect

3 — 12 words / 63 rolls (same session)

The same 60 rolls, followed by 222 entered without leaving the input screen.

SHA256 entropy   : f0e6b7664f783e3afa14498aa1188ac229f0e30e31f8c92799421d3f8a65c632
Expected words   : valley cup sunset page loop buddy tribe matrix melt angle cargo luggage

4 — Re-seeding entropy after [4]

The pool value at the moment key 4 is pressed to open Add some dice rolls into the mix, before any new roll.

5dd23ea0950ab9716a171affcaaf05145f9d35f8de1970d3bb2a70312a88dd7e

This is SHA256(76098bd4…d49fbd56), i.e. the hash of the entropy already truncated to 16 bytes. This is the heart of the mechanism, and the exact point where third-party tools break.

5 — 12 words / 60 then [4] 222

The same 60 rolls, confirmed, then 222 entered via key 4.

SHA256 entropy   : f488fcc1c3a4865ff5c41d38e463d95ab145a7283b5aaf01a6e0149df469fc37
Expected words   : virus elevator corn mansion embody copper strike lonely decline cart wagon remind

The demonstration lies in comparing vectors 3 and 5. Exactly the same 63 digits, in the same order. Only the way they are entered changes. Result: two entirely different seeds, each perfectly valid with a correct checksum.

Expected output

Embedded wordlist: 2048 words, matches the BIP39 reference
  SHA256 (canonical english.txt form): 2f5eed53a4727b4bf8880d8f3f19...
  -> wordlist integrity check, unrelated to entropy

[OK  ] SHA256 of empty string (starting entropy)
[OK  ] 12 words / 60 rolls
[OK  ] 12 words / 63 rolls (same session)
[OK  ] Re-seeding entropy after [4]
[OK  ] 12 words / 60 then [4] 222

ALL TESTS PASS

Exit code 0 if everything passes, 1 otherwise. If a single test fails, do not use the script: it does not reproduce the firmware mechanics.


4. Usage

python3 ccdice.py 12      # 12-word seed
python3 ccdice.py 24      # 24-word seed

Input is taken one roll at a time, with no need to press Enter: the script reproduces the behaviour of the Coldcard screen, which redisplays the hash after every key press (screen_updater() in the firmware loop).

Key Action
1 to 6 Enter a roll
ENTER Confirm the block (equivalent to OK on the Coldcard)
4 After confirming: continue with Add some dice rolls into the mix
q Finish

5. Reading the output

--------------------------------------------------------------------------
GENERATION
--------------------------------------------------------------------------
  Entropy BEFORE input (compare with the Coldcard screen):
  e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

  #0001  [3]  4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce
  #0002  [5]  1a5f1e2e3f4c8b9d0a7e6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a392817
  ...
  #0060  [6]  76098bd44297998c1fd26314d49fbd560db54797e045d1e1773286f35e54f8b9

  Entropy AFTER truncation (16 bytes):
  76098bd44297998c1fd26314d49fbd56

  BIP39 words (60 rolls total):
  invite era vital lunch kangaroo ship
  leg erase below pilot urban protect

  Block distribution: 1:10  2:10  3:10  4:10  5:10  6:10

  [4] add more rolls   [q] finish
Element Meaning
Entropy BEFORE input Pool state when the block opens
#NNNN [c] hash Roll number, face obtained, running SHA256
Entropy AFTER truncation Effective entropy used for the BIP39 conversion
Block distribution Count per face

Comparison points against the Coldcard:

  1. The entropy shown before the first roll of a block
  2. The hash after every roll, continuously
  3. The BIP39 words on confirmation

The hash shown during input is the full 32-byte SHA256 — that is indeed what the Coldcard displays. Truncation to 16 bytes (12 words) only happens on confirmation, and the script displays it separately.

Every ADD [key 4] block takes the re-seeding value as its BEFORE input entropy — the one from vector 4. This is the most discriminating comparison point.

Distribution check

The script reproduces the firmware test: if a face exceeds 30% of a block's rolls, it prints a warning equivalent to the Coldcard message "Distribution of dice rolls is not random". On a deliberately degenerate test series, this warning is normal and expected.

Thresholds

A warning is printed below 50 rolls (12 words) or 99 rolls (24 words), the thresholds enforced by the firmware.


6. Comparison procedure

  1. Run --selftest and check that all five vectors pass.
  2. On the Coldcard, open a dice input screen.
  3. Read the entropy shown before the first roll → it must be e3b0c4…52b855. Check that it matches in the script.
  4. Enter the rolls in parallel on both, comparing the hash after each press.
  5. Confirm on both sides, compare the resulting words.
  6. Press 4 on both sides, compare the re-seeding value, then continue entering in parallel.
  7. Destroy the roll notes and exit the test seed.

The point of comparing roll by roll: if a divergence occurs, you know at which exact roll it appears, instead of noticing a mismatch at the end of a block.


7. Limitations

  • Covers only the dice-only path. On the standard New Seed Words flow, the Coldcard mixes in entropy from its internal TRNG: the result is non-reproducible by design, and that is not an anomaly.
  • Does not verify firmware authenticity or signature. Malicious firmware can apply the correct formula and still divert funds elsewhere. This verification is a mathematical consistency check, not a proof of device integrity.
  • English wordlist only.

License

MIT — see LICENSE.

About

Independent Python script to verify Coldcard dice entropy — including the "Add dice rolls" re-seeding mechanism, unsupported by Coldcard's own script.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages