Практическое руководство: как самостоятельно получить энтропию игральной костью, превратить её в сид-фразу оффлайн и проверить результат тремя независимыми реализациями.
Гид подготовлен Тони⚡️. Поддержать проект.
Прецедент #
🥱 Если вы уже знаете причины, по которым полагаться на генераторы случайных чисел нежелательно, можете перейти к разделу матчасть.
В ночь с 30 на 31 июля с примерно пятисот биткоин-адресов одной серией вывели 594,48 BTC — около 38 миллионов долларов. 1324 UTXO ушли пятьюстами транзакциями в окне трёх блоков; 562 BTC затем свели на три адреса. Разбор цепочки принадлежит Робу Гамильтону из AnchorWatch и разошёлся по прессе — CoinDesk, TFTC, atlas21.
В тот же день Coinkite опубликовали предупреждение по Mk3 и (чуть позже) технический разбор, инженеры Block — собственное расследование, а исследователь LLFOURN — модель стоимости атаки для затронутых поколений устройств (на неё ссылаются сами Coinkite). Все пострадавшие адреса, по тому же ончейн-анализу, вели к кошелькам с одной подписью.
Причина, если коротко, оказалась не в криптографии, а в сборке. В 2021 году Coinkite переехали на библиотеку libsecp256k1 от Bitcoin Core и ради этого притащили в прошивку прослойку libNgU. При переезде генерация сида уехала с ckcc.rng_bytes() на ngu.random.bytes(), и этот путь разрешился в программный запасной генератор MicroPython вместо аппаратного TRNG. Собственный TRNG-код при этом никуда не делся, лежал в прошивке и даже работал — вот только, как пишет сам Новак:
При этом, написанный мной код TRNG действительно использовался — но лишь по случайности, и только для менее важных вещей.
Флаг MICROPY_HW_ENABLE_RNG был явно выставлен в ноль — в расчёте на то, что так отключаются обе ветки. Но проверка в коде была написана через #ifndef, то есть смотрела, определён ли макрос, а не равен ли он нулю. Определён — значит, #error не сработал, и сборка молча выбрала программную реализацию. Обе реализации имели одинаковую сигнатуру, нужный TRNG-код физически присутствовал в бинарнике — ревью это видело и успокаивалось.
Программный генератор строил “случайность” из заводских данных чипа и показаний таймеров. Coinkite оценивают оставшееся пространство поиска “при текущих предположениях об атаке” примерно в 40 бит на Mk3 и примерно в 72 бита на Mk4, Mk5 и Q — вместо положенных 128. Block считает отдельно и осторожнее: у них выходит верхняя граница около 2⁴⁰·⁷ для Mk2/Mk3 и менее 2⁷³·³ для актуальных моделей, причём они специально оговаривают, что 2⁷³ — это “широкая верхняя граница”, а не 73 бита реальной стойкости, потому что поля таймеров скоррелированы.
Сорок бит — это порядка 1,1 триллиона вариантов. Не “невозможно перебрать”, а “перебирают”: именно поэтому Coinkite сами предлагают считать такие сиды скомпрометированными.
Если ваш сид создавался на COLDCARD начиная с марта 2021 года и вы не добавляли при создании минимум 50 честных бросков кости — считайте его скомпрометированным. Обновление прошивки уже созданный сид не лечит, перенос сида в другое устройство не лечит тоже. Спасает только новый сид и миграция средств.
Затронуты все прошивки Mk3 начиная с 4.0.1 (март 2021) и любые последующие — так формулирует Coinkite; Block отсчитывает от 4.0.0 (17 марта 2021) и указывает диапазон Mk2/Mk3 4.0.0–4.1.9. Исправленные версии для актуальных моделей — 5.6.0 для Mk4 и Mk5 и 1.5.0Q для Q.
Что случилось — уже разобрали все кому не лень. Сейчас куда интереснее другой вопрос, и он практический: что делать дальше? Как создать новый сид так, чтобы качество ключа не зависело целиком от того, не ошибся ли производитель вашего аппаратного кошелька?
Ответ известен давно и он приятно скучный: возьмите энтропию сами. Игральной костью.
Главный урок #
У всей этой истории есть одно свойство, которое стоит проговорить прямо: баг был невидим. Устройство показывало красивые двенадцать слов, отпечаток, адрес — всё как обычно. Никакой индикации, что аппаратный источник случайности молчит, не было. Пользователь не мог это заметить в принципе, а ревью пропустило проблему, потому что нужный TRNG-код физически лежал в бинарнике; никто не проверил, какая именно реализация вызывается из ветки генерации сида.
Отсюда следует правило куда более важное, чем любые номера прошивок:
Всё, что вы не можете проверить, вы обязаны считать точкой уязвимости. Генератор случайных чисел внутри закрытой коробки проверить нельзя — по определению: и правильный, и сломанный генератор выдают внешне одинаковый результат.
Кость этой проблемы лишена. У неё нет прошивки, нет сабмодулей, нет флагов сборки. Вы видите результат каждого броска своими глазами, и — что важнее — весь дальнейший путь от бросков до слов вы можете повторить на трёх независимых реализациях и сверить, что они дали один и тот же ответ.
Матчасть #
Сид-фраза — это человекочитаемая запись случайного числа. Двенадцать слов кодируют 128 бит энтропии плюс 4 бита контрольной суммы, двадцать четыре слова — 256 бит плюс 8 бит контрольной суммы. Именно эти 128 или 256 бит и есть весь ваш кошелёк; всё остальное — производные.
Честная шестигранная кость даёт log₂6 = 2,585 бита на бросок. Отсюда два числа, которые вам нужно запомнить:
| Бросков | Энтропия | Сид |
|---|---|---|
| 50 | 129,2 бита | 12 слов |
| 99 | 255,9 бита | 24 слова |
Это ровно те пороги, которые советуют COLDCARD и SeedSigner, и ровно тот порог (50 бросков), ниже которого Coinkite теперь считает сид скомпрометированным.
Педанты заметят, что 99 бросков дают 255,9 бита, а не 256 — недобор в одну десятую бита. Практического значения это не имеет, а вот совместимость имеет: все реализации ждут ровно 50 или ровно 99 бросков. Не импровизируйте с количеством.
Как броски превращаются в слова #
Стандартный метод предельно прост:
- Записываете броски подряд как обычную строку цифр:
65515223131652... - Берёте от этой строки SHA256.
- Первые 128 бит хеша (для 12 слов) или все 256 (для 24 слов) — это ваша энтропия.
- Энтропия по правилам BIP-39 нарезается на слова, последнее содержит контрольную сумму.
Проверить второй шаг можно в одну команду на любом компьютере:
echo -n 123456 | sha256sum
# 8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92
Если решите проверить собственные броски, команда будет той же самой, только вместо
123456подставите свою строку целиком.
Это тот же пример, что приведён в документации COLDCARD. Ровно этот метод реализован в COLDCARD, в SeedSigner, в инструменте Иэна Коулмана и в Seed Tool — и именно поэтому их результаты можно сверять между собой.
Обратите внимание: SHA256 здесь ничего не “улучшает” и энтропию не создаёт. Он лишь равномерно размазывает те 129,2 бита, что вы набросали, по 128-битному полю. Если вы бросите кость десять раз, никакой хеш из этого 128 бит не сделает.
Что понадобится #
- Игральная кость. Одна, обычная шестигранная. Не набор из настолки с закруглёнными углами и выгрызенными точками — берите ровную, а лучше прозрачную “казиношную” с острыми гранями.
- Стаканчик или коробка, чтобы бросать с отскоком, а не аккуратно выкладывать кость на стол.
- Бумага и ручка. Обычные.
- Компьютер, который не подключён к интернету. Идеально — ноутбук, загруженный с флешки в Tails и физически отключённый от сети (кабель вынут, Wi-Fi выключен).
- Файл
bip39-standalone.htmlиз релизов инструмента Иэна Коулмана — скачанный заранее на другом компьютере и перенесённый флешкой.
Скачанный файл нужно проверить до того, как вы унесёте его на оффлайн-машину: сверитьsha256sum bip39-standalone.htmlс хешем в релизе и проверить PGP-подпись (gpg --verify signature.txt.asc). Инструмент, которым вы генерируете ключ, — это ровно та вещь, которую злоумышленнику интереснее всего подменить.
Шаг 0. Калибровка на тестовом векторе #
Прежде чем бросать кость по-настоящему, прогоните всю цепочку на чужих числах. У SeedSigner есть официальный документ по верификации с опубликованными тестовыми векторами. Я пересчитал их независимо — они сходятся:
50 бросков → 12 слов
Броски: 65515223131652132161133154444123616466443112153441
SHA256: 6cb09af855050dcde6fe2adc3181c250982011e2cf17821cbed56a908ec527c3
Слова: hole luggage safe present express tragic orbit shed switch metal identify path
Отпечаток (XFP): 8D9CCED8
Первый адрес m/84'/0'/0'/0/0: bc1q00lln3r4mt4uwvg7mxv96xgpewauwmggkex2ff
99 бросков → 24 слова
Броски: 365562562366324323462114514522636261416651215635464243544344561642216122212163156245311666125142556
SHA256: 8cb7a6c94273dce9f538601075e4f7633b6880d7ae4355555a06f43c7b29b66d
Слова: million rule rather lumber differ insane stay army away quality differ shoe repair document gadget silent fetch print like trick sibling network super rotate
Отпечаток (XFP): 75c9674c
Первый адрес m/84'/0'/0'/0/0: bc1qvg76yg9k0gsg9dad9fh39szc9mfr0mjaucp7ge
Введите эти броски в свой инструмент и убедитесь, что получаете точно такие же слова. Если получились другие — вы что-то настроили не так, и это надо выяснить сейчас, на публичных числах, а не потом, на числах, которыми вы собираетесь защищать сбережения.
Эти сиды опубликованы. Никогда не отправляйте на них средства — их видит весь интернет.
Шаг 1. Оффлайн-окружение #
Загружаемся с флешки в Tails, отключаем сеть, открываем bip39-standalone.html в браузере. Никаких других вкладок, никакого “а заодно посмотрю курс”.
Ставим галочку Show entropy details — появляется поле для ввода собственной энтропии.
Шаг 2. Бросаем кость #
Пятьдесят раз для двенадцати слов, девяносто девять — для двадцати четырёх. Правила простые и все они существуют не для красоты:
- Бросайте из стаканчика, с отскоком от стенки. Кость, которую аккуратно кладут на стол, — это не случайность.
- Записывайте каждый бросок, включая “некрасивые”. Три шестёрки подряд — нормальный результат, и выбросить его означает уничтожить часть энтропии. Любая последовательность равновероятна.
- Не перебрасывайте “неудачные” броски. Как только вы начали выбирать, случайность закончилась.
- Записывайте на бумагу, ручкой, в один ряд.
- Делайте это в одиночестве. Камера телефона, видеоняня, окно напротив — всё это каналы утечки ключа.
Последовательность бросков — это и есть ваш приватный ключ, ровно в той же степени, что и сид-фраза. Никогда не фотографируйте её, не набирайте на подключённом к сети компьютере, не сохраняйте в заметках. После получения слов бумажку с бросками нужно уничтожить.
Шаг 3. Превращаем броски в слова #
А вот здесь — самое важное место всего гида, и именно на нём люди спотыкаются.
В инструменте Коулмана есть переключатель типа энтропии: Binary, Base 6, Dice, Base 10, Hex, Card. Кажется очевидным, что для бросков кости нужно выбрать Dice.
Выбирать Dice нельзя.
Ловушка режима “Dice” #
В режиме Dice инструмент сначала переводит броски в базу 6, заменяя все шестёрки на нули, и только потом хеширует. То есть в SHA256 уходит не та строка, которую вы записали на бумаге, а другая.

Режим Dice: шестёрки заменены на нули в Filtered Entropy
COLDCARD и SeedSigner хешируют строку как есть:

Режим Base 10: Filtered Entropy совпадает со строкой бросков
Результат — два совершенно разных кошелька из одних и тех же бросков. Вот тот же тестовый вектор из 50 бросков, посчитанный обоими способами:
| Режим | Полученные слова |
|---|---|
| Base 10 (правильно) | million rule rather lumber differ insane stay army away quality differ shoe repair document gadget silent fetch print like trick sibling network super rotate |
| Dice (ловушка) | bubble sell damp rural chat alcohol number type garlic sphere property endorse allow siege connect spirit during dream laundry noise security enhance shed question |
Ничего общего. Разные отпечатки, разные адреса, разные деньги. Об этом прямо написано в исходниках SeedSigner, в комментарии к функции генерации сида:
Важное замечание: этот метод НЕ совместим с режимом “Dice” из инструмента iancoleman.
Выбирайте тип энтропии вручную:
Base 10 [0-9](илиHex [0-9A-F]— для цифр 1–6 они дают одинаковый результат). Не полагайтесь на автоопределение: строку из цифр 1–6 инструмент сам определит какDiceи молча уведёт вас в другой кошелёк.Проверить, что всё правильно, можно прямо на экране: в блоке Filtered Entropy должна отображаться ваша строка бросков как есть. Если вместо шестёрок там нули — вы в режиме
Dice, остановитесь.
Дальше в выпадающем списке Mnemonic Length выбираем 12 Words или 24 Words. По умолчанию там стоит Use Raw Entropy — это другой режим, он не хеширует броски и даст третий, тоже несовместимый результат.
Слова появятся в поле BIP39 Mnemonic.
Про красное предупреждение #
Если вы всё же заглянете в режим Dice, инструмент покажет тревожную плашку: “The mnemonic will appear more secure than it really is”. В режиме Base 10 её не будет. Стоит понимать, почему, — иначе легко испугаться не того.
Инструмент считает “Total Bits” по своей внутренней шкале, и она не равна реальной энтропии ваших бросков:
- в режиме
Diceприменяется нарочито консервативное несмещённое извлечение — 1,67 бита на бросок. Для 50 бросков это 84 “бита”, меньше 128, отсюда и предупреждение; - в режиме
Base 10каждая цифра кодируется тремя битами, для 50 бросков получается “150 бит” — а это уже завышение.
Правильный ответ — ни то, ни другое. Реальная энтропия честной кости составляет 2,585 бита на бросок, то есть 129,2 бита за 50 бросков. Этого достаточно для 12 слов, и именно на этой цифре стоят пороги 50 и 99.
Иными словами: в режимеDiceпредупреждение излишне строгое, а в режимеBase 10его отсутствие излишне оптимистично. Но выбирать всё равно надоBase 10— не ради циферки на экране, а ради совместимости с остальными реализациями. Только она даёт возможность перекрёстной проверки.
Шаг 4. Перекрёстная проверка #
Это то, ради чего всё затевалось. Одна реализация может врать; три независимые, написанные на разных языках разными людьми, врать одинаково не могут.
Проверка 1 — хеш, посчитанный руками. Инструмент считает SHA256 сам, внутри себя. Посчитайте его независимо, в терминале Tails:
echo -n <ваши броски> | sha256sum
Возьмите первые 32 символа полученного хеша (для 12 слов) или все 64 (для 24 слов) и введите их в то же поле энтропии, но с типом Hex [0-9A-F] и длиной Use Raw Entropy. В этом режиме инструмент ничего не хеширует, а берёт биты как есть — и должен выдать ровно те же слова.
На тестовом векторе это выглядит так:
echo -n 65515223131652132161133154444123616466443112153441 | sha256sum
# 6cb09af855050dcde6fe2adc3181c250982011e2cf17821cbed56a908ec527c3
# первые 32 символа → Hex + Use Raw Entropy →
# hole luggage safe present express tragic orbit shed switch metal identify path
Слова совпали с теми, что вы получили из бросков в режиме Base 10. Значит, шаг “броски → SHA256” инструмент выполнил именно так, как вы ожидали.
Проверка 2 — второй независимый инструмент. Seed Tool (bitcoiner.guide) — отдельная кодовая база, работает так же: раздел Seed Generation Input → Show the Entropy Section, тип Hex, длина 12 Words / 24 Words. Файл тоже качается заранее и запускается оффлайн.
Проверка 3 — скрипт от производителя. У COLDCARD есть rolls.py, у SeedSigner — утилита tools/mnemonic.py, запускающая ровно тот же код, что работает на устройстве:
cd tools
python3 mnemonic.py dice 65515223131652132161133154444123616466443112153441
Совпали все? Значит, ваши слова — действительно результат ваших бросков, и никакой сломанный build-скрипт между вами и ключом не стоял.
Шаг 5. Переносим сид в устройство #
Теперь у вас есть сид, созданный без участия чьего-либо генератора случайных чисел. Его можно ввести в любое устройство как существующий:
- COLDCARD:
Import Existing→24 Words(или12 Words) → ввод слов. - SeedSigner:
Seeds→Enter seed words. - Sparrow, Electrum и прочее ПО: обычный импорт мнемоники.
И сразу же — финальная сверка, самая важная:
- Сгенерируйте на устройстве первый адрес получения и посмотрите его на экране самого устройства, а не в приложении на компьютере.
- Сравните с адресом, который выдал оффлайн-инструмент для того же пути деривации (по умолчанию для нативного SegWit это
m/84'/0'/0'/0/0). - Заодно запишите рядом с сид-фразой отпечаток кошелька (XFP) — устройство показывает его само. Дальше он пригодится каждый раз, когда вы будете подключать кошелёк к новому ПО или применять парольную фразу.
Совпало — цепочка сошлась целиком: от бумажки с бросками до адреса на экране аппаратного кошелька. Не совпало — не отправляйте на этот адрес ничего, пока не разберётесь. Чаще всего причина в разном типе скрипта (Legacy / Nested SegWit / Native SegWit / Taproot) или в том, что где-то применилась парольная фраза.
Парольная фраза BIP-39 — отдельный, независимый слой, и он никак не связан с этой историей. Помните: любая парольная фраза, включая опечатку, порождает валидный кошелёк. Всегда сверяйте XFP перед отправкой средств. Подробнее — в гиде по COLDCARD и в документации производителя.
Шаг 6. Уборка #
- Уничтожьте бумажку с бросками. Броски — это ключ.
- Выключите Tails. Сессия эфемерная, после перезагрузки от неё ничего не остаётся.
- Сид-фраза остаётся на бумаге или металле, парольная фраза — отдельно от неё.
- Отправьте небольшую тестовую сумму, дождитесь подтверждения, попробуйте её потратить. Только после этого переносите основное.
Проверьте восстановление до того, как положите на кошелёк серьёзные деньги. Бэкап, который ни разу не восстанавливали, — это не бэкап, а надежда.
Варианты и альтернативы #
Монетка вместо кости #
Тот же метод работает с монетой: 128 бросков для 12 слов, 256 — для 24. Записываете нули и единицы, тип энтропии — Binary, длина — 12 или 24 слова. Один бросок — ровно один бит, без всякой возни с базой 6 и без споров о том, сколько там бит на бросок. Минус очевиден: 256 подбрасываний монеты — это долго и утомительно, и на семидесятом броске велик соблазн “дописать примерно так же”.
Кости прямо на устройстве #
И COLDCARD, и SeedSigner умеют принимать броски сами: у SeedSigner это Tools → New Seed → 🎲, у COLDCARD — Import Existing → Dice Rolls (описание процедуры). Этот путь хеширует последовательность бросков напрямую, не трогая генератор устройства, и результат вы можете точно так же перепроверить оффлайн-инструментом.
Способ проще описанного выше и почти так же хорош. “Почти” — потому что вы вводите броски в устройство и обязаны верить, что оно захешировало именно их. Перекрёстная проверка снимает и это возражение: сверьте слова с тем, что выдал оффлайн-инструмент на той же строке.
Гибрид: свои кости плюс энтропия устройства #
У COLDCARD есть функция Add Dice Rolls — устройство подмешивает ваши броски к собственной энтропии. Это именно то, что спасло часть пользователей в нынешней истории: сломанный генератор давал мало энтропии, но 50 честных бросков сверху перекрывали недостачу целиком.
Хороший режим по умолчанию, если вы не хотите отдельного ритуала с Tails. Но помните, чем он отличается: здесь вы не проверяете результат, а страхуете его. Если генератор устройства окажется не просто слабым, а враждебным, подмешивание не спасёт.
Мультисиг #
По тому же ончейн-анализу, в выводе средств пострадали кошельки с одной подписью. Отсюда легко сделать вывод “мультисиг спасает” — и здесь надо быть аккуратным, потому что вывод верен только наполовину.
Инженеры Block проговаривают ограничение прямым текстом:
Даже если COLDCARD используется в схеме мультисиг, если эта схема составлена исключительно из уязвимых устройств, воздействие уязвимости сохраняется. Для защиты от этой проблемы необходим кворум из защищённых устройств.
То есть мультисиг из трёх COLDCARD на затронутой прошивке не защищает ни от чего: все три ключа выведены одним и тем же сломанным генератором, и перебираются они так же, как один. Защищает не мультисиг сам по себе, а разнородность кворума — устройства разных производителей на разных кодовых базах. Чтобы вскрыть такой кошелёк, багу нужно случиться независимо в нескольких местах сразу.
Подробнее о том что такое мультисиг и как настроить такую схему читайте в этом материале
А то, что в этом конкретном инциденте не тронули мультисиг, говорит скорее о выборе целей атакующим, чем о неуязвимости схемы.
Если сумма для вас значима, разнородный мультисиг даёт больше, чем самый ответственный ритуал с костями на одном устройстве. Кости и мультисиг при этом отлично сочетаются.
Кому всё это не нужно #
Ритуал с костями превращает вас в единственную точку отказа. Ошибиться здесь легче, чем кажется: не тот режим в инструменте (см. выше), пропущенный бросок, “подправленная” последовательность, оставленная в ящике бумажка с бросками, опечатка при переносе слов. Все эти ошибки тише и вероятнее, чем баг в прошивке, — и потери от них абсолютно такие же.
Если вы держите сумму, которую вам не жалко, и не готовы возиться с Tails и тремя перепроверками — обновите прошивку, сгенерируйте сид на устройстве и живите спокойно. Исправленная прошивка делает то, что должна.
Кости имеют смысл, когда:
- сумма такая, что вам действительно важно исключить чужой генератор из уравнения;
- вы храните вдолгую и хотите, чтобы качество ключа не зависело от того, что вскроется в чьём-то репозитории через пять лет;
- вы уже пострадали от этой истории и восстанавливаете доверие с нуля;
- вам просто нравится, когда каждый шаг можно проверить самому.
Чего делать нельзя #
- Не вводите сид-фразу ни на каких сайтах. Никогда. После любого громкого инцидента появляется волна “проверялок уязвимости” и писем “от Coinkite” — все они существуют ровно для того, чтобы вы ввели свои слова. Настоящая проверка не требует сида: она требует посмотреть, на какой прошивке создавался ключ.
- Не придумывайте случайность. Слова из книги, дата рождения, “случайные” нажатия по клавиатуре, “я просто написал цифры наугад” — всё это перебирается быстрее, чем вы дочитаете этот абзац.
- Не используйте онлайн-инструмент. Тот же самый файл
bip39-standalone.html, открытый в браузере, подключённом к интернету, — это уже не оффлайн-генерация. - Не фотографируйте броски и слова. Фотография уходит в облако быстрее, чем вы успеваете об этом подумать.
- Не спешите. Паническая миграция угробила больше монет, чем большинство уязвимостей. Новый сид, проверка бэкапа, проверка адреса на экране, тестовая транзакция — и только потом остальное.
Итог #
Вся криптография Биткоина держится на двух вещах: на больших простых числах и на честной случайности. Первое проверить нельзя — приходится верить математике. Второе проверить можно, и в этом вся суть.
Метод “кости → SHA256 → BIP-39” хорош не тем, что кость какая-то особенно случайная. Он хорош тем, что каждый шаг здесь наблюдаем и воспроизводим: броски вы видите своими глазами, хеш считаете одной командой, слова получаете тремя-четырьмя разными программами и сверяете между собой, адрес сравниваете с экраном устройства. Нигде в этой цепочке нет места, где что-то может тихо сломаться и не подать виду.
Именно этого не хватило COLDCARD в марте 2021 года — и стоило это 594 биткоина.
